OdfDocument.java
/**
* **********************************************************************
*
* <p>DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
*
* <p>Copyright 2008, 2010 Oracle and/or its affiliates. All rights reserved.
*
* <p>Use is subject to license terms.
*
* <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0. You can also obtain a copy of the License at
* http://odftoolkit.org/docs/license.txt
*
* <p>Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied.
*
* <p>See the License for the specific language governing permissions and limitations under the
* License.
*
* <p>**********************************************************************
*/
package org.odftoolkit.odfdom.doc;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import org.odftoolkit.odfdom.doc.table.OdfTable;
import org.odftoolkit.odfdom.dom.OdfContentDom;
import org.odftoolkit.odfdom.dom.OdfDocumentNamespace;
import org.odftoolkit.odfdom.dom.OdfMetaDom;
import org.odftoolkit.odfdom.dom.OdfSchemaConstraint;
import org.odftoolkit.odfdom.dom.OdfSchemaDocument;
import org.odftoolkit.odfdom.dom.attribute.text.TextAnchorTypeAttribute;
import org.odftoolkit.odfdom.dom.element.draw.DrawPageElement;
import org.odftoolkit.odfdom.dom.element.office.OfficeAnnotationElement;
import org.odftoolkit.odfdom.dom.element.office.OfficeBodyElement;
import org.odftoolkit.odfdom.dom.element.table.TableTableCellElement;
import org.odftoolkit.odfdom.dom.element.table.TableTableElement;
import org.odftoolkit.odfdom.dom.element.text.TextPElement;
import org.odftoolkit.odfdom.dom.style.OdfStyleFamily;
import org.odftoolkit.odfdom.dom.style.props.OdfStyleProperty;
import org.odftoolkit.odfdom.dom.style.props.OdfTextProperties;
import org.odftoolkit.odfdom.incubator.doc.draw.OdfDrawFrame;
import org.odftoolkit.odfdom.incubator.doc.draw.OdfDrawImage;
import org.odftoolkit.odfdom.incubator.doc.office.OdfOfficeStyles;
import org.odftoolkit.odfdom.incubator.doc.style.OdfDefaultStyle;
import org.odftoolkit.odfdom.incubator.meta.OdfOfficeMeta;
import org.odftoolkit.odfdom.pkg.MediaType;
import org.odftoolkit.odfdom.pkg.OdfElement;
import org.odftoolkit.odfdom.pkg.OdfName;
import org.odftoolkit.odfdom.pkg.OdfPackage;
import org.odftoolkit.odfdom.pkg.OdfValidationException;
import org.odftoolkit.odfdom.type.Duration;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
/** This abstract class is representing one of the possible ODF documents. */
public abstract class OdfDocument extends OdfSchemaDocument {
// Static parts of file references
private static final String SLASH = "/";
private OdfMediaType mMediaType;
private OdfOfficeMeta mOfficeMeta;
private long documentOpeningTime;
private static final Pattern CONTROL_CHAR_PATTERN = Pattern.compile("\\p{Cntrl}");
private static final String EMPTY_STRING = "";
private Calendar mCreationDate;
private static final String FORMER_OPEN_OFFICE_VERSION =
"StarOffice/8$Win32 OpenOffice.org_project/680m18$Build-9161";
protected Boolean mHasCollaboration = null;
// Using static factory instead of constructor
protected OdfDocument(OdfPackage pkg, String internalPath, OdfMediaType mediaType)
throws SAXException {
super(pkg, internalPath, mediaType.getMediaTypeString());
mMediaType = mediaType;
// set document opening time.
documentOpeningTime = System.currentTimeMillis();
}
/** This enum contains all possible media types of OpenDocument documents. */
public enum OdfMediaType implements MediaType {
CHART("application/vnd.oasis.opendocument.chart", "odc"),
CHART_TEMPLATE("application/vnd.oasis.opendocument.chart-template", "otc"),
FORMULA("application/vnd.oasis.opendocument.formula", "odf"),
FORMULA_TEMPLATE("application/vnd.oasis.opendocument.formula-template", "otf"),
DATABASE_FRONT_END("application/vnd.oasis.opendocument.base", "odb"),
GRAPHICS("application/vnd.oasis.opendocument.graphics", "odg"),
GRAPHICS_TEMPLATE("application/vnd.oasis.opendocument.graphics-template", "otg"),
IMAGE("application/vnd.oasis.opendocument.image", "odi"),
IMAGE_TEMPLATE("application/vnd.oasis.opendocument.image-template", "oti"),
PRESENTATION("application/vnd.oasis.opendocument.presentation", "odp"),
PRESENTATION_TEMPLATE("application/vnd.oasis.opendocument.presentation-template", "otp"),
SPREADSHEET("application/vnd.oasis.opendocument.spreadsheet", "ods"),
SPREADSHEET_TEMPLATE("application/vnd.oasis.opendocument.spreadsheet-template", "ots"),
TEXT("application/vnd.oasis.opendocument.text", "odt"),
TEXT_MASTER("application/vnd.oasis.opendocument.text-master", "odm"),
TEXT_TEMPLATE("application/vnd.oasis.opendocument.text-template", "ott"),
TEXT_WEB("application/vnd.oasis.opendocument.text-web", "oth");
private final String mMediaType;
private final String mSuffix;
OdfMediaType(String mediaType, String suffix) {
this.mMediaType = mediaType;
this.mSuffix = suffix;
}
/** @return the mediatype String of this document */
@Override
public String getMediaTypeString() {
return mMediaType;
}
/** @return the ODF filesuffix of this document */
@Override
public String getSuffix() {
return mSuffix;
}
/**
* @param mediaType string defining an ODF document
* @return the according OdfMediatype encapsulating the given string and the suffix
*/
public static OdfMediaType getOdfMediaType(String mediaType) {
OdfMediaType odfMediaType = null;
if (mediaType != null) {
String mediaTypeShort =
mediaType.substring(mediaType.lastIndexOf(".") + 1, mediaType.length());
mediaTypeShort = mediaTypeShort.replace('-', '_').toUpperCase();
try {
odfMediaType = OdfMediaType.valueOf(mediaTypeShort);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Given mediaType '" + mediaType + "' is not an ODF mediatype!");
}
}
return odfMediaType;
}
}
/**
* Loads the ODF root document from the given Resource.
*
* <p>NOTE: Initial meta data (like the document creation time) will be added in this method.
*
* @param res a resource containing a package with a root document
* @param odfMediaType the media type of the root document
* @return the OpenDocument document or NULL if the media type is not supported by ODFDOM.
* @throws java.lang.Exception - if the document could not be created.
*/
protected static OdfDocument loadTemplate(Resource res, OdfMediaType odfMediaType)
throws Exception {
InputStream in = res.createInputStream();
OdfPackage pkg = null;
try {
pkg = OdfPackage.loadPackage(in);
} finally {
in.close();
}
OdfDocument newDocument = newDocument(pkg, ROOT_DOCUMENT_PATH, odfMediaType, Boolean.FALSE);
// add creation time, the metadata have to be explicitly set
newDocument.mCreationDate = Calendar.getInstance();
return newDocument;
}
/**
* Loads the ODF root document from the given Resource.
*
* <p>NOTE: Initial meta data (like the document creation time) will be added in this method.
*
* @param res a resource containing a package with a root document
* @param odfMediaType the media type of the root document
* @param enableCollaboration - user changes equivalent for creating this document are gathered
* @return the OpenDocument document or NULL if the media type is not supported by ODFDOM.
* @throws java.lang.Exception - if the document could not be created.
*/
protected static OdfDocument loadTemplate(
Resource res, OdfMediaType odfMediaType, Boolean enableCollaboration) throws Exception {
InputStream in = res.createInputStream();
OdfPackage pkg = null;
try {
pkg = OdfPackage.loadPackage(in);
} finally {
in.close();
}
OdfDocument newDocument =
newDocument(pkg, ROOT_DOCUMENT_PATH, odfMediaType, enableCollaboration);
// add creation time, the metadata have to be explicitly set
newDocument.mCreationDate = Calendar.getInstance();
return newDocument;
}
/**
* Loads the ODF root document from the ODF package provided by its path.
*
* <p>OdfDocument relies on the file being available for read access over the whole life-cycle of
* OdfDocument.
*
* @param documentPath - the path from where the document can be loaded
* @return the OpenDocument from the given path or NULL if the media type is not supported by
* ODFDOM.
* @throws java.lang.Exception - if the document could not be created.
*/
public static OdfDocument loadDocument(String documentPath) throws Exception {
return loadDocument(OdfPackage.loadPackage(documentPath));
}
/**
* Loads the ODF root document from the ODF package provided by a Stream.
*
* <p>Since an InputStream does not provide the arbitrary (non sequential) read access needed by
* OdfDocument, the InputStream is cached. This usually takes more time compared to the other
* createInternalDocument methods. An advantage of caching is that there are no problems
* overwriting an input file.
*
* @param inStream - the InputStream of the ODF document.
* @param configuration - key/value pairs of user given run-time settings (configuration)
* @return the document created from the given InputStream
* @throws java.lang.Exception - if the document could not be created.
*/
public static OdfDocument loadDocument(InputStream inStream, Map<String, Object> configuration)
throws Exception {
return loadDocument(OdfPackage.loadPackage(inStream, configuration));
}
/**
* Loads the ODF root document from the ODF package provided by a Stream.
*
* <p>Since an InputStream does not provide the arbitrary (non sequential) read access needed by
* OdfDocument, the InputStream is cached. This usually takes more time compared to the other
* createInternalDocument methods. An advantage of caching is that there are no problems
* overwriting an input file.
*
* @param inStream - the InputStream of the ODF document.
* @param configuration - key/value pairs of user given run-time settings (configuration)
* @param enableCollaboration - user changes equivalent for creating this document are gathered
* @return the document created from the given InputStream
* @throws java.lang.Exception - if the document could not be created.
*/
public static OdfDocument loadDocument(
InputStream inStream, Map<String, Object> configuration, Boolean enableCollaboration)
throws Exception {
return loadDocument(OdfPackage.loadPackage(inStream, configuration), enableCollaboration);
}
/**
* Loads the ODF root document from the ODF package provided by a Stream.
*
* <p>Since an InputStream does not provide the arbitrary (non sequential) read access needed by
* OdfDocument, the InputStream is cached. This usually takes more time compared to the other
* createInternalDocument methods. An advantage of caching is that there are no problems
* overwriting an input file.
*
* @param inStream - the InputStream of the ODF document.
* @return the document created from the given InputStream
* @throws java.lang.Exception - if the document could not be created.
*/
public static OdfDocument loadDocument(InputStream inStream) throws Exception {
return loadDocument(OdfPackage.loadPackage(inStream));
}
/**
* Loads the ODF root document from the ODF package provided by a Stream.
*
* <p>Since an InputStream does not provide the arbitrary (non sequential) read access needed by
* OdfDocument, the InputStream is cached. This usually takes more time compared to the other
* createInternalDocument methods. An advantage of caching is that there are no problems
* overwriting an input file.
*
* @param inStream - the InputStream of the ODF document.
* @param enableCollaboration - user changes equivalent for creating this document are gathered
* @return the document created from the given InputStream
* @throws java.lang.Exception - if the document could not be created.
*/
public static OdfDocument loadDocument(InputStream inStream, Boolean enableCollaboration)
throws Exception {
return loadDocument(OdfPackage.loadPackage(inStream), enableCollaboration);
}
/**
* Loads the ODF root document from the ODF package provided as a File.
*
* @param file - a file representing the ODF document.
* @return the document created from the given File
* @throws java.lang.Exception - if the document could not be created.
*/
public static OdfDocument loadDocument(File file) throws Exception {
return loadDocument(OdfPackage.loadPackage(file));
}
/**
* Loads the ODF root document from the ODF package.
*
* @param odfPackage - the ODF package containing the ODF document.
* @return the root document of the given OdfPackage
* @throws java.lang.Exception - if the ODF document could not be created.
*/
public static OdfDocument loadDocument(OdfPackage odfPackage) throws Exception {
return loadDocument(odfPackage, ROOT_DOCUMENT_PATH);
}
/**
* Loads the ODF root document from the ODF package.
*
* @param odfPackage - the ODF package containing the ODF document.
* @param enableCollaboration - user changes equivalent for creating this document are gathered
* @return the root document of the given OdfPackage
* @throws java.lang.Exception - if the ODF document could not be created.
*/
public static OdfDocument loadDocument(OdfPackage odfPackage, Boolean enableCollaboration)
throws Exception {
return loadDocument(odfPackage, ROOT_DOCUMENT_PATH, enableCollaboration);
}
/**
* Creates an OdfDocument from the OpenDocument provided by an ODF package.
*
* @param odfPackage - the ODF package containing the ODF document.
* @param internalPath - the path to the ODF document relative to the package root, or an empty
* String for the root document.
* @return the root document of the given OdfPackage
* @throws java.lang.Exception - if the ODF document could not be created.
*/
public static OdfDocument loadDocument(OdfPackage odfPackage, String internalPath)
throws Exception {
return loadDocument(odfPackage, internalPath, Boolean.FALSE);
}
/**
* Creates an OdfDocument from the OpenDocument provided by an ODF package.
*
* @param odfPackage - the ODF package containing the ODF document.
* @param internalPath - the path to the ODF document relative to the package root, or an empty
* String for the root document.
* @param enableCollaboration - user changes equivalent for creating this document are gathered
* @return the root document of the given OdfPackage
* @throws java.lang.Exception - if the ODF document could not be created.
*/
public static OdfDocument loadDocument(
OdfPackage odfPackage, String internalPath, Boolean enableCollaboration) throws Exception {
String documentMediaType = odfPackage.getMediaTypeString(internalPath);
OdfMediaType odfMediaType = null;
try {
odfMediaType = OdfMediaType.getOdfMediaType(documentMediaType);
} catch (IllegalArgumentException e) {
// the returned NULL will be taking care of afterwards
}
if (odfMediaType == null) {
ErrorHandler errorHandler = odfPackage.getErrorHandler();
if (documentMediaType != null) {
Matcher matcherCTRL = CONTROL_CHAR_PATTERN.matcher(documentMediaType);
if (matcherCTRL.find()) {
documentMediaType = matcherCTRL.replaceAll(EMPTY_STRING);
}
}
OdfValidationException ve =
new OdfValidationException(
OdfSchemaConstraint.DOCUMENT_WITHOUT_ODF_MIMETYPE, internalPath, documentMediaType);
if (errorHandler != null) {
errorHandler.fatalError(ve);
}
throw ve;
}
return newDocument(odfPackage, internalPath, odfMediaType, enableCollaboration);
}
// return null if the media type can not be recognized.
private static OdfDocument loadDocumentFromTemplate(OdfMediaType odfMediaType) throws Exception {
final Resource documentTemplate;
switch (odfMediaType) {
case TEXT:
case TEXT_TEMPLATE:
case TEXT_MASTER:
case TEXT_WEB:
documentTemplate = OdfTextDocument.EMPTY_TEXT_DOCUMENT_RESOURCE;
break;
case SPREADSHEET:
case SPREADSHEET_TEMPLATE:
documentTemplate = OdfSpreadsheetDocument.EMPTY_SPREADSHEET_DOCUMENT_RESOURCE;
break;
case PRESENTATION:
case PRESENTATION_TEMPLATE:
documentTemplate = OdfPresentationDocument.EMPTY_PRESENTATION_DOCUMENT_RESOURCE;
break;
case GRAPHICS:
case GRAPHICS_TEMPLATE:
documentTemplate = OdfGraphicsDocument.EMPTY_GRAPHICS_DOCUMENT_RESOURCE;
break;
case CHART:
case CHART_TEMPLATE:
documentTemplate = OdfChartDocument.EMPTY_CHART_DOCUMENT_RESOURCE;
break;
case IMAGE:
case IMAGE_TEMPLATE:
documentTemplate = OdfImageDocument.EMPTY_IMAGE_DOCUMENT_RESOURCE;
break;
default:
documentTemplate = null;
throw new IllegalArgumentException(
"Given mediaType '" + odfMediaType.mMediaType + "' is not yet supported!");
}
return loadTemplate(documentTemplate, odfMediaType);
}
/**
* Creates one of the ODF documents based a given mediatype.
*
* @param odfMediaType The ODF Mediatype of the ODF document to be created.
* @param enableCollaboration - user changes equivalent for creating this document are gathered
* @return The ODF document, which mediatype dependends on the parameter or NULL if media type
* were not supported.
*/
private static OdfDocument newDocument(
OdfPackage pkg, String internalPath, OdfMediaType odfMediaType, Boolean enableCollaboration)
throws SAXException {
OdfDocument newDoc = null;
switch (odfMediaType) {
case TEXT:
newDoc =
new OdfTextDocument(
pkg, internalPath, OdfTextDocument.OdfMediaType.TEXT, enableCollaboration);
break;
case TEXT_TEMPLATE:
newDoc =
new OdfTextDocument(
pkg, internalPath, OdfTextDocument.OdfMediaType.TEXT_TEMPLATE, enableCollaboration);
break;
case TEXT_MASTER:
newDoc =
new OdfTextDocument(
pkg, internalPath, OdfTextDocument.OdfMediaType.TEXT_MASTER, enableCollaboration);
break;
case TEXT_WEB:
newDoc =
new OdfTextDocument(
pkg, internalPath, OdfTextDocument.OdfMediaType.TEXT_WEB, enableCollaboration);
break;
case SPREADSHEET:
newDoc =
new OdfSpreadsheetDocument(
pkg, internalPath, OdfSpreadsheetDocument.OdfMediaType.SPREADSHEET);
break;
case SPREADSHEET_TEMPLATE:
newDoc =
new OdfSpreadsheetDocument(
pkg, internalPath, OdfSpreadsheetDocument.OdfMediaType.SPREADSHEET_TEMPLATE);
break;
case PRESENTATION:
newDoc =
new OdfPresentationDocument(
pkg, internalPath, OdfPresentationDocument.OdfMediaType.PRESENTATION);
break;
case PRESENTATION_TEMPLATE:
newDoc =
new OdfPresentationDocument(
pkg, internalPath, OdfPresentationDocument.OdfMediaType.PRESENTATION_TEMPLATE);
break;
case GRAPHICS:
newDoc =
new OdfGraphicsDocument(pkg, internalPath, OdfGraphicsDocument.OdfMediaType.GRAPHICS);
break;
case GRAPHICS_TEMPLATE:
newDoc =
new OdfGraphicsDocument(
pkg, internalPath, OdfGraphicsDocument.OdfMediaType.GRAPHICS_TEMPLATE);
break;
case CHART:
newDoc = new OdfChartDocument(pkg, internalPath, OdfChartDocument.OdfMediaType.CHART);
break;
case CHART_TEMPLATE:
newDoc =
new OdfChartDocument(pkg, internalPath, OdfChartDocument.OdfMediaType.CHART_TEMPLATE);
break;
case IMAGE:
newDoc = new OdfImageDocument(pkg, internalPath, OdfImageDocument.OdfMediaType.IMAGE);
break;
case IMAGE_TEMPLATE:
newDoc =
new OdfImageDocument(pkg, internalPath, OdfImageDocument.OdfMediaType.IMAGE_TEMPLATE);
break;
default:
newDoc = null;
throw new IllegalArgumentException(
"Given mediaType '" + odfMediaType.mMediaType + "' is not yet supported!");
}
// returning null if MediaType is not supported
return newDoc;
}
/**
* Returns an embedded OdfPackageDocument from the given package path.
*
* @param documentPath to the ODF document within the package. The path is relative the current
* ODF document path.
* @return an embedded OdfPackageDocument
*/
@Override
public OdfDocument loadSubDocument(String documentPath) {
return (OdfDocument) super.loadSubDocument(documentPath);
}
/**
* Method returns all embedded OdfPackageDocuments, which match a valid OdfMediaType, of the
* current OdfPackageDocument. Note: The root document is not part of the returned collection.
*
* @return a map with all embedded documents and their paths of the current OdfPackageDocument
*/
public Map<String, OdfDocument> loadSubDocuments() {
return loadSubDocuments(null);
}
/**
* Method returns all embedded OdfPackageDocuments of sthe current OdfPackageDocument matching the
* according MediaType. This is done by matching the subfolder entries of the manifest file with
* the given OdfMediaType.
*
* @param desiredMediaType media type of the documents to be returned (used as a filter).
* @return embedded documents of the current OdfPackageDocument matching the given media type
*/
public Map<String, OdfDocument> loadSubDocuments(OdfMediaType desiredMediaType) {
String wantedMediaString = null;
if (desiredMediaType != null) {
wantedMediaString = desiredMediaType.getMediaTypeString();
}
Map<String, OdfDocument> embeddedObjectsMap = new HashMap<String, OdfDocument>();
// check manifest for current embedded OdfPackageDocuments
Set<String> manifestEntries = mPackage.getFilePaths();
for (String path : manifestEntries) {
// any directory that is not the root document "/"
if (path.length() > 1 && path.endsWith(SLASH)) {
String entryMediaType = mPackage.getFileEntry(path).getMediaTypeString();
// if the entry is a document (directory has mediaType)
if (entryMediaType != null) {
// if a specific ODF mediatype was requested
if (wantedMediaString != null) {
// test if the desired mediatype matches the current
if (entryMediaType.equals(wantedMediaString)) {
path = normalizeDocumentPath(path);
embeddedObjectsMap.put(path, (OdfDocument) mPackage.loadDocument(path));
}
} else {
// test if any ODF mediatype matches the current
for (OdfMediaType mediaType : OdfMediaType.values()) {
if (entryMediaType.equals(mediaType.getMediaTypeString())) {
embeddedObjectsMap.put(path, (OdfDocument) mPackage.loadDocument(path));
}
}
}
}
}
}
return embeddedObjectsMap;
}
/**
* Sets the media type of the OdfDocument
*
* @param odfMediaType media type to be set
*/
protected void setOdfMediaType(OdfMediaType odfMediaType) {
mMediaType = odfMediaType;
super.setMediaTypeString(odfMediaType.getMediaTypeString());
}
/** Gets the media type of the OdfDocument */
protected OdfMediaType getOdfMediaType() {
return mMediaType;
}
/**
* Get the meta data feature instance of the current document
*
* @return the meta data feature instance which represent <code>office:meta</code> in the meta.xml
*/
public OdfOfficeMeta getOfficeMetadata() {
if (mOfficeMeta == null) {
try {
OdfMetaDom metaDom = getMetaDom();
mOfficeMeta = new OdfOfficeMeta(metaDom);
} catch (Exception ex) {
Logger.getLogger(OdfDocument.class.getName()).log(Level.SEVERE, null, ex);
}
}
return mOfficeMeta;
}
/**
* Save the document to an OutputStream. Delegate to the root document and save possible embedded
* OdfDocuments.
*
* <p>If the input file has been cached (this is the case when loading from an InputStream), the
* input file can be overwritten.
*
* <p>If not, the OutputStream may not point to the input file! Otherwise this will result in
* unwanted behaviour and broken files.
*
* <p>When save the embedded document to a stand alone document, all the file entries of the
* embedded document will be copied to a new document package. If the embedded document is outside
* of the current document directory, you have to embed it to the sub directory and refresh the
* link of the embedded document. you should reload it from the stream to get the saved embedded
* document.
*
* @param out - the OutputStream to write the file to
* @throws java.lang.Exception if the document could not be saved
*/
public void save(OutputStream out) throws Exception {
updateMetaData();
if (!isRootDocument()) {
OdfDocument newDoc = loadDocumentFromTemplate(getOdfMediaType());
newDoc.insertDocument(this, ROOT_DOCUMENT_PATH);
newDoc.updateMetaData();
newDoc.mPackage.save(out);
// ToDo: (Issue 219 - PackageRefactoring) - Return the document, when not closing!
// Should we close the sources now? User will never receive the open package!
} else {
mPackage.save(out);
}
}
/**
* Save the document to a given file.
*
* <p>If the input file has been cached (this is the case when loading from an InputStream), the
* input file can be overwritten.
*
* <p>Otherwise it's allowed to overwrite the input file as long as the same path name is used
* that was used for loading (no symbolic link foo2.odt pointing to the loaded file foo1.odt, no
* network path X:\foo.odt pointing to the loaded file D:\foo.odt).
*
* <p>When saving the embedded document to a stand alone document, all files of the embedded
* document will be copied to a new document package. If the embedded document is outside of the
* current document directory, you have to embed it to the sub directory and refresh the link of
* the embedded document. You should reload it from the given file to get the saved embedded
* document.
*
* @param file - the file to save the document
* @throws java.lang.Exception if the document could not be saved
*/
@Override
public void save(File file) throws Exception {
updateMetaData();
if (!isRootDocument()) {
OdfDocument newDoc = loadDocumentFromTemplate(getOdfMediaType());
newDoc.insertDocument(this, ROOT_DOCUMENT_PATH);
newDoc.updateMetaData();
newDoc.mPackage.save(file);
// ToDo: (Issue 219 - PackageRefactoring) - Return the document, when not closing!
// Should we close the sources now? User will never receive the open package!
} else {
this.mPackage.save(file);
}
}
/**
* Close the document and release all temporary created data. After execution of this method, this
* class is no longer usable. Do this as the last action to free resources. Closing an already
* closed document has no effect.
*/
@Override
public void close() {
// set all member variables explicit to null
mMediaType = null;
mOfficeMeta = null;
super.close();
}
/**
* Get the content root of a document.
*
* <p>You may prefer to use the getContentRoot methods of subclasses of OdfDocument. Their return
* parameters are already casted to respective subclasses of OdfElement.
*
* @param the type of the content root, depend on the document type
* @return the child element of office:body, e.g. office:text for text docs
* @throws Exception if the file DOM could not be created.
*/
@SuppressWarnings("unchecked")
<T extends OdfElement> T getContentRoot(Class<T> clazz) throws Exception {
OdfElement contentRoot = getContentDom().getRootElement();
OfficeBodyElement contentBody =
OdfElement.findFirstChildNode(OfficeBodyElement.class, contentRoot);
NodeList childs = contentBody.getChildNodes();
for (int i = 0; i < childs.getLength(); i++) {
Node cur = childs.item(i);
if ((cur != null) && clazz.isInstance(cur)) {
return (T) cur;
}
}
return null;
}
/**
* Get the content root of a document.
*
* <p>You may prefer to use the getContentRoot methods of subclasses of OdfDocument.
*
* @return the child element of office:body, e.g. office:text for text docs
* @throws Exception if the file DOM could not be created.
*/
public OdfElement getContentRoot() throws Exception {
return getContentRoot(OdfElement.class);
}
@Override
public String toString() {
return "\n"
+ getMediaTypeString()
+ " - ID: "
+ this.hashCode()
+ " "
+ getPackage().getBaseURI();
}
/**
* Insert an Image from the specified uri to the end of the OdfDocument.
*
* @param imageUri The URI of the image that will be added to the document, add image stream to
* the package, in the 'Pictures/' graphic directory with the same image file name as in the
* URI. If the imageURI is relative first the user.dir is taken to make it absolute.
* @return Returns the internal package path of the image, which was created based on the given
* URI.
*/
public String newImage(URI imageUri) {
try {
OdfContentDom contentDom = this.getContentDom();
OdfDrawFrame drawFrame = contentDom.newOdfElement(OdfDrawFrame.class);
XPath xpath = contentDom.getXPath();
if (this instanceof OdfSpreadsheetDocument) {
TableTableCellElement lastCell =
(TableTableCellElement)
xpath.evaluate("//table:table-cell[last()]", contentDom, XPathConstants.NODE);
lastCell.appendChild(drawFrame);
drawFrame.removeAttribute("text:anchor-type");
} else if (this instanceof OdfTextDocument) {
TextPElement lastPara =
(TextPElement) xpath.evaluate("//text:p[last()]", contentDom, XPathConstants.NODE);
if (lastPara == null) {
lastPara = ((OdfTextDocument) this).newParagraph();
}
lastPara.appendChild(drawFrame);
drawFrame.setTextAnchorTypeAttribute(TextAnchorTypeAttribute.Value.PARAGRAPH.toString());
} else if (this instanceof OdfPresentationDocument) {
DrawPageElement lastPage =
(DrawPageElement)
xpath.evaluate("//draw:page[last()]", contentDom, XPathConstants.NODE);
lastPage.appendChild(drawFrame);
}
OdfDrawImage image = (OdfDrawImage) drawFrame.newDrawImageElement();
String imagePath = image.newImage(imageUri);
return imagePath;
} catch (Exception ex) {
Logger.getLogger(OdfDocument.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
/**
* Return an instance of table feature with the specific table name.
*
* @param name of the table being searched for.
* @return an instance of table feature with the specific table name.
*/
public OdfTable getTableByName(String name) {
try {
OdfElement root = getContentDom().getRootElement();
OfficeBodyElement officeBody = OdfElement.findFirstChildNode(OfficeBodyElement.class, root);
OdfElement typedContent = OdfElement.findFirstChildNode(OdfElement.class, officeBody);
NodeList childList = typedContent.getChildNodes();
for (int i = 0; i < childList.getLength(); i++) {
if (childList.item(i) instanceof TableTableElement) {
TableTableElement table = (TableTableElement) childList.item(i);
if (table
.getOdfAttributeValue(OdfName.newName(OdfDocumentNamespace.TABLE, "name"))
.equals(name)) {
return OdfTable.getInstance(table);
}
}
}
} catch (Exception e) {
Logger.getLogger(OdfDocument.class.getName()).log(Level.SEVERE, null, e);
}
return null;
}
/**
* Return a list of table features in this document. For general ODF documents it searches for
* them recursively through the document. For ODF documents, there is a getOdsTableList
*
* @see OdfSpreadsheetDocument:getSpreadsheetTables
* @return a list of table features in this document.
*/
@Deprecated(
since =
"It was not clear that this is searching recursively, especialy in OdfSpreadsheetDocuments")
public List<OdfTable> getTableList() {
return getTableList(false);
}
/**
* Return a list of table features in this document. For general ODF documents it searches for
* tables recursively through the document.
*
* @see OdfSpreadsheetDocument:getSpreadsheetTables
* @param doRecursiveSearch In spreadsheet documents you do not need a recursive search.
* @return a list of table features in this document.
*/
public List<OdfTable> getTableList(boolean doRecursiveSearch) {
List<OdfTable> tableList = null;
try {
List<TableTableElement> tableElementList = getTables(doRecursiveSearch);
tableList = new ArrayList<OdfTable>(tableElementList.size());
for (int i = 0; i < tableElementList.size(); i++) {
tableList.add(OdfTable.getInstance(tableElementList.get(i)));
}
} catch (Exception e) {
Logger.getLogger(OdfDocument.class.getName()).log(Level.SEVERE, null, e);
}
return tableList;
}
/**
* Update document meta data in the ODF document. Following metadata data is being updated:
*
* <ul>
* <li>The name of the person who last modified this document will be the Java user.name System
* property
* <li>The date and time when the document was last modified using current data
* <li>The number of times this document has been edited is incremented by 1
* <li>The total time spent editing this document
* </ul>
*
* TODO:This method will be moved to OdfMetadata class. see
* http://odftoolkit.org/bugzilla/show_bug.cgi?id=204
*/
public void updateMetaData() {
if (getOfficeMetadata().hasAutomaticUpdate()) {
OdfOfficeMeta metaData = getOfficeMetadata();
// OpenOffice 3.4.1 needs this metadata to continue list numbering correctly
metaData.setGenerator(FORMER_OPEN_OFFICE_VERSION);
// set creation date
if (mCreationDate != null) {
getOfficeMetadata().setCreationDate(mCreationDate);
}
// update late modfied date
Calendar calendar = Calendar.getInstance();
metaData.setDate(calendar);
// update editing-cycles
Integer cycle = metaData.getEditingCycles();
if (cycle != null) {
metaData.setEditingCycles(++cycle);
} else {
metaData.setEditingCycles(1);
}
// update editing-duration
long editingDuration = calendar.getTimeInMillis() - documentOpeningTime;
editingDuration = (editingDuration < 1) ? 1 : editingDuration;
try {
DatatypeFactory aFactory = DatatypeFactory.newInstance();
metaData.setEditingDuration(new Duration(aFactory.newDurationDayTime(editingDuration)));
} catch (DatatypeConfigurationException e) {
Logger.getLogger(OdfDocument.class.getName())
.log(
Level.SEVERE,
"editing duration update fail as DatatypeFactory can not be instanced",
e);
}
}
}
// /////////////////
// Following is the implementation of locale settings
// ////////////////
/**
* Unicode characters are in general divided by office applications into three different groups.
*
* <p>1) There is CJK: the Chinese, Japanese and Korean script (also old Vietnamese belong to this
* group). See http://en.wikipedia.org/wiki/CJK_characters
*
* <p>2) There is CTL: Complex Text Layout, which uses BIDI algorithms and/or glyph modules for
* instance Arabic, Hebrew, Indic and Thai. See http://en.wikipedia.org/wiki/Complex_Text_Layout
*
* <p>3) And there is all the rest, which was once called by MS Western.
*/
public enum UnicodeGroup {
/** Western language */
WESTERN,
/** Chinese, Japanese and Korean */
CJK,
/** Complex Text Layout language */
CTL;
}
private static final HashSet<String> CJKLanguage = new HashSet<String>();
private static final HashSet<String> CTLLanguage = new HashSet<String>();
{
CJKLanguage.add("zh"); // LANGUAGE_CHINES
CJKLanguage.add("ja"); // LANGUAGE_JAPANESE
CJKLanguage.add("ko"); // LANGUAGE_KOREANE
CTLLanguage.add("am"); // LANGUAGE_AMHARIC_ETHIOPIA
CTLLanguage.add("ar"); // LANGUAGE_ARABIC_SAUDI_ARABIA
CTLLanguage.add("as"); // LANGUAGE_ASSAMESE
CTLLanguage.add("bn"); // LANGUAGE_BENGALI
CTLLanguage.add("bo"); // LANGUAGE_TIBETAN
CTLLanguage.add("brx"); // LANGUAGE_USER_BODO_INDIA
CTLLanguage.add("dgo"); // LANGUAGE_USER_DOGRI_INDIA
CTLLanguage.add("dv"); // LANGUAGE_DHIVEHI
CTLLanguage.add("dz"); // LANGUAGE_DZONGKHA
CTLLanguage.add("fa"); // LANGUAGE_FARSI
CTLLanguage.add("gu"); // LANGUAGE_GUJARATI
CTLLanguage.add("he"); // LANGUAGE_HEBREW
CTLLanguage.add("hi"); // LANGUAGE_HINDI
CTLLanguage.add("km"); // LANGUAGE_KHMER
CTLLanguage.add("kn"); // LANGUAGE_KANNADA
CTLLanguage.add("ks"); // LANGUAGE_KASHMIRI
CTLLanguage.add("ku"); // LANGUAGE_USER_KURDISH_IRAQ
CTLLanguage.add("lo"); // LANGUAGE_LAO
CTLLanguage.add("mai"); // LANGUAGE_USER_MAITHILI_INDIA
CTLLanguage.add("ml"); // LANGUAGE_MALAYALAM
CTLLanguage.add("mn"); // LANGUAGE_MONGOLIAN_MONGOLIAN
CTLLanguage.add("mni"); // LANGUAGE_MANIPURI
CTLLanguage.add("mr"); // LANGUAGE_MARATHI
CTLLanguage.add("my"); // LANGUAGE_BURMESE
CTLLanguage.add("ne"); // LANGUAGE_NEPALI
CTLLanguage.add("or"); // LANGUAGE_ORIYA
CTLLanguage.add("pa"); // LANGUAGE_PUNJABI
CTLLanguage.add("sa"); // LANGUAGE_SANSKRIT
CTLLanguage.add("sd"); // LANGUAGE_SINDHI
CTLLanguage.add("si"); // LANGUAGE_SINHALESE_SRI_LANKA
CTLLanguage.add("syr"); // LANGUAGE_SYRIAC
CTLLanguage.add("ta"); // LANGUAGE_TAMIL
CTLLanguage.add("te"); // LANGUAGE_TELUGU
CTLLanguage.add("th"); // LANGUAGE_THAI
CTLLanguage.add("ug"); // LANGUAGE_UIGHUR_CHINA
CTLLanguage.add("ur"); // LANGUAGE_URDU
CTLLanguage.add("yi"); // LANGUAGE_YIDDISH
}
/**
* Set a locale information.
*
* <p>The locale information will affect the language and country setting of the document. Thus
* the font settings, the spell checkings and etc will be affected.
*
* @param locale - an instance of Locale
*/
public void setLocale(Locale locale) {
setLocale(locale, getUnicodeGroup(locale));
}
/**
* This method will return Locale, which presents the default language and country information
* settings in this document.
*
* @return an instance of Locale that the default language and country is set to.
*/
/**
* Similar to OpenOffice.org, ODFDOM assumes that every Locale is related to one of the three
* Unicodes Groups, either CJK, CTL or Western.
*
* @param locale the UnicodeGroup is requested for
* @return the related UnicodeGroup
*/
public static UnicodeGroup getUnicodeGroup(Locale locale) {
String language = locale.getLanguage();
if (CJKLanguage.contains(language)) {
return UnicodeGroup.CJK;
}
if (CTLLanguage.contains(language)) {
return UnicodeGroup.CTL;
}
return UnicodeGroup.WESTERN;
}
/**
* Set a locale of a specific script type.
*
* <p>If the locale does not belong to the script type, it will not be set.
*
* @param locale - Locale information
* @param unicodeGroup - The script type
*/
private void setLocale(Locale locale, UnicodeGroup unicodeGroup) {
try {
switch (unicodeGroup) {
case WESTERN:
setDefaultWesternLanguage(locale);
break;
case CJK:
setDefaultAsianLanguage(locale);
break;
case CTL:
setDefaultComplexLanguage(locale);
break;
}
} catch (Exception e) {
Logger.getLogger(OdfDocument.class.getName()).log(Level.SEVERE, "Failed to set locale", e);
}
}
/**
* This method will return Locale, which presents the default language and country information
* settings in this document
*
* <p>ODF allows to set a Locale for each of the three UnicodeGroups. Therefore there might be
* three different Locale for the document.
*
* @param unicodeGroup - One of the three (CJK, CTL or Western).
* @return the Locale for the given UnicodeGroup
*/
public Locale getLocale(UnicodeGroup unicodeGroup) {
try {
switch (unicodeGroup) {
case WESTERN:
return getDefaultLanguageByProperty(
OdfTextProperties.Country, OdfTextProperties.Language);
case CJK:
return getDefaultLanguageByProperty(
OdfTextProperties.CountryAsian, OdfTextProperties.LanguageAsian);
case CTL:
return getDefaultLanguageByProperty(
OdfTextProperties.CountryComplex, OdfTextProperties.LanguageComplex);
}
} catch (Exception e) {
Logger.getLogger(OdfDocument.class.getName()).log(Level.SEVERE, "Failed to get locale", e);
}
return null;
}
/** Returns the current Locale for the OdfStyleProperty of the corresponding UnicodeGroup */
private Locale getDefaultLanguageByProperty(
OdfStyleProperty countryProp, OdfStyleProperty languageProp) throws Exception {
String lang = null, ctry = null;
OdfOfficeStyles styles = getStylesDom().getOfficeStyles();
// get language and country setting from default style setting for
// paragraph
OdfDefaultStyle defaultStyle = styles.getDefaultStyle(OdfStyleFamily.Paragraph);
if (defaultStyle != null) {
if (defaultStyle.hasProperty(countryProp) && defaultStyle.hasProperty(languageProp)) {
ctry = defaultStyle.getProperty(countryProp);
lang = defaultStyle.getProperty(languageProp);
return new Locale(lang, ctry);
}
}
// if no default style setting for paragraph
// get language and country setting from other default style settings
Iterable<OdfDefaultStyle> defaultStyles = styles.getDefaultStyles();
Iterator<OdfDefaultStyle> itera = defaultStyles.iterator();
while (itera.hasNext()) {
OdfDefaultStyle style = itera.next();
if (style.hasProperty(countryProp) && style.hasProperty(languageProp)) {
ctry = style.getProperty(countryProp);
lang = style.getProperty(languageProp);
return new Locale(lang, ctry);
}
}
return null;
}
/**
* This method will set the default language and country information of the document, based on the
* parameter of the Locale information.
*
* @param locale - an instance of Locale that the default language and country will be set to.
* @throws Exception
*/
private void setDefaultWesternLanguage(Locale locale) throws Exception {
OdfOfficeStyles styles = getStylesDom().getOfficeStyles();
Iterable<OdfDefaultStyle> defaultStyles = styles.getDefaultStyles();
if (defaultStyles != null) {
Iterator<OdfDefaultStyle> itera = defaultStyles.iterator();
while (itera.hasNext()) {
OdfDefaultStyle style = itera.next();
if (style.getFamily().getProperties().contains(OdfTextProperties.Language)) {
style.setProperty(OdfTextProperties.Language, locale.getLanguage());
style.setProperty(OdfTextProperties.Country, locale.getCountry());
}
}
}
}
/**
* This method will set the default Asian language and country information of the document, based
* on the parameter of the Locale information. If the Locale instance is not set a Asian language
* (Chinese, Traditional Chinese, Japanese and Korean, nothing will take effect.
*
* @param locale - an instance of Locale that the default Asian language and country will be set
* to.
* @throws Exception
*/
private void setDefaultAsianLanguage(Locale locale) throws Exception {
OdfOfficeStyles styles = getStylesDom().getOfficeStyles();
Iterable<OdfDefaultStyle> defaultStyles = styles.getDefaultStyles();
if (defaultStyles != null) {
Iterator<OdfDefaultStyle> itera = defaultStyles.iterator();
while (itera.hasNext()) {
OdfDefaultStyle style = itera.next();
if (style.getFamily().getProperties().contains(OdfTextProperties.LanguageAsian)) {
style.setProperty(OdfTextProperties.LanguageAsian, locale.getLanguage());
style.setProperty(OdfTextProperties.CountryAsian, locale.getCountry());
}
}
}
}
/**
* This method will set the default complex language and country information of the document,
* based on the parameter of the Locale information.
*
* @param locale - an instance of Locale that the default complex language and country will be set
* to.
* @throws Exception
*/
private void setDefaultComplexLanguage(Locale locale) throws Exception {
OdfOfficeStyles styles = getStylesDom().getOfficeStyles();
Iterable<OdfDefaultStyle> defaultStyles = styles.getDefaultStyles();
if (defaultStyles != null) {
Iterator<OdfDefaultStyle> itera = defaultStyles.iterator();
while (itera.hasNext()) {
OdfDefaultStyle style = itera.next();
if (style.getFamily().getProperties().contains(OdfTextProperties.LanguageComplex)) {
style.setProperty(OdfTextProperties.LanguageComplex, locale.getLanguage());
style.setProperty(OdfTextProperties.CountryComplex, locale.getCountry());
}
}
}
}
private Set<String> mFontNames = null;
private Map<String, OfficeAnnotationElement> annotations = null;
/**
* This is a temporary solution to know about the fonts within the documents. Project client does
* not provide font information, therefore the 16 fonts are hard coded
*/
public Set<String> getFontNames() {
if (mFontNames == null) {
mFontNames = new HashSet<String>();
}
return mFontNames;
}
public void addAnnotation(String name, OfficeAnnotationElement element) {
if (annotations == null) {
annotations = new HashMap<String, OfficeAnnotationElement>();
}
annotations.put(name, element);
}
public OfficeAnnotationElement getAnnotation(String name) {
if (annotations == null || !annotations.containsKey(name)) {
return null;
}
return annotations.get(name);
}
public void removeAnnotation(String name) {
if (annotations != null && annotations.containsKey(name)) {
annotations.remove(name);
}
}
public String getUniqueAnnotationName() {
String prefix = "CmtId";
int freeIndex = 0;
if (annotations != null) {
while (annotations.containsKey(prefix + freeIndex)) {
++freeIndex;
}
}
return prefix + freeIndex;
}
protected void removeCachedView() {
mPackage = getPackage();
// removes the LO/AO view caching
mPackage.remove("Thumbnails/thumbnail.png");
}
/**
* @return TRUE if the document was created by CollabTextDocument and thereby supports user
* changes, otherwise FALSE
*/
public Boolean hasCollaboration() {
return mHasCollaboration != null && mHasCollaboration;
}
}