blob: 49e3534e4246688fe907b57b62e739d63b88610e [file] [log] [blame]
/************************************************************************
*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
*
* Copyright 2008, 2010 Oracle and/or its affiliates. All rights reserved.
*
* Use is subject to license terms.
*
* 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
*
* 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.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*
************************************************************************/
package org.odftoolkit.simple;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
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.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import org.odftoolkit.odfdom.dom.OdfContentDom;
import org.odftoolkit.odfdom.dom.OdfDocumentNamespace;
import org.odftoolkit.odfdom.dom.OdfSchemaConstraint;
import org.odftoolkit.odfdom.dom.OdfSchemaDocument;
import org.odftoolkit.odfdom.dom.OdfStylesDom;
import org.odftoolkit.odfdom.dom.attribute.text.TextAnchorTypeAttribute;
import org.odftoolkit.odfdom.dom.element.OdfStyleBase;
import org.odftoolkit.odfdom.dom.element.draw.DrawFrameElement;
import org.odftoolkit.odfdom.dom.element.draw.DrawPageElement;
import org.odftoolkit.odfdom.dom.element.office.OfficeBodyElement;
import org.odftoolkit.odfdom.dom.element.office.OfficeMasterStylesElement;
import org.odftoolkit.odfdom.dom.element.style.StyleFontFaceElement;
import org.odftoolkit.odfdom.dom.element.table.TableTableCellElement;
import org.odftoolkit.odfdom.dom.element.table.TableTableCellElementBase;
import org.odftoolkit.odfdom.dom.element.table.TableTableTemplateElement;
import org.odftoolkit.odfdom.dom.element.text.TextPElement;
import org.odftoolkit.odfdom.dom.element.text.TextSectionElement;
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.OdfOfficeAutomaticStyles;
import org.odftoolkit.odfdom.incubator.doc.office.OdfOfficeStyles;
import org.odftoolkit.odfdom.incubator.doc.style.OdfDefaultStyle;
import org.odftoolkit.odfdom.pkg.MediaType;
import org.odftoolkit.odfdom.pkg.OdfElement;
import org.odftoolkit.odfdom.pkg.OdfFileDom;
import org.odftoolkit.odfdom.pkg.OdfName;
import org.odftoolkit.odfdom.pkg.OdfNamespace;
import org.odftoolkit.odfdom.pkg.OdfPackage;
import org.odftoolkit.odfdom.pkg.OdfPackageDocument;
import org.odftoolkit.odfdom.pkg.OdfValidationException;
import org.odftoolkit.odfdom.pkg.manifest.OdfFileEntry;
import org.odftoolkit.odfdom.type.Duration;
import org.odftoolkit.simple.meta.Meta;
import org.odftoolkit.simple.table.AbstractTableContainer;
import org.odftoolkit.simple.table.Cell;
import org.odftoolkit.simple.table.Table;
import org.odftoolkit.simple.table.Table.TableBuilder;
import org.odftoolkit.simple.table.TableContainer;
import org.odftoolkit.simple.table.TableTemplate;
import org.odftoolkit.simple.text.Paragraph;
import org.odftoolkit.simple.text.Section;
import org.w3c.dom.Attr;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
/**
* This abstract class is representing one of the possible ODF documents
*/
public abstract class Document extends OdfSchemaDocument implements TableContainer {
// Static parts of file references
private static final String SLASH = "/";
private OdfMediaType mMediaType;
private Meta mOfficeMeta;
private long documentOpeningTime;
private TableContainerImpl tableContainerImpl;
private static final Pattern CONTROL_CHAR_PATTERN = Pattern.compile("\\p{Cntrl}");
private static final String EMPTY_STRING = "";
private IdentityHashMap<OdfElement, Component> mComponentRepository = new IdentityHashMap<OdfElement, Component>();
// FIXME: This field is only used in method copyResourcesFrom to improve
// copy performance, should not be used in any other way.
// methods loadDocument(String documentPath) and loadDocument(File file)
// will initialize it.
// This field and its methods should be removed after ODFDOM supplies batch
// copy.
private File mFile = null;
// if the copy foreign slide for several times,
// the same style might be copied for several times with the different name
// so use styleRenameMap to keep track the renamed style so we can reuse the
// style,
// rather than new several styles which only have the different style names.
// while if the style elements really have the same style name but with
// different content
// such as that these style elements are from different document
// so the value for each key should be a list
private HashMap<String, List<String>> styleRenameMap = new HashMap<String, List<String>>();
// the map is used to record if the renamed style name is appended to the
// current dom
private HashMap<String, Boolean> styleAppendMap = new HashMap<String, Boolean>();
// the object rename map for image.
// can not easily recognize if the embedded document are the same.
// private HashMap<String, String> objectRenameMap = new HashMap<String,
// String>();
// Using static factory instead of constructor
protected Document(OdfPackage pkg, String internalPath, OdfMediaType mediaType) {
super(pkg, internalPath, mediaType.getMediaTypeString());
mMediaType = mediaType;
// set document opening time.
documentOpeningTime = System.currentTimeMillis();
}
/**
* This enum contains all possible media types of Document 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
*/
public String getMediaTypeString() {
return mMediaType;
}
/**
* @return the ODF filesuffix of this document
*/
public String getSuffix() {
return mSuffix;
}
/**
*
* @param mediaType
* string defining an ODF document
* @return the according OdfMediatype encapuslating 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 either not yet supported or not an ODF mediatype!");
}
}
return odfMediaType;
}
}
/**
* Loads an Document from the given resource. NOTE: Initial meta data 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 Document document or NULL if the media type is not supported
* by SIMPLE.
* @throws java.lang.Exception
* - if the document could not be created.
*/
protected static Document loadTemplate(Resource res, OdfMediaType odfMediaType) throws Exception {
InputStream in = res.createInputStream();
OdfPackage pkg = null;
try {
pkg = OdfPackage.loadPackage(in);
} finally {
in.close();
}
Document newDocument = newDocument(pkg, ROOT_DOCUMENT_PATH, odfMediaType);
// add initial meta data to new document.
initializeMetaData(newDocument);
return newDocument;
}
/**
* Loads a Document from the provided path.
*
* <p>
* Document relies on the file being available for read access over the
* whole life cycle of Document.
* </p>
*
* @param documentPath
* - the path from where the document can be loaded
* @param password
* - file password.
* @return the Document from the given path or NULL if the media type is not
* supported by SIMPLE.
* @throws java.lang.Exception
* - if the document could not be created.
* @since 0.8
*/
public static Document loadDocument(String documentPath, String password) throws Exception {
File file = new File(documentPath);
return loadDocument(file, password);
}
/**
* Loads a Document from the provided path.
*
* <p>
* Document relies on the file being available for read access over the
* whole life cycle of Document.
* </p>
*
* @param documentPath
* - the path from where the document can be loaded
* @return the Document from the given path or NULL if the media type is not
* supported by SIMPLE.
* @throws java.lang.Exception
* - if the document could not be created.
*/
public static Document loadDocument(String documentPath) throws Exception {
File file = new File(documentPath);
return loadDocument(file);
}
/**
* Creates a Document from the Document provided by a resource Stream.
*
* <p>
* Since an InputStream does not provide the arbitrary (non sequentiell)
* read access needed by Document, 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.
* </p>
*
* @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 Document loadDocument(InputStream inStream) throws Exception {
return loadDocument(OdfPackage.loadPackage(inStream));
}
/**
* Creates a Document from the Document provided by a File.
*
* <p>
* Document relies on the file being available for read access over the
* whole lifecycle of Document.
* </p>
*
* @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 Document loadDocument(File file) throws Exception {
Document doc = loadDocument(OdfPackage.loadPackage(file));
doc.setFile(file);
return doc;
}
/**
* Creates a Document from the Document provided by a File.
*
* <p>
* Document relies on the file being available for read access over the
* whole lifecycle of Document.
* </p>
*
* @param file
* - a file representing the ODF document.
* @param password
* - file password.
* @return the document created from the given File
* @throws java.lang.Exception
* - if the document could not be created.
* @since 0.7
*/
public static Document loadDocument(File file, String password) throws Exception {
Document doc = loadDocument(OdfPackage.loadPackage(file, password, null));
doc.setFile(file);
return doc;
}
/**
* Creates a Document from the Document provided by an 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 Document loadDocument(OdfPackage odfPackage) throws Exception {
return loadDocument(odfPackage, ROOT_DOCUMENT_PATH);
}
/**
* Creates a Document from the Document 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.
* @return the root document of the given OdfPackage
* @throws java.lang.Exception
* - if the ODF document could not be created.
*/
public static Document loadDocument(OdfPackage odfPackage, String internalPath) throws Exception {
String documentMediaType = odfPackage.getMediaTypeString(internalPath);
if (documentMediaType == null) {
throw new IllegalArgumentException("Given internalPath '" + internalPath + "' is an illegal or inappropriate argument.");
}
OdfMediaType odfMediaType = OdfMediaType.getOdfMediaType(documentMediaType);
if (odfMediaType == null) {
ErrorHandler errorHandler = odfPackage.getErrorHandler();
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);
}
/**
* Sets password of this document.
*
* @param password
* the password of this document.
* @since 0.8
*/
public void setPassword(String password) {
getPackage().setPassword(password);
}
// return null if the media type can not be recognized.
private static Document loadDocumentFromTemplate(OdfMediaType odfMediaType) throws Exception {
switch (odfMediaType) {
case TEXT:
case TEXT_TEMPLATE:
case TEXT_MASTER:
case TEXT_WEB:
// documentTemplate = TextDocument.EMPTY_TEXT_DOCUMENT_RESOURCE;
TextDocument document = TextDocument.newTextDocument();
document.changeMode(TextDocument.OdfMediaType.TEXT_WEB);
return document;
case SPREADSHEET:
SpreadsheetDocument spreadsheet = SpreadsheetDocument.newSpreadsheetDocument();
spreadsheet.changeMode(SpreadsheetDocument.OdfMediaType.SPREADSHEET);
return spreadsheet;
case SPREADSHEET_TEMPLATE:
SpreadsheetDocument spreadsheettemplate = SpreadsheetDocument.newSpreadsheetDocument();
spreadsheettemplate.changeMode(SpreadsheetDocument.OdfMediaType.SPREADSHEET_TEMPLATE);
return spreadsheettemplate;
case PRESENTATION:
PresentationDocument presentation = PresentationDocument.newPresentationDocument();
presentation.changeMode(PresentationDocument.OdfMediaType.PRESENTATION);
return presentation;
case PRESENTATION_TEMPLATE:
PresentationDocument presentationtemplate = PresentationDocument.newPresentationDocument();
presentationtemplate.changeMode(PresentationDocument.OdfMediaType.PRESENTATION_TEMPLATE);
return presentationtemplate;
case GRAPHICS:
GraphicsDocument graphics = GraphicsDocument.newGraphicsDocument();
graphics.changeMode(GraphicsDocument.OdfMediaType.GRAPHICS);
return graphics;
case GRAPHICS_TEMPLATE:
GraphicsDocument graphicstemplate = GraphicsDocument.newGraphicsDocument();
graphicstemplate.changeMode(GraphicsDocument.OdfMediaType.GRAPHICS_TEMPLATE);
return graphicstemplate;
case CHART:
ChartDocument chart = ChartDocument.newChartDocument();
chart.changeMode(ChartDocument.OdfMediaType.CHART);
return chart;
case CHART_TEMPLATE:
ChartDocument charttemplate = ChartDocument.newChartDocument();
charttemplate.changeMode(ChartDocument.OdfMediaType.CHART_TEMPLATE);
return charttemplate;
// case IMAGE:
// case IMAGE_TEMPLATE:
default:
throw new IllegalArgumentException("Given mediaType '" + odfMediaType.toString() + "' is either not yet supported or not an ODF mediatype!");
}
}
/**
* Creates one of the ODF documents based a given mediatype.
*
* @param odfMediaType
* The ODF Mediatype of the ODF document to be created.
* @return The ODF document, which mediatype dependends on the parameter or
* NULL if media type were not supported.
*/
private static Document newDocument(OdfPackage pkg, String internalPath, OdfMediaType odfMediaType) {
Document newDoc = null;
switch (odfMediaType) {
case TEXT:
newDoc = new TextDocument(pkg, internalPath, TextDocument.OdfMediaType.TEXT);
break;
case TEXT_TEMPLATE:
newDoc = new TextDocument(pkg, internalPath, TextDocument.OdfMediaType.TEXT_TEMPLATE);
break;
case TEXT_MASTER:
newDoc = new TextDocument(pkg, internalPath, TextDocument.OdfMediaType.TEXT_MASTER);
break;
case TEXT_WEB:
newDoc = new TextDocument(pkg, internalPath, TextDocument.OdfMediaType.TEXT_WEB);
break;
case SPREADSHEET:
newDoc = new SpreadsheetDocument(pkg, internalPath, SpreadsheetDocument.OdfMediaType.SPREADSHEET);
break;
case SPREADSHEET_TEMPLATE:
newDoc = new SpreadsheetDocument(pkg, internalPath, SpreadsheetDocument.OdfMediaType.SPREADSHEET_TEMPLATE);
break;
case PRESENTATION:
newDoc = new PresentationDocument(pkg, internalPath, PresentationDocument.OdfMediaType.PRESENTATION);
break;
case PRESENTATION_TEMPLATE:
newDoc = new PresentationDocument(pkg, internalPath, PresentationDocument.OdfMediaType.PRESENTATION_TEMPLATE);
break;
case GRAPHICS:
newDoc = new GraphicsDocument(pkg, internalPath, GraphicsDocument.OdfMediaType.GRAPHICS);
break;
case GRAPHICS_TEMPLATE:
newDoc = new GraphicsDocument(pkg, internalPath, GraphicsDocument.OdfMediaType.GRAPHICS_TEMPLATE);
break;
case CHART:
newDoc = new ChartDocument(pkg, internalPath, ChartDocument.OdfMediaType.CHART);
break;
case CHART_TEMPLATE:
newDoc = new ChartDocument(pkg, internalPath, ChartDocument.OdfMediaType.CHART_TEMPLATE);
break;
// case IMAGE:
// case IMAGE_TEMPLATE:
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
* path to the ODF document within the package. The path is
* relative to the current document.
* @return an embedded Document
*/
public Document getEmbeddedDocument(String documentPath) {
String internalPath = getDocumentPath() + documentPath;
internalPath = normalizeDocumentPath(internalPath);
Document embeddedDocument = (Document) mPackage.getCachedDocument(internalPath);
// if the document was not already loaded, fine mimetype and create a
// new instance
if (embeddedDocument == null) {
String mediaTypeString = getMediaTypeString();
OdfMediaType odfMediaType = OdfMediaType.getOdfMediaType(mediaTypeString);
if (odfMediaType == null) {
embeddedDocument = newDocument(mPackage, internalPath, odfMediaType);
} else {
try {
String documentMediaType = mPackage.getMediaTypeString(internalPath);
odfMediaType = OdfMediaType.getOdfMediaType(documentMediaType);
if (odfMediaType == null) {
return null;
}
embeddedDocument = Document.loadDocument(mPackage, internalPath);
} catch (Exception ex) {
Logger.getLogger(OdfPackageDocument.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
return embeddedDocument;
}
/**
* Method returns all embedded OdfPackageDocuments, which match a valid
* OdfMediaType, of the root OdfPackageDocument.
*
* @return a list with all embedded documents of the root OdfPackageDocument
*/
// ToDo: (Issue 219 - PackageRefactoring) - Better return Path of
// Documents??
public List<Document> getEmbeddedDocuments() {
List<Document> embeddedObjects = new ArrayList<Document>();
// ToDo: (Issue 219 - PackageRefactoring) - Algorithm enhancement:
// Instead going through all the files for each mimetype, better
// Check all files, which have a mimetype if it is one of the desired,
// perhaps start with ODF prefix
for (OdfMediaType mediaType : OdfMediaType.values()) {
embeddedObjects.addAll(getEmbeddedDocuments(mediaType));
}
return embeddedObjects;
}
/**
* Method returns all embedded OdfPackageDocuments of the root
* OdfPackageDocument matching the according MediaType. This is done by
* matching the subfolder entries of the manifest file with the given
* OdfMediaType.
*
* @param mediaType
* media type which is used as a filter
* @return embedded documents of the root OdfPackageDocument matching the
* given media type
*/
public List<Document> getEmbeddedDocuments(OdfMediaType mediaType) {
String wantedMediaString = null;
if (mediaType != null) {
wantedMediaString = mediaType.getMediaTypeString();
}
List<Document> embeddedObjects = new ArrayList<Document>();
// 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)) {
normalizeDocumentPath(path);
embeddedObjects.add(getEmbeddedDocument(path));
}
} else {
// test if any ODF mediatype matches the current
for (OdfMediaType type : OdfMediaType.values()) {
if (entryMediaType.equals(type.getMediaTypeString())) {
embeddedObjects.add(getEmbeddedDocument(path));
}
}
}
}
}
}
return embeddedObjects;
}
/**
* Embed an OdfPackageDocument to the current OdfPackageDocument. All the
* file entries of child document will be embedded as well to the current
* document package.
*
* @param documentPath
* to the directory the ODF document should be inserted (relative
* to the current document).
* @param sourceDocument
* the OdfPackageDocument to be embedded.
*/
public void insertDocument(OdfPackageDocument sourceDocument, String documentPath) {
super.insertDocument(sourceDocument, documentPath);
}
/**
* Sets the media type of the Document
*
* @param odfMediaType
* media type to be set
*/
protected void setOdfMediaType(OdfMediaType odfMediaType) {
mMediaType = odfMediaType;
super.setMediaTypeString(odfMediaType.getMediaTypeString());
}
/**
* Gets the media type of the Document
*/
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 Meta getOfficeMetadata() {
if (mOfficeMeta == null) {
try {
mOfficeMeta = new Meta(getMetaDom());
} catch (Exception ex) {
Logger.getLogger(Document.class.getName()).log(Level.SEVERE, null, ex);
}
}
return mOfficeMeta;
}
/**
* Save the document to an OutputStream. Delegate to the root document and
* save possible embedded Documents.
*
* <p>
* If the input file has been cached (this is the case when loading from an
* InputStream), the input file can be overwritten.
* </p>
*
* <p>
* If not, the OutputStream may not point to the input file! Otherwise this
* will result in unwanted behaviour and broken files.
* </p>
*
* <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 {
// 2DO FLUSH AND SAVE IN PACKAGE
flushDoms();
updateMetaData();
if (!isRootDocument()) {
Document 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 {
// 2DO MOVE CACHE TO PACKAGE
// // the root document only have to flush the DOM of all open child
// documents
// flushAllDOMs();
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>
*
* <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>
*
* <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
*/
public void save(File file) throws Exception {
// 2DO FLUSH AND SAVE IN PACKAGE
flushDoms();
updateMetaData();
if (!isRootDocument()) {
Document 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);
}
}
/**
* Save the document to a given file with given password.
*
* <p>
* If the input file has been cached (this is the case when loading from an
* InputStream), the input file can be overwritten.
* </p>
*
* <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>
*
* <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.
* @param file
* the password of this document.
*
* @throws java.lang.Exception
* if the document could not be saved
* @since 0.8
*/
public void save(File file, String password) throws Exception {
// 2DO FLUSH AND SAVE IN PACKAGE
flushDoms();
updateMetaData();
if (!isRootDocument()) {
Document newDoc = loadDocumentFromTemplate(getOdfMediaType());
newDoc.insertDocument(this, ROOT_DOCUMENT_PATH);
newDoc.updateMetaData();
newDoc.mPackage.setPassword(password);
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 {
mPackage.setPassword(password);
mPackage.save(file);
}
}
/**
* Close the OdfPackage and release all temporary created data. Acter
* 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. Note that this will not close any cached documents.
*/
@Override
public void close() {
// set all member variables explicit to null
mMediaType = null;
mOfficeMeta = null;
mComponentRepository.clear();
super.close();
}
/**
* Get the content root of a document.
*
* You may prefer to use the getContentRoot methods of subclasses of
* Document. Their return parameters are already casted to respective
* subclasses of OdfElement.
*
* @param clazz
* 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")
protected <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.
*
* You may prefer to use the getContentRoot methods of subclasses of
* Document.
*
* @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 Document.
*
* @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 SpreadsheetDocument) {
TableTableCellElement lastCell = (TableTableCellElement) xpath.evaluate("//table:table-cell[last()]", contentDom, XPathConstants.NODE);
lastCell.appendChild(drawFrame);
drawFrame.removeAttribute("text:anchor-type");
} else if (this instanceof TextDocument) {
TextPElement lastPara = (TextPElement) xpath.evaluate("//text:p[last()]", contentDom, XPathConstants.NODE);
if (lastPara == null) {
lastPara = ((TextDocument) this).newParagraph();
}
lastPara.appendChild(drawFrame);
drawFrame.setTextAnchorTypeAttribute(TextAnchorTypeAttribute.Value.PARAGRAPH.toString());
} else if (this instanceof PresentationDocument) {
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(Document.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
/**
* Meta data about the document will be initialized. Following metadata data
* is being added:
* <ul>
* <li>The initial creator name will be the Java user.name System property.</li>
* <li>The date and time when this document was created using the current
* data.</li>
* <li>The number of times this document has been edited.</li>
* <li>The default language will be the Java user.language System property.</li>
* </ul>
*
* @param newDoc
* the Document object which need to initialize meta data.
*
* TODO:This method will be moved to OdfMetadata class. see
* http://odftoolkit.org/bugzilla/show_bug.cgi?id=204
*/
private static void initializeMetaData(Document newDoc) {
Meta metaData = newDoc.getOfficeMetadata();
// add initial-creator info.
String creator = System.getProperty("user.name");
metaData.setInitialCreator(creator);
// add creation-date info.
Calendar calendar = Calendar.getInstance();
metaData.setCreationDate(calendar);
// add editing-cycles info.
metaData.setEditingCycles(0);
// add language info.
String language = System.getProperty("user.language");
if (language != null) {
metaData.setLanguage(language);
}
}
/**
* 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>
* <li>The date and time when the document was last modified using current
* data</li>
* <li>The number of times this document has been edited is incremented by 1
* </li>
* <li>The total time spent editing this document</li>
* </ul>
*
* TODO:This method will be moved to OdfMetadata class. see
* http://odftoolkit.org/bugzilla/show_bug.cgi?id=204
*
* @throws Exception
*/
private void updateMetaData() throws Exception {
if (mMetaDom != null) {
Meta metaData = getOfficeMetadata();
String creator = System.getProperty("user.name");
// update creator info.
metaData.setCreator(creator);
// update date info.
Calendar calendar = Calendar.getInstance();
metaData.setDcdate(calendar);
// update editing-cycles info.
Integer cycle = metaData.getEditingCycles();
if (cycle != null) {
metaData.setEditingCycles(++cycle);
} else {
metaData.setEditingCycles(1);
}
// update editing-duration info.
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(Document.class.getName()).log(Level.SEVERE, "editing duration update fail as DatatypeFactory can not be instanced", e);
}
}
}
// /////////////////
// Following is the implementation of locale settings
// ////////////////
/**
* <p>
* Unicode characters are in general divided by office applications into
* three different types:
*
* <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. 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 ScriptType {
/**
* Western language
*/
WESTERN,
/**
* Chinese, Japanese and Korean
*/
CJK,
/**
* Complex Text Layout language
*/
CTL;
}
private final static HashSet<String> CJKLanguage = new HashSet<String>();
private final static 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
}
/**
* <p>
* 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, getScriptType(locale));
}
public static ScriptType getScriptType(Locale locale) {
String language = locale.getLanguage();
if (CJKLanguage.contains(language))
return ScriptType.CJK;
if (CTLLanguage.contains(language))
return ScriptType.CTL;
return ScriptType.WESTERN;
}
/**
* <p>
* Set a locale of a specific script type.
* <p>
* If the locale is not belone to the script type, nothing will happen.
*
* @param locale
* - Locale information
* @param scriptType
* - The script type
*/
public void setLocale(Locale locale, ScriptType scriptType) {
try {
switch (scriptType) {
case WESTERN:
setWesternLanguage(locale);
break;
case CJK:
setDefaultAsianLanguage(locale);
break;
case CTL:
setDefaultComplexLanguage(locale);
break;
}
} catch (Exception e) {
Logger.getLogger(Document.class.getName()).log(Level.SEVERE, "Failed to set locale", e);
}
}
/**
* <p>
* Get a locale information of a specific script type.
*
* @param scriptType
* - The script type
* @return the Locale information
*/
public Locale getLocale(ScriptType scriptType) {
try {
switch (scriptType) {
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(Document.class.getName()).log(Level.SEVERE, "Failed to get locale", e);
}
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 setWesternLanguage(Locale locale) throws Exception {
if (getScriptType(locale) != ScriptType.WESTERN)
return;
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());
}
}
}
}
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 return an instance of 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.
*/
/**
* 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 {
if (getScriptType(locale) != ScriptType.CJK)
return;
String user_language = locale.getLanguage();
if (!user_language.equals(Locale.CHINESE.getLanguage()) && !user_language.equals(Locale.TRADITIONAL_CHINESE.getLanguage())
&& !user_language.equals(Locale.JAPANESE.getLanguage()) && !user_language.equals(Locale.KOREAN.getLanguage()))
return;
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 {
if (getScriptType(locale) != ScriptType.CTL)
return;
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());
}
}
}
}
/**
* This method will search both the document content and header/footer,
* return an iterator of section objects.
* <p>
* The sections defined in embed document won't be covered.
*
* @return an iterator of Section objects
*/
public Iterator<Section> getSectionIterator() {
TextSectionElement element;
ArrayList<Section> list = new ArrayList<Section>();
try {
// search in content.xml
OdfElement root = getContentDom().getRootElement();
OfficeBodyElement officeBody = OdfElement.findFirstChildNode(OfficeBodyElement.class, root);
NodeList sectionList = officeBody.getElementsByTagNameNS(OdfDocumentNamespace.TEXT.getUri(), "section");
for (int i = 0; i < sectionList.getLength(); i++) {
element = (TextSectionElement) sectionList.item(i);
list.add(Section.getInstance(element));
}
// Search in style.xml
root = getStylesDom().getRootElement();
OfficeMasterStylesElement masterStyle = OdfElement.findFirstChildNode(OfficeMasterStylesElement.class, root);
sectionList = masterStyle.getElementsByTagNameNS(OdfDocumentNamespace.TEXT.getUri(), "section");
for (int i = 0; i < sectionList.getLength(); i++) {
element = (TextSectionElement) sectionList.item(i);
list.add(Section.getInstance(element));
}
} catch (Exception e) {
Logger.getLogger(Document.class.getName()).log(Level.SEVERE, "Failed in sectionIterator", e);
}
return list.iterator();
}
/**
* This method will search both the document content and header/footer,
* return a section with a specific name.
* <p>
* This method won't search in the embed document.
* <p>
* Null will be returned if there is no section found.
*
* @param name
* - the name of a section
* @return a section object with a specific name
*/
public Section getSectionByName(String name) {
TextSectionElement element;
try {
OdfElement root = getContentDom().getRootElement();
OfficeBodyElement officeBody = OdfElement.findFirstChildNode(OfficeBodyElement.class, root);
XPath xpath = getContentDom().getXPath();
String xpathValue = ".//text:section[@text:name=\"" + name + "\"]";
element = (TextSectionElement) xpath.evaluate(xpathValue, officeBody, XPathConstants.NODE);
if (element != null) {
return Section.getInstance(element);
}
root = getStylesDom().getRootElement();
OfficeMasterStylesElement masterStyle = OdfElement.findFirstChildNode(OfficeMasterStylesElement.class, root);
xpath = getStylesDom().getXPath();
element = (TextSectionElement) xpath.evaluate(".//text:section[@text:name=\"" + name + "\"]", masterStyle, XPathConstants.NODE);
if (element != null) {
return Section.getInstance(element);
}
} catch (Exception e) {
Logger.getLogger(Document.class.getName()).log(Level.SEVERE, "Failed in getSectionByName", e);
}
return null;
}
/**
* Remove an ODF element from the document. All the resources that are only
* related with this element will be removed at the same time.
*
* @param odfElement
* - the odf element that would be moved.
*/
public boolean removeElementLinkedResource(OdfElement odfElement) {
boolean success = deleteLinkedRef(odfElement);
success &= deleteStyleRef(odfElement);
return success;
}
/**
* Return a unique string with a character "a" followed by randomly
* generating 6 hex numbers
*
* @return a unique string
*/
String makeUniqueName() {
return String.format("a%06x", (int) (Math.random() * 0xffffff));
}
private String getNewUniqueString(String oldStr) {
int lastIndex = oldStr.lastIndexOf("-");
if (lastIndex == -1) {
return oldStr + "-" + makeUniqueName();
}
String suffix = oldStr.substring(lastIndex + 1);
if (suffix.matches("a[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]")) {
return oldStr.substring(0, lastIndex + 1) + makeUniqueName();
} else
return oldStr + "-" + makeUniqueName();
}
private void updateAttribute(Attr attr) {
String oldID = attr.getValue();
String newID = getNewUniqueString(oldID);
attr.setValue(newID);
}
/**
* Make a content copy of the specified element, and the returned element
* should have the specified ownerDocument.
*
* @param element
* The element that need to be copied
* @param dom
* The specified DOM tree that the returned element belong to
* @param deep
* If true, recursively clone the subtree under the element,
* false, only clone the element itself
* @return Returns a duplicated element which is not in the DOM tree with
* the specified element
*/
Node cloneForeignElement(Node element, OdfFileDom dom, boolean deep) {
if (element instanceof OdfElement) {
OdfElement cloneElement = dom.createElementNS(((OdfElement) element).getOdfName());
NamedNodeMap attributes = element.getAttributes();
if (attributes != null) {
for (int i = 0; i < attributes.getLength(); i++) {
Node item = attributes.item(i);
String qname = null;
String prefix = item.getPrefix();
if (prefix == null) {
qname = item.getLocalName();
cloneElement.setAttribute(qname, item.getNodeValue());
} else {
qname = prefix + ":" + item.getLocalName();
cloneElement.setAttributeNS(item.getNamespaceURI(), qname, item.getNodeValue());
}
}
}
if (deep) {
Node childNode = element.getFirstChild();
while (childNode != null) {
cloneElement.appendChild(cloneForeignElement(childNode, dom, true));
childNode = childNode.getNextSibling();
}
}
return cloneElement;
} else {
return dom.createTextNode(element.getNodeValue());
}
}
/**
* This method will update all the attribute "xml:id" to make it unique
* within the whole document content
* <p>
* This method is usually be invoked before inserting a copied ODF element
* to document content.
*
* @param element
* - the element that need to be inserted.
*/
void updateXMLIds(OdfElement element) {
try {
XPath xpath = getContentDom().getXPath();
String xpathValue = "//*[@xml:id]";
NodeList childList = (NodeList) xpath.evaluate(xpathValue, element, XPathConstants.NODESET);
if (childList == null)
return;
for (int i = 0; i < childList.getLength(); i++) {
OdfElement ele = (OdfElement) childList.item(i);
Attr attri = ele.getAttributeNodeNS(OdfDocumentNamespace.XML.getUri(), "id");
updateAttribute(attri);
}
} catch (Exception e) {
Logger.getLogger(Document.class.getName()).log(Level.SEVERE, "Failed in updateXMLIds", e);
}
}
/**
* This method will update all the attribute
* "text:name","table:name","draw:name","chart:name", to make it unique
* within the whole document content.
* <p>
* This method is usually be invoked before inserting a copied ODF element
* to document content.
*
* @param element
* - the element that need to be inserted.
*/
// anim:name, chart:name, config:name, office:name, presentation:name,
// svg:name,
void updateNames(OdfElement element) {
try {
XPath xpath = getContentDom().getXPath();
String xpathValue = "descendant-or-self::node()[@text:name|@table:name|@draw:name|@chart:name]";
NodeList childList = (NodeList) xpath.evaluate(xpathValue, element, XPathConstants.NODESET);
if (childList == null)
return;
for (int i = 0; i < childList.getLength(); i++) {
OdfElement ele = (OdfElement) childList.item(i);
Attr attri = ele.getAttributeNodeNS(OdfDocumentNamespace.TEXT.getUri(), "name");
if (attri != null)
updateAttribute(attri);
attri = ele.getAttributeNodeNS(OdfDocumentNamespace.TABLE.getUri(), "name");
if (attri != null)
updateAttribute(attri);
if (ele instanceof DrawFrameElement)// only update draw:frame
{
attri = ele.getAttributeNodeNS(OdfDocumentNamespace.DRAW.getUri(), "name");
if (attri != null)
updateAttribute(attri);
}
}
} catch (Exception e) {
Logger.getLogger(Document.class.getName()).log(Level.SEVERE, "Failed in updateXMLIds", e);
}
}
/**
* This method will copy the linked resource of the element which need to be
* copied, from the source package to the target package.
* <p>
* If the target package contains a resource with the same path and name,
* the name of the resource will be renamed.
* <p>
* This method will copy resources all in one batch.
*
* @param sourceCloneEle
* - the element that need to be copied
* @param srcDocument
* - the source document
*/
void copyLinkedRefInBatch(OdfElement sourceCloneEle, Document srcDocument) {
try {
OdfFileDom fileDom = (OdfFileDom) sourceCloneEle.getOwnerDocument();
XPath xpath;
if (fileDom instanceof OdfContentDom) {
xpath = ((OdfContentDom) fileDom).getXPath();
} else {
xpath = ((OdfStylesDom) fileDom).getXPath();
}
// OdfPackageDocument srcDoc = fileDom.getDocument();
// new a map to put the original name and the rename string, in case
// that the same name might be referred by the slide several times.
HashMap<String, String> objectRenameMap = new HashMap<String, String>();
NodeList linkNodes = (NodeList) xpath.evaluate(".//*[@xlink:href]", sourceCloneEle, XPathConstants.NODESET);
for (int i = 0; i <= linkNodes.getLength(); i++) {
OdfElement object = null;
if (linkNodes.getLength() == i) {
if (sourceCloneEle.hasAttributeNS(OdfDocumentNamespace.XLINK.getUri(), "href")) {
object = sourceCloneEle;
} else {
break;
}
} else {
object = (OdfElement) linkNodes.item(i);
}
String refObjPath = object.getAttributeNS(OdfDocumentNamespace.XLINK.getUri(), "href");
if (refObjPath != null && refObjPath.length() > 0) {
// the path of the object is start with "./"
boolean hasPrefix = false;
String prefix = "./";
String newObjPath;
if (refObjPath.startsWith(prefix)) {
refObjPath = refObjPath.substring(2);
hasPrefix = true;
}
// check if this linked resource has been copied
if (objectRenameMap.containsKey(refObjPath)) {
// this object has been copied already
newObjPath = objectRenameMap.get(refObjPath);
object.setAttributeNS(OdfDocumentNamespace.XLINK.getUri(), "xlink:href", hasPrefix ? (prefix + newObjPath) : newObjPath);
continue;
}
// check if the current document contains the same path
OdfFileEntry fileEntry = getPackage().getFileEntry(refObjPath);
// note: if refObjPath is a directory, it must end with '/'
if (fileEntry == null) {
fileEntry = getPackage().getFileEntry(refObjPath + "/");
}
if (fileEntry != null) {
// rename the object path
newObjPath = objectRenameMap.get(refObjPath);
if (newObjPath == null) {
// if refObjPath still contains ".", it means that
// it has the suffix
// then change the name before the suffix string
int dotIndex = refObjPath.indexOf(".");
if (dotIndex != -1) {
newObjPath = refObjPath.substring(0, dotIndex) + "-" + makeUniqueName() + refObjPath.substring(dotIndex);
} else {
newObjPath = refObjPath + "-" + makeUniqueName();
}
objectRenameMap.put(refObjPath, newObjPath);
}
object.setAttributeNS(OdfDocumentNamespace.XLINK.getUri(), "xlink:href", hasPrefix ? (prefix + newObjPath) : newObjPath);
} else
objectRenameMap.put(refObjPath, refObjPath);
}
}
// copy resource in batch
copyResourcesFrom(srcDocument, objectRenameMap);
} catch (Exception e) {
Logger.getLogger(Document.class.getName()).log(Level.SEVERE, null, e);
}
}
/*****************************/
/*
* These codes are moved from OdfPackage, and should be removed till
* OdfPackage can provide a mechanism to copy resources in batch.
*/
/*****************************/
private InputStream readAsInputStream(ZipInputStream inputStream) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
if (outputStream != null) {
byte[] buf = new byte[4096];
int r = 0;
while ((r = inputStream.read(buf, 0, 4096)) > -1) {
outputStream.write(buf, 0, r);
}
}
return new ByteArrayInputStream(outputStream.toByteArray());
}
private String normalizeFilePath(String internalPath) {
if (internalPath.equals(EMPTY_STRING)) {
String errMsg = "The internalPath given by parameter is an empty string!";
Logger.getLogger(OdfPackage.class.getName()).severe(errMsg);
throw new IllegalArgumentException(errMsg);
} else {
return normalizePath(internalPath);
}
}
private static final String DOUBLE_DOT = "..";
private static final String DOT = ".";
private static final String COLON = ":";
private static final Pattern BACK_SLASH_PATTERN = Pattern.compile("\\\\");
private static final Pattern DOUBLE_SLASH_PATTERN = Pattern.compile("//");
private String normalizePath(String path) {
if (path == null) {
String errMsg = "The internalPath given by parameter is NULL!";
Logger.getLogger(OdfPackage.class.getName()).severe(errMsg);
throw new IllegalArgumentException(errMsg);
} else if (!mightBeExternalReference(path)) {
if (path.equals(EMPTY_STRING)) {
path = SLASH;
} else {
// exchange all backslash "\" with a slash "/"
if (path.indexOf('\\') != -1) {
path = BACK_SLASH_PATTERN.matcher(path).replaceAll(SLASH);
}
// exchange all double slash "//" with a slash "/"
while (path.indexOf("//") != -1) {
path = DOUBLE_SLASH_PATTERN.matcher(path).replaceAll(SLASH);
}
// if directory replacements (e.g. ..) exist, resolve and remove
// them
if (path.indexOf("/.") != -1 || path.indexOf("./") != -1) {
path = removeChangeDirectories(path);
}
}
}
return path;
}
private boolean mightBeExternalReference(String internalPath) {
boolean isExternalReference = false;
// if the fileReference is a external relative documentURL..
if (internalPath.startsWith(DOUBLE_DOT) || // or absolute documentURL
// AND not root document
internalPath.startsWith(SLASH) && !internalPath.equals(SLASH) || // or
// absolute
// IRI
internalPath.contains(COLON)) {
isExternalReference = true;
}
return isExternalReference;
}
private String removeChangeDirectories(String path) {
boolean isDirectory = path.endsWith(SLASH);
StringTokenizer tokenizer = new StringTokenizer(path, SLASH);
int tokenCount = tokenizer.countTokens();
List<String> tokenList = new ArrayList<String>(tokenCount);
// add all paths to a list
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
tokenList.add(token);
}
if (!isDirectory) {
String lastPath = tokenList.get(tokenCount - 1);
if (lastPath.equals(DOT) || lastPath.equals(DOUBLE_DOT)) {
isDirectory = true;
}
}
String currentToken;
int removeDirLevel = 0;
StringBuilder out = new StringBuilder();
// work on the list from back to front
for (int i = tokenCount - 1; i >= 0; i--) {
currentToken = tokenList.get(i);
// every ".." will remove an upcoming path
if (currentToken.equals(DOUBLE_DOT)) {
removeDirLevel++;
} else if (currentToken.equals(DOT)) {
} else {
// if a path have to be remove, neglect current path
if (removeDirLevel > 0) {
removeDirLevel--;
} else {
// add the path segment
out.insert(0, SLASH);
out.insert(0, currentToken);
}
}
}
if (removeDirLevel > 0) {
return EMPTY_STRING;
} else {
if (!isDirectory) {
// remove trailing slash /
out.deleteCharAt(out.length() - 1);
}
return out.toString();
}
}
/*****************************/
// FIXME: These two methods are only used in method copyResourcesFrom to
// improve copy performance, should not be used in any other way.
// methods loadDocument(String documentPath) and loadDocument(File file)
// will initialize mFile.
// This field and these two methods should be removed after ODFDOM supplies
// batch copy.
private void setFile(File thisFile) {
mFile = thisFile;
}
private File getFile() {
return mFile;
}
/**
* This method will copy resources from source document to this document.
* The second parameter contains a map between all the name of resources in
* the source document and the rename string. If the source document is
* loaded from a file, a good performance method will be used. If the source
* document is loaded from a input stream, package layer methods will be
* invoked to copy these resources, with bad performance.
*
* In future, the code of this method will move to ODFDOM package layer.
* Till then, good performance will be gotten whether the source document is
* loaded from file or from input stream.
*
*/
void copyResourcesFrom(Document srcDoc, HashMap<String, String> objectRenameMap) throws Exception {
if (srcDoc.getFile() != null) {
ArrayList<String> copiedFolder = new ArrayList<String>();
Set<String> refObjPathSet = objectRenameMap.keySet();
FileInputStream tempFileStream = new FileInputStream(srcDoc.getFile());
ZipInputStream zipStream = new ZipInputStream(tempFileStream);
ZipEntry zipEntry = zipStream.getNextEntry();
while (zipEntry != null) {
String refObjPath = zipEntry.getName();
for (String path : refObjPathSet) {
if (refObjPath.equals(path)) {
String newObjPath = objectRenameMap.get(refObjPath);
refObjPath = normalizeFilePath(refObjPath);
String mediaType = srcDoc.getPackage().getFileEntry(refObjPath).getMediaTypeString();
InputStream is = readAsInputStream(zipStream);
getPackage().insert(is, newObjPath, mediaType);
break;
} else if (refObjPath.startsWith(path + "/")) {
String suffix = refObjPath.substring(path.length());
String newObjPath = objectRenameMap.get(path) + suffix;
refObjPath = normalizeFilePath(refObjPath);
String mediaType = srcDoc.getPackage().getFileEntry(refObjPath).getMediaTypeString();
InputStream is = readAsInputStream(zipStream);
getPackage().insert(is, newObjPath, mediaType);
if (!copiedFolder.contains(path)) {
mediaType = srcDoc.getPackage().getFileEntry(path + "/").getMediaTypeString();
getPackage().insert((InputStream) null, objectRenameMap.get(path) + "/", mediaType);
copiedFolder.add(path);
}
break;
}
}
zipEntry = zipStream.getNextEntry();
}
zipStream.close();
tempFileStream.close();
} else {
Set<String> refObjPathSet = objectRenameMap.keySet();
for (String refObjPath : refObjPathSet) {
String newObjPath = objectRenameMap.get(refObjPath);
InputStream is = srcDoc.getPackage().getInputStream(refObjPath);
if (is != null) {
String mediaType = srcDoc.getPackage().getFileEntry(refObjPath).getMediaTypeString();
getPackage().insert(is, newObjPath, mediaType);
} else {
Document embedDoc = ((Document) srcDoc).getEmbeddedDocument(refObjPath);
if (embedDoc != null) {
insertDocument(embedDoc, newObjPath);
}
}
}
}
}
/**
* This method will copy the linked resource of the element which need to be
* copied, from the source package to the target package.
* <p>
* If the target package contains a resource with the same path and name,
* the name of the resource will be renamed.
*
* @param sourceCloneEle
* - the element that need to be copied
*/
// clone the source clone element's referred object path to the current
// package
// if the current package contains the same name with the referred object
// path,
// rename the object path and path reference of this slide element
// notes: the source clone element is the copied one to avoid changing the
// content of the source document.
void copyLinkedRef(OdfElement sourceCloneEle) {
try {
OdfFileDom fileDom = (OdfFileDom) sourceCloneEle.getOwnerDocument();
XPath xpath;
if (fileDom instanceof OdfContentDom) {
xpath = ((OdfContentDom) fileDom).getXPath();
} else {
xpath = ((OdfStylesDom) fileDom).getXPath();
}
OdfPackageDocument srcDoc = fileDom.getDocument();
// new a map to put the original name and the rename string, in case
// that the same name might be referred by the slide several times.
HashMap<String, String> objectRenameMap = new HashMap<String, String>();
NodeList linkNodes = (NodeList) xpath.evaluate(".//*[@xlink:href]", sourceCloneEle, XPathConstants.NODESET);
for (int i = 0; i <= linkNodes.getLength(); i++) {
OdfElement object = null;
if (linkNodes.getLength() == i) {
if (sourceCloneEle.hasAttributeNS(OdfDocumentNamespace.XLINK.getUri(), "href")) {
object = sourceCloneEle;
} else {
break;
}
} else {
object = (OdfElement) linkNodes.item(i);
}
String refObjPath = object.getAttributeNS(OdfDocumentNamespace.XLINK.getUri(), "href");
if (refObjPath != null && refObjPath.length() > 0) {
// the path of the object is start with "./"
boolean hasPrefix = false;
String prefix = "./";
if (refObjPath.startsWith(prefix)) {
refObjPath = refObjPath.substring(2);
hasPrefix = true;
}
// check if the current document contains the same path
OdfFileEntry fileEntry = getPackage().getFileEntry(refObjPath);
// note: if refObjPath is a directory, it must end with '/'
if (fileEntry == null) {
fileEntry = getPackage().getFileEntry(refObjPath + "/");
}
String newObjPath = refObjPath;
if (fileEntry != null) {
// rename the object path
newObjPath = objectRenameMap.get(refObjPath);
if (newObjPath == null) {
// if refObjPath still contains ".", it means that
// it has the suffix
// then change the name before the suffix string
int dotIndex = refObjPath.indexOf(".");
if (dotIndex != -1) {
newObjPath = refObjPath.substring(0, dotIndex) + "-" + makeUniqueName() + refObjPath.substring(dotIndex);
} else {
newObjPath = refObjPath + "-" + makeUniqueName();
}
objectRenameMap.put(refObjPath, newObjPath);
}
object.setAttributeNS(OdfDocumentNamespace.XLINK.getUri(), "xlink:href", hasPrefix ? (prefix + newObjPath) : newObjPath);
}
InputStream is = srcDoc.getPackage().getInputStream(refObjPath);
if (is != null) {
String mediaType = srcDoc.getPackage().getFileEntry(refObjPath).getMediaTypeString();
getPackage().insert(is, newObjPath, mediaType);
} else {
Document embedDoc = ((Document) srcDoc).getEmbeddedDocument(refObjPath);
if (embedDoc != null) {
insertDocument(embedDoc, newObjPath);
}
}
}
}
} catch (Exception e) {
Logger.getLogger(Document.class.getName()).log(Level.SEVERE, null, e);
}
}
/**
* When a element needs to be copied to a different document, all the style
* definitions that are related with this element need to be copied.
*
* @param sourceCloneEle
* - the element that need to be copied
* @param srcDoc
* - the source document
*/
void copyForeignStyleRef(OdfElement sourceCloneEle, Document srcDoc) {
try {
ArrayList<String> tempList = new ArrayList<String>();
OdfFileDom srcContentDom = srcDoc.getContentDom();
XPath xpath = srcContentDom.getXPath();
// 1. collect all the referred style element which has "style:name"
// attribute
// 1.1. style:name of content.xml
String styleQName = "style:name";
NodeList srcStyleDefNodeList = (NodeList) xpath.evaluate("*/office:automatic-styles/*[@" + styleQName + "]", srcContentDom, XPathConstants.NODESET);
IdentityHashMap<OdfElement, List<OdfElement>> srcContentStyleCloneEleList = new IdentityHashMap<OdfElement, List<OdfElement>>();
IdentityHashMap<OdfElement, OdfElement> appendContentStyleList = new IdentityHashMap<OdfElement, OdfElement>();
getCopyStyleList(null, sourceCloneEle, styleQName, srcStyleDefNodeList, srcContentStyleCloneEleList, appendContentStyleList, tempList, true);
// 1.2. style:name of styles.xml
srcStyleDefNodeList = (NodeList) xpath.evaluate(".//*[@" + styleQName + "]", srcDoc.getStylesDom(), XPathConstants.NODESET);
IdentityHashMap<OdfElement, List<OdfElement>> srcStylesStyleCloneEleList = new IdentityHashMap<OdfElement, List<OdfElement>>();
IdentityHashMap<OdfElement, OdfElement> appendStylesStyleList = new IdentityHashMap<OdfElement, OdfElement>();
tempList.clear();
getCopyStyleList(null, sourceCloneEle, styleQName, srcStyleDefNodeList, srcStylesStyleCloneEleList, appendStylesStyleList, tempList, true);
// 1.3 rename, copy the referred style element to the corresponding
// position in the dom tree
insertCollectedStyle(styleQName, srcContentStyleCloneEleList, getContentDom(), appendContentStyleList);
insertCollectedStyle(styleQName, srcStylesStyleCloneEleList, getStylesDom(), appendStylesStyleList);
// 2. collect all the referred style element which has "draw:name"
// attribute
// 2.1 draw:name of styles.xml
// the value of draw:name is string or StyleName,
// only when the value is StyleName type, the style definition
// should be cloned to the destination document
// in ODF spec, such attribute type is only exist in <office:styles>
// element, so only search it in styles.xml dom
tempList.clear();
styleQName = "draw:name";
srcStyleDefNodeList = (NodeList) xpath.evaluate(".//*[@" + styleQName + "]", srcDoc.getStylesDom(), XPathConstants.NODESET);
IdentityHashMap<OdfElement, List<OdfElement>> srcDrawStyleCloneEleList = new IdentityHashMap<OdfElement, List<OdfElement>>();
IdentityHashMap<OdfElement, OdfElement> appendDrawStyleList = new IdentityHashMap<OdfElement, OdfElement>();
Iterator<OdfElement> iter = appendContentStyleList.keySet().iterator();
while (iter.hasNext()) {
OdfElement styleElement = iter.next();
OdfElement cloneStyleElement = appendContentStyleList.get(styleElement);
getCopyStyleList(styleElement, cloneStyleElement, styleQName, srcStyleDefNodeList, srcDrawStyleCloneEleList, appendDrawStyleList, tempList,
false);
}
iter = appendStylesStyleList.keySet().iterator();
while (iter.hasNext()) {
OdfElement styleElement = iter.next();
OdfElement cloneStyleElement = appendStylesStyleList.get(styleElement);
getCopyStyleList(styleElement, cloneStyleElement, styleQName, srcStyleDefNodeList, srcDrawStyleCloneEleList, appendDrawStyleList, tempList,
false);
}
// 2.2 rename, copy the referred style element to the corresponding
// position in the dom tree
// note: "draw:name" style element only exist in styles.dom
insertCollectedStyle(styleQName, srcDrawStyleCloneEleList, getStylesDom(), appendDrawStyleList);
} catch (Exception e) {
Logger.getLogger(Document.class.getName()).log(Level.SEVERE, null, e);
}
}
// 1. modified the style name of the style definition element which has the
// same name with the source document
// 2. As to the style definition which match 1) condition, modified the
// referred style name of the element which reference this style
// 3. All the style which also contains other style reference, should be
// copied to the source document.
private void insertCollectedStyle(String styleQName, IdentityHashMap<OdfElement, List<OdfElement>> srcStyleCloneEleList, OdfFileDom dom,
IdentityHashMap<OdfElement, OdfElement> appendStyleList) {
try {
String stylePrefix = OdfNamespace.getPrefixPart(styleQName);
String styleLocalName = OdfNamespace.getLocalPart(styleQName);
String styleURI = OdfDocumentNamespace.STYLE.getUri();
if (stylePrefix.equals("draw"))
styleURI = OdfDocumentNamespace.DRAW.getUri();
// is the DOM always the styles.xml
XPath xpath = dom.getXPath();
NodeList destStyleNodeList;
if (dom instanceof OdfContentDom)
destStyleNodeList = (NodeList) xpath.evaluate("*/office:automatic-styles/*[@" + styleQName + "]", dom, XPathConstants.NODESET);
else
destStyleNodeList = (NodeList) xpath.evaluate(".//*[@" + styleQName + "]", dom, XPathConstants.NODESET);
Iterator<OdfElement> iter = srcStyleCloneEleList.keySet().iterator();
while (iter.hasNext()) {
OdfElement styleElement = iter.next();
OdfElement cloneStyleElement = appendStyleList.get(styleElement);
if (cloneStyleElement == null) {
cloneStyleElement = (OdfElement) styleElement.cloneNode(true);
appendStyleList.put(styleElement, cloneStyleElement);
}
String styleName = styleElement.getAttributeNS(styleURI, styleLocalName);
List<String> newStyleNameList = styleRenameMap.get(styleName);
// if the newStyleNameList != null, means that styleName exists
// in dest document
// and it has already been renamed
if ((newStyleNameList != null) || (isStyleNameExist(destStyleNodeList, styleName) != null)) {
String newStyleName = null;
if (newStyleNameList == null) {
newStyleNameList = new ArrayList<String>();
newStyleName = styleName + "-" + makeUniqueName();
newStyleNameList.add(newStyleName);
styleRenameMap.put(styleName, newStyleNameList);
} else {
for (int i = 0; i < newStyleNameList.size(); i++) {
String styleNameIter = newStyleNameList.get(i);
OdfElement destStyleElementWithNewName = isStyleNameExist(destStyleNodeList, styleNameIter);
// check if the two style elements have the same
// content
// if not, the cloneStyleElement should rename,
// rather than reuse the new style name
cloneStyleElement.setAttributeNS(styleURI, styleQName, styleNameIter);
if ((destStyleElementWithNewName != null) && destStyleElementWithNewName.equals(cloneStyleElement)) {
newStyleName = styleNameIter;
break;
}
}
if (newStyleName == null) {
newStyleName = styleName + "-" + makeUniqueName();
newStyleNameList.add(newStyleName);
}
}
// System.out.println("renaming:"+styleName+"-"+newStyleName);
// if newStyleName has been set in the element as the new
// name
// which means that the newStyleName is conform to the odf
// spec
// then change element style reference name
if (changeStyleRefName(srcStyleCloneEleList.get(styleElement), styleName, newStyleName)) {
cloneStyleElement.setAttributeNS(styleURI, styleQName, newStyleName);
// if display name should also be renamed
String displayName = cloneStyleElement.getAttributeNS(styleURI, "display-name");
if ((displayName != null) && (displayName.length() > 0)) {
cloneStyleElement.setAttributeNS(styleURI, stylePrefix + ":display-name",
displayName + newStyleName.substring(newStyleName.length() - 8));
}
}
}
}
iter = appendStyleList.keySet().iterator();
while (iter.hasNext()) {
OdfElement styleElement = iter.next();
OdfElement cloneStyleElement = appendStyleList.get(styleElement);
String newStyleName = cloneStyleElement.getAttributeNS(styleURI, styleLocalName);
Boolean isAppended = styleAppendMap.get(newStyleName);
// if styleAppendMap contain the newStyleName,
// means that cloneStyleElement has already been appended
if ((isAppended != null) && isAppended.booleanValue() == true) {
continue;
} else {
styleAppendMap.put(newStyleName, true);
}
OdfElement cloneForeignStyleElement = (OdfElement) cloneForeignElement(cloneStyleElement, dom, true);
String styleElePath = getElementPath(styleElement);
appendForeignStyleElement(cloneForeignStyleElement, dom, styleElePath);
copyLinkedRef(cloneStyleElement);
}
} catch (Exception e) {
Logger.getLogger(Document.class.getName()).log(Level.SEVERE, null, e);
}
}
// get all the copy of referred style element which is directly referred or
// indirectly referred by cloneEle
// styleQName is style:name
// all the style are defined in srcStyleNodeList
// and these style are all have the styleName defined in styleQName
// attribute
// the key of copyStyleEleList is the style definition element
// the value of the corresponding key is the clone of the element which
// refer to the key,
// the cloned element can be the content of slide or the style element.
// the key of appendStyleList is the style definition element which has the
// other style reference
// the value of the corresponding key is the the style definition clone
// element
// loop means if recursive call this function
// if loop == true, get the style definition element reference other style
// definition element
private void getCopyStyleList(OdfElement ele, OdfElement cloneEle, String styleQName, NodeList srcStyleNodeList,
IdentityHashMap<OdfElement, List<OdfElement>> copyStyleEleList, IdentityHashMap<OdfElement, OdfElement> appendStyleList, List<String> attrStrList,
boolean loop) {
try {
String styleLocalName = OdfNamespace.getLocalPart(styleQName);
String stylePrefix = OdfNamespace.getPrefixPart(styleQName);
String styleURI = OdfDocumentNamespace.STYLE.getUri();
if (stylePrefix.equals("draw"))
styleURI = OdfDocumentNamespace.DRAW.getUri();
// OdfElement override the "toString" method
String cloneEleStr = cloneEle.toString();
for (int i = 0; i < srcStyleNodeList.getLength(); i++) {
OdfElement styleElement = (OdfElement) srcStyleNodeList.item(i);
String styleName = styleElement.getAttributeNS(styleURI, styleLocalName);
if (styleName != null) {
int index = 0;
index = cloneEleStr.indexOf("=\"" + styleName + "\"", index);
while (index >= 0) {
String subStr = cloneEleStr.substring(0, index);
int lastSpaceIndex = subStr.lastIndexOf(' ');
String attrStr = subStr.substring(lastSpaceIndex + 1, index);
if (attrStr.equals(styleQName) || attrStrList.contains(attrStr + "=" + "\"" + styleName + "\"")) {
index = cloneEleStr.indexOf("=\"" + styleName + "\"", index + styleName.length());
continue;
}
attrStrList.add(attrStr + "=" + "\"" + styleName + "\"");
XPath xpath = ((OdfFileDom) cloneEle.getOwnerDocument()).getXPath();
NodeList styleRefNodes = (NodeList) xpath.evaluate(".//*[@" + attrStr + "='" + styleName + "']", cloneEle, XPathConstants.NODESET);
boolean isExist = false;
for (int j = 0; j <= styleRefNodes.getLength(); j++) {
OdfElement styleRefElement = null;
if (j == styleRefNodes.getLength()) {
isExist = isStyleNameRefExist(cloneEle, styleName, false);
if (isExist) {
styleRefElement = cloneEle;
} else {
continue;
}
} else {
OdfElement tmpElement = (OdfElement) styleRefNodes.item(j);
if (isStyleNameRefExist(tmpElement, styleName, false)) {
styleRefElement = tmpElement;
} else {
continue;
}
}
boolean hasLoopStyleDef = true;
if (!(styleElement instanceof StyleFontFaceElement)) {
if (copyStyleEleList.get(styleElement) == null) {
List<OdfElement> styleRefEleList = new ArrayList<OdfElement>();
copyStyleEleList.put(styleElement, styleRefEleList);
hasLoopStyleDef = false;
}
copyStyleEleList.get(styleElement).add(styleRefElement);
}
OdfElement cloneStyleElement = appendStyleList.get(styleElement);
if (cloneStyleElement == null) {
cloneStyleElement = (OdfElement) styleElement.cloneNode(true);
appendStyleList.put(styleElement, cloneStyleElement);
}
if (loop && !hasLoopStyleDef) {
getCopyStyleList(styleElement, cloneStyleElement, styleQName, srcStyleNodeList, copyStyleEleList, appendStyleList, attrStrList,
loop);
}
}
index = cloneEleStr.indexOf("=\"" + styleName + "\"", index + styleName.length());
}
}
}
} catch (Exception e) {
Logger.getLogger(Document.class.getName()).log(Level.SEVERE, null, e);
}
}
// append the cloneStyleElement to the contentDom which position is defined
// by styleElePath
private void appendForeignStyleElement(OdfElement cloneStyleEle, OdfFileDom dom, String styleElePath) {
StringTokenizer token = new StringTokenizer(styleElePath, "/");
boolean isExist = true;
boolean found = false;
Node iterNode = dom.getFirstChild();
Node parentNode = dom;
while (token.hasMoreTokens()) {
String onePath = token.nextToken();
found = false;
while ((iterNode != null) && isExist) {
String path = iterNode.getNamespaceURI();
String prefix = iterNode.getPrefix();
if (prefix == null) {
path += "@" + iterNode.getLocalName();
} else {
path += "@" + prefix + ":" + iterNode.getLocalName();
}
if (!path.equals(onePath)) {
// not found, then get the next sibling to find such path
// node
iterNode = iterNode.getNextSibling();
} else {
// found, then get the child nodes to find the next path
// node
parentNode = iterNode;
found = true;
iterNode = iterNode.getFirstChild();
break;
}
}
if (!found) {
// should new the element since the current path node
if (isExist) {
isExist = false;
}
StringTokenizer token2 = new StringTokenizer(onePath, "@");
OdfElement newElement = dom.createElementNS(OdfName.newName(token2.nextToken(), token2.nextToken()));
parentNode.appendChild(newElement);
parentNode = newElement;
}
}
parentNode.appendChild(cloneStyleEle);
}
// The returned string is a path from the top of the dom tree to the
// specified element
// and the path is split by "/" between each node
private String getElementPath(OdfElement styleEle) {
String path = "";
Node parentNode = styleEle.getParentNode();
while (!(parentNode instanceof OdfFileDom)) {
String qname = null;
String prefix = parentNode.getPrefix();
if (prefix == null) {
qname = parentNode.getLocalName();
} else {
qname = prefix + ":" + parentNode.getLocalName();
}
path = parentNode.getNamespaceURI() + "@" + qname + "/" + path;
parentNode = parentNode.getParentNode();
}
return path;
}
// change the element referred oldStyleName to the new name
// if true then set newStyleName attribute value successfully
// if false means that the newStyleName value is not conform to the ODF
// spec, so do not modify the oldStyleName
private boolean changeStyleRefName(List<OdfElement> list, String oldStyleName, String newStyleName) {
boolean rtn = false;
for (int index = 0; index < list.size(); index++) {
OdfElement element = list.get(index);
NamedNodeMap attributes = element.getAttributes();
if (attributes != null) {
for (int i = 0; i < attributes.getLength(); i++) {
Node item = attributes.item(i);
String value = item.getNodeValue();
if (oldStyleName.equals(value)) {
try {
item.setNodeValue(newStyleName);
rtn = true;
break;
} catch (IllegalArgumentException e) {
return false;
}
}
}
}
}
return rtn;
}
// check if the element contains the referred styleName
private boolean isStyleNameRefExist(Node element, String styleName, boolean deep) {
NamedNodeMap attributes = element.getAttributes();
if (attributes != null) {
for (int i = 0; i < attributes.getLength(); i++) {
Node item = attributes.item(i);
if (item.getNodeValue().equals(styleName) && !item.getNodeName().equals("style:name")) {
// this is style definition, not reference.
return true;
}
}
}
if (deep) {
Node childNode = element.getFirstChild();
while (childNode != null) {
if (!isStyleNameRefExist(childNode, styleName, true)) {
childNode = childNode.getNextSibling();
} else {
return true;
}
}
}
return false;
}
// check if nodeList contains the node that "style:name" attribute has the
// same value with styleName
// Note: nodeList here is all the style definition list
private OdfElement isStyleNameExist(NodeList nodeList, String styleName) {
for (int i = 0; i < nodeList.getLength(); i++) {
OdfElement element = (OdfElement) nodeList.item(i);
String name = element.getAttributeNS(OdfDocumentNamespace.STYLE.getUri(), "name");
if (name.equals(styleName)) {
// return true;
return element;
}
}
// return false;
return null;
}
/**
* This method will delete all the linked resources that are only related
* with this element.
*
* @param odfEle
* - the element to be deleted.
* @return true if successfully delete, or else, false will be returned
*/
// delete all the xlink:href object which is contained in slideElement and
// does not referred by other slides
boolean deleteLinkedRef(OdfElement odfEle) {
boolean success = true;
try {
OdfFileDom contentDom = getContentDom();
XPath xpath = contentDom.getXPath();
NodeList linkNodes = (NodeList) xpath.evaluate("//*[@xlink:href]", contentDom, XPathConstants.NODESET);
for (int i = 0; i < linkNodes.getLength(); i++) {
OdfElement object = (OdfElement) linkNodes.item(i);
String refObjPath = object.getAttributeNS(OdfDocumentNamespace.XLINK.getUri(), "href");
int relation = odfEle.compareDocumentPosition(object);
// if slide element contains the returned element which has the
// xlink:href reference
if ((relation & Node.DOCUMENT_POSITION_CONTAINED_BY) > 0 && refObjPath != null && refObjPath.length() > 0) {
// the path of the object is start with "./"
NodeList pathNodes = (NodeList) xpath.evaluate("//*[@xlink:href='" + refObjPath + "']", getContentDom(), XPathConstants.NODESET);
int refCount = pathNodes.getLength();
if (refCount == 1) {
// delete "./"
if (refObjPath.startsWith("./")) {
refObjPath = refObjPath.substring(2);
}
// check if the current document contains the same path
OdfFileEntry fileEntry = getPackage().getFileEntry(refObjPath);
if (fileEntry != null) {
// it is a stream, such as image, binary file
getPackage().remove(refObjPath);
} else {
// note: if refObjPath is a directory, it must end
// with '/'
fileEntry = getPackage().getFileEntry(refObjPath + "/");
removeDocument(refObjPath);
}
}
}
}
} catch (XPathExpressionException e) {
Logger.getLogger(Document.class.getName()).log(Level.SEVERE, null, e);
success = false;
} catch (Exception e) {
Logger.getLogger(Document.class.getName()).log(Level.SEVERE, null, e);
success = false;
}
return success;
}
/**
* This method will delete all the style definitions that are only related
* with this element.
*
* @param odfEle
* - the element to be deleted.
* @return true if successfully delete, or else, false will be returned
*/
boolean deleteStyleRef(OdfElement odfEle) {
boolean success = true;
try {
// method 1:
// 1.1. iterate child element of the content element
// 1.2. if the child element is an OdfStylableElement, get the
// style-name ref count
// //////////////
// method 2:
// 2.1. get the list of the style definition
ArrayList<OdfElement> removeStyles = new ArrayList<OdfElement>();
OdfOfficeAutomaticStyles autoStyles = getContentDom().getAutomaticStyles();
NodeList stylesList = autoStyles.getChildNodes();
OdfFileDom contentDom = getContentDom();
XPath xpath = contentDom.getXPath();
// 2.2. get the reference of each style which occurred in the
// current page
for (int i = 0; i < stylesList.getLength(); i++) {
Node item = stylesList.item(i);
if (item instanceof OdfElement) {
OdfElement node = (OdfElement) item;
String styleName = node.getAttributeNS(OdfDocumentNamespace.STYLE.getUri(), "name");
if (styleName != null) {
// search the styleName contained at the current page
// element
NodeList styleNodes = (NodeList) xpath.evaluate("//*[@*='" + styleName + "']", contentDom, XPathConstants.NODESET);
int styleCnt = styleNodes.getLength();
if (styleCnt > 1) {
// the first styleName is occurred in the style
// definition
// so check if the second styleName and last
// styleName is occurred in the current page element
// if yes, then remove it
OdfElement elementFirst = (OdfElement) styleNodes.item(1);
OdfElement elementLast = (OdfElement) styleNodes.item(styleCnt - 1);
boolean isSamePage = false;
if (elementFirst instanceof DrawPageElement) {
DrawPageElement tempPage = (DrawPageElement) elementFirst;
if (tempPage.equals(odfEle)) {
isSamePage = true;
}
}
int relationFirst = odfEle.compareDocumentPosition(elementFirst);
int relationLast = odfEle.compareDocumentPosition(elementLast);
// if slide element contains the child element which
// has the styleName reference
if (((relationFirst & Node.DOCUMENT_POSITION_CONTAINED_BY) > 0 && (relationLast & Node.DOCUMENT_POSITION_CONTAINED_BY) > 0)
|| (isSamePage && (styleCnt == 1))) {
if (node instanceof OdfStyleBase) {
removeStyles.add(node);
}
}
} else {
continue;
}
}
}
}
for (int i = 0; i < removeStyles.size(); i++) {
autoStyles.removeChild(removeStyles.get(i));
}
} catch (Exception e) {
Logger.getLogger(Document.class.getName()).log(Level.SEVERE, null, e);
success = false;
}
return success;
}
public Table addTable() {
return getTableContainerImpl().addTable();
}
public Table addTable(int numRows, int numCols) {
return getTableContainerImpl().addTable(numRows, numCols);
}
public Table getTableByName(String name) {
return getTableContainerImpl().getTableByName(name);
}
public java.util.List<Table> getTableList() {
return getTableContainerImpl().getTableList();
}
public TableBuilder getTableBuilder() {
return getTableContainerImpl().getTableBuilder();
}
protected TableContainer getTableContainerImpl() {
if (tableContainerImpl == null) {
tableContainerImpl = new TableContainerImpl();
}
return tableContainerImpl;
}
private class TableContainerImpl extends AbstractTableContainer {
public OdfElement getTableContainerElement() {
OdfElement containerElement = null;
try {
containerElement = getContentRoot();
} catch (Exception e) {
Logger.getLogger(Document.class.getName()).log(Level.SEVERE, null, e);
}
return containerElement;
}
}
/**
* Return the component repository of this document.
*
* @return the component repository of this document.
*/
protected IdentityHashMap<OdfElement, Component> getComponentMap() {
return mComponentRepository;
}
/**
* Construct a
*
*
* <code>TableTemplate<code> feature by extracting style template from an pre-defined table in a foreign document. The styles loaded by the template will be copied into the document as well and can be referenced by table directly.
* <p>
* The imported table need to be at least a 5*5 table (e.g. A1E5). Each type of style in the template will be set according to the style reference in a specific table cell, as following:
* <br>first column - A2
* <br>last column - E2
* <br>first row - A2
* <br>last row - E2
* <br>even rows - B3
* <br>odd rows - B2
* <br>even columns - C2
* <br>odd columns - B2
* <br>body - B2
* <br>first-row-start-column -A1
* <br>first-row-end-column -E1
* <br>last-row-start-column -A5
* <br>last-row-end-column -E5
*
* @param templateFileInputStream
* - the InputStream of the ODF document.
* @param tableName
* - the table name which will be used to load styles as template
* @throws Exception
* - if content DOM could not be initialized
*/
public TableTemplate LoadTableTemplateFromForeignTable(
InputStream templateFileInputStream, String tableName) throws Exception {
Document doc = Document.loadDocument(templateFileInputStream);
if (doc == null)
throw new IllegalStateException(
"Cannot load specified template file.");
Table table = doc.getTableByName(tableName);
if (table == null)
throw new IllegalStateException(
"Cannot load table template from specified file.");
if (table.getRowCount() < 5 || table.getColumnCount() < 5)
throw new IllegalStateException(
"The template cannot be loaded. It should be at least a 5*5 table.");
TableTemplate template = new TableTemplate(getStylesDom()
.newOdfElement(TableTableTemplateElement.class));
// first-row-start-column
Cell cell = table.getCellByPosition(0, 0);
cell.getParagraphIterator().hasNext();
cell.getParagraphIterator().next().getStyleName();
Paragraph para = cell.getParagraphByIndex(0, false);
String paraStyle = (para != null ? para.getStyleName() : null);
template.setExtendedStyleByType(
TableTemplate.ExtendedStyleType.FIRSTROWSTARTCOLUM, cell
.getStyleName(), paraStyle);
TableTableCellElementBase oldCellEle = cell.getOdfElement();
TableTableCellElementBase newCellEle = (TableTableCellElementBase) oldCellEle
.cloneNode(true);
copyForeignStyleRef(newCellEle, cell.getOwnerDocument());
// first-row-end-column
cell = table.getCellByPosition(4, 0);
para = cell.getParagraphByIndex(0, false);
paraStyle = (para != null ? para.getStyleName() : null);
template.setExtendedStyleByType(
TableTemplate.ExtendedStyleType.FIRSTROWENDCOLUMN, cell
.getStyleName(), paraStyle);
oldCellEle = cell.getOdfElement();
newCellEle = (TableTableCellElementBase) oldCellEle.cloneNode(true);
copyForeignStyleRef(newCellEle, cell.getOwnerDocument());
// last-row-start-column
cell = table.getCellByPosition(0, 4);
para = cell.getParagraphByIndex(0, false);
paraStyle = (para != null ? para.getStyleName() : null);
template.setExtendedStyleByType(
TableTemplate.ExtendedStyleType.LASTROWSTARTCOLUMN, cell
.getStyleName(), paraStyle);
oldCellEle = cell.getOdfElement();
newCellEle = (TableTableCellElementBase) oldCellEle.cloneNode(true);
copyForeignStyleRef(newCellEle, cell.getOwnerDocument());
// last-row-end-column
cell = table.getCellByPosition(4, 4);
para = cell.getParagraphByIndex(0, false);
paraStyle = (para != null ? para.getStyleName() : null);
template.setExtendedStyleByType(
TableTemplate.ExtendedStyleType.LASTROWENDCOLUMN, cell
.getStyleName(), paraStyle);
oldCellEle = cell.getOdfElement();
newCellEle = (TableTableCellElementBase) oldCellEle.cloneNode(true);
copyForeignStyleRef(newCellEle, cell.getOwnerDocument());
// first column
cell = table.getCellByPosition(0, 1);
para = cell.getParagraphByIndex(0, false);
paraStyle = (para != null ? para.getStyleName() : null);
template.setTableFirstColumnStyle(cell.getStyleName(), paraStyle);
oldCellEle = cell.getOdfElement();
newCellEle = (TableTableCellElementBase) oldCellEle.cloneNode(true);
copyForeignStyleRef(newCellEle, cell.getOwnerDocument());
// last column
cell = table.getCellByPosition(4, 2);
para = cell.getParagraphByIndex(0, false);
paraStyle = (para != null ? para.getStyleName() : null);
template.setTableLastColumnStyle(cell.getStyleName(), paraStyle);
oldCellEle = cell.getOdfElement();
newCellEle = (TableTableCellElementBase) oldCellEle.cloneNode(true);
copyForeignStyleRef(newCellEle, cell.getOwnerDocument());
// first row
cell = table.getCellByPosition(1, 0);
para = cell.getParagraphByIndex(0, false);
paraStyle = (para != null ? para.getStyleName() : null);
template.setTableFirstRowStyle(cell.getStyleName(), paraStyle);
oldCellEle = cell.getOdfElement();
newCellEle = (TableTableCellElementBase) oldCellEle.cloneNode(true);
copyForeignStyleRef(newCellEle, cell.getOwnerDocument());
// last row
cell = table.getCellByPosition(1, 4);
para = cell.getParagraphByIndex(0, false);
paraStyle = (para != null ? para.getStyleName() : null);
template.setTableLastRowStyle(cell.getStyleName(), paraStyle);
oldCellEle = cell.getOdfElement();
newCellEle = (TableTableCellElementBase) oldCellEle.cloneNode(true);
copyForeignStyleRef(newCellEle, cell.getOwnerDocument());
// body (=odd row/column)
cell = table.getCellByPosition(1, 1);
para = cell.getParagraphByIndex(0, false);
paraStyle = (para != null ? para.getStyleName() : null);
template.setTableBodyStyle(cell.getStyleName(), paraStyle);
template.setTableOddRowsStyle(cell.getStyleName(), paraStyle);
template.setTableOddColumnsStyle(cell.getStyleName(), paraStyle);
oldCellEle = cell.getOdfElement();
newCellEle = (TableTableCellElementBase) oldCellEle.cloneNode(true);
copyForeignStyleRef(newCellEle, cell.getOwnerDocument());
// even row
cell = table.getCellByPosition(1, 2);
para = cell.getParagraphByIndex(0, false);
paraStyle = (para != null ? para.getStyleName() : null);
template.setTableEvenRowsStyle(cell.getStyleName(), paraStyle);
oldCellEle = cell.getOdfElement();
newCellEle = (TableTableCellElementBase) oldCellEle.cloneNode(true);
copyForeignStyleRef(newCellEle, cell.getOwnerDocument());
// even row
cell = table.getCellByPosition(2, 1);
para = cell.getParagraphByIndex(0, false);
paraStyle = (para != null ? para.getStyleName() : null);
template.setTableEvenColumnsStyle(cell.getStyleName(), paraStyle);
oldCellEle = cell.getOdfElement();
newCellEle = (TableTableCellElementBase) oldCellEle.cloneNode(true);
copyForeignStyleRef(newCellEle, cell.getOwnerDocument());
return template;
}
}