All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.odftoolkit.odfdom.changes.ChangesFileSaxHandler Maven / Gradle / Ivy

Go to download

ODFDOM is an OpenDocument Format (ODF) framework. Its purpose is to provide an easy common way to create, access and manipulate ODF files, without requiring detailed knowledge of the ODF specification. It is designed to provide the ODF developer community with an easy lightwork programming API portable to any object-oriented language. The current reference implementation is written in Java.

There is a newer version: 1.0.0-BETA1
Show newest version
/**
 * **********************************************************************
 *
 * 

DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER * *

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. * *

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.odfdom.changes; import static org.odftoolkit.odfdom.changes.OperationConstants.CONFIG_MAX_TABLE_CELLS; import static org.odftoolkit.odfdom.changes.OperationConstants.CONFIG_MAX_TABLE_COLUMNS; import static org.odftoolkit.odfdom.changes.OperationConstants.CONFIG_MAX_TABLE_ROWS; import static org.odftoolkit.odfdom.changes.OperationConstants.OPK_STYLE_ID; import static org.odftoolkit.odfdom.changes.PageArea.FOOTER_DEFAULT; import static org.odftoolkit.odfdom.changes.PageArea.FOOTER_EVEN; import static org.odftoolkit.odfdom.changes.PageArea.FOOTER_FIRST; import static org.odftoolkit.odfdom.changes.PageArea.HEADER_DEFAULT; import static org.odftoolkit.odfdom.changes.PageArea.HEADER_EVEN; import static org.odftoolkit.odfdom.changes.PageArea.HEADER_FIRST; import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.Stack; import java.util.logging.Level; import java.util.logging.Logger; import org.json.JSONException; import org.json.JSONObject; import org.odftoolkit.odfdom.doc.OdfDocument; import org.odftoolkit.odfdom.dom.OdfContentDom; import org.odftoolkit.odfdom.dom.OdfDocumentNamespace; import org.odftoolkit.odfdom.dom.OdfMetaDom; import org.odftoolkit.odfdom.dom.OdfSchemaConstraint; import org.odftoolkit.odfdom.dom.OdfSchemaDocument; import org.odftoolkit.odfdom.dom.OdfSettingsDom; import org.odftoolkit.odfdom.dom.OdfStylesDom; import org.odftoolkit.odfdom.dom.element.OdfStylableElement; import org.odftoolkit.odfdom.dom.element.OdfStyleableShapeElement; import org.odftoolkit.odfdom.dom.element.draw.DrawConnectorElement; import org.odftoolkit.odfdom.dom.element.draw.DrawFrameElement; import org.odftoolkit.odfdom.dom.element.draw.DrawGElement; import org.odftoolkit.odfdom.dom.element.draw.DrawImageElement; import org.odftoolkit.odfdom.dom.element.draw.DrawLineElement; import org.odftoolkit.odfdom.dom.element.draw.DrawMeasureElement; import org.odftoolkit.odfdom.dom.element.draw.DrawShapeElementBase; import org.odftoolkit.odfdom.dom.element.draw.DrawTextBoxElement; import org.odftoolkit.odfdom.dom.element.office.OfficeAnnotationElement; import org.odftoolkit.odfdom.dom.element.office.OfficeAnnotationEndElement; import org.odftoolkit.odfdom.dom.element.style.StyleFontFaceElement; import org.odftoolkit.odfdom.dom.element.style.StyleFooterStyleElement; import org.odftoolkit.odfdom.dom.element.style.StyleHeaderFooterPropertiesElement; import org.odftoolkit.odfdom.dom.element.style.StyleHeaderStyleElement; import org.odftoolkit.odfdom.dom.element.style.StyleMasterPageElement; import org.odftoolkit.odfdom.dom.element.style.StyleStyleElement; import org.odftoolkit.odfdom.dom.element.svg.SvgDescElement; import org.odftoolkit.odfdom.dom.element.table.TableCoveredTableCellElement; import org.odftoolkit.odfdom.dom.element.table.TableTableCellElement; import org.odftoolkit.odfdom.dom.element.table.TableTableColumnElement; import org.odftoolkit.odfdom.dom.element.table.TableTableElement; import org.odftoolkit.odfdom.dom.element.table.TableTableRowElement; import org.odftoolkit.odfdom.dom.element.text.TextAElement; import org.odftoolkit.odfdom.dom.element.text.TextHElement; import org.odftoolkit.odfdom.dom.element.text.TextLineBreakElement; import org.odftoolkit.odfdom.dom.element.text.TextListElement; import org.odftoolkit.odfdom.dom.element.text.TextListHeaderElement; import org.odftoolkit.odfdom.dom.element.text.TextListItemElement; import org.odftoolkit.odfdom.dom.element.text.TextListStyleElement; import org.odftoolkit.odfdom.dom.element.text.TextNoteCitationElement; import org.odftoolkit.odfdom.dom.element.text.TextPElement; import org.odftoolkit.odfdom.dom.element.text.TextParagraphElementBase; import org.odftoolkit.odfdom.dom.element.text.TextSElement; import org.odftoolkit.odfdom.dom.element.text.TextSpanElement; import org.odftoolkit.odfdom.dom.element.text.TextTabElement; import org.odftoolkit.odfdom.dom.element.text.TextUserFieldDeclElement; import org.odftoolkit.odfdom.dom.style.OdfStyleFamily; import org.odftoolkit.odfdom.incubator.doc.office.OdfOfficeAutomaticStyles; import org.odftoolkit.odfdom.incubator.doc.office.OdfOfficeStyles; import org.odftoolkit.odfdom.incubator.doc.style.OdfStyle; import org.odftoolkit.odfdom.incubator.doc.style.OdfStylePageLayout; import org.odftoolkit.odfdom.incubator.doc.text.OdfTextListStyle; import org.odftoolkit.odfdom.pkg.OdfAttribute; 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.OdfValidationException; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; import org.xml.sax.Attributes; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** @author svante.schubertATgmail.com */ public class ChangesFileSaxHandler extends org.odftoolkit.odfdom.pkg.OdfFileSaxHandler { private static final Logger LOG = Logger.getLogger(ChangesFileSaxHandler.class.getName()); private static final String ROW_SPAN = "rowSpan"; // ToDo: Fix API with its 'ugly' property name private static final String COLUMN_SPAN = "gridSpan"; // ODF value types used for cell content private static final String LIBRE_OFFICE_MS_INTEROP_NAMESPACE = "urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0"; private static final String LIBRE_OFFICE_MS_INTEROP_TYPE_CHECKBOX = "vnd.oasis.opendocument.field.FORMCHECKBOX"; private static final String LIBRE_OFFICE_MS_INTEROP_CHECKBOX_UNICODE = "\u25A1"; private static final Integer ONE = 1; public static final String COMMENT_PREFIX = "cmt"; // the empty XML file to which nodes will be added private OdfFileDom mFileDom; private JsonOperationProducer mJsonOperationProducer; private Map mAutoListStyles = null; private Map mUserFieldDecls = null; /** * Represents a stack of TextSpanElement. The text span will be added during startElement(..) with * the start address of text span And during endElement(..) the correct span will be returned and * the end address can be provided as well. */ private final ArrayDeque mTextSelectionStack; private final StringBuilder mCharsForElement = new StringBuilder(); // Text for operations will be collected separately to allow to push not at every new delimiter // (e.g. span). // In addition will be in the output string exchange to spaces private final StringBuilder mCharsForOperation = new StringBuilder(); // Gatheres the start text position for operations private boolean mIsCharsBeginning = true; List mCharsStartPosition = null; private int mComponentDepth = -1; // as component depth starts with zero // the actual component. Linking each other building the tree view of the document private Component mCurrentComponent; // the position of the component, being updated for the operations being generated private final LinkedList mLastComponentPositions = new LinkedList(); /** DOM is created by default, but is in general not needed */ private final boolean domCreationEnabled = true; // private final ArrayDeque mShapePropertiesStack; // *** TABLE PROPERTIES *** // ToDo: Move this table member variables to a special Table related parser/evaluator (Strategy // Pattern?) // name of the table or spreadsheet private String mTableName; private TableTableElement mTableElement; private List mColumns; // The relative widths of the columns of a table private List mColumnRelWidths; private int mColumnCount; // Required as the component table/draw can only be delayed created, // After the first child has been parsed.. And should ONLY ONCE created! private boolean isTableNew = false; Map mTableHardFormatting = null; // *** LIST Properties *** // @text:start-value is provided to the first paragraph only private int mListStartValue = -1; private final ArrayDeque mListStyleStack; // used to track in a text:h/text:p if currently whitespace is being deleted/trimmed private final ArrayDeque mWhitespaceStatusStack; /** * Quick cache to get the correct linked list. Key is the xml:id of the first list. The sequence * of all continued lists and usability functions are provided by ContinuedList */ private final Map mLinkedLists = new HashMap(); // *** FOR BLOCKING OPERATIONS // the number of elements above the current element during parsing. // Required to find out if the correct blocking element for the UI was found int mElementDepth = 0; // The depth of the element responsible of blocking further operations int mBlockingElementDepth = 0; boolean mNoOperationsAllowed = false; // All following blocking modes have different behavior boolean mIsBlockingFrame = false; // itself and children are allowed boolean mIsIgnoredElement = false; // not even itself allowed boolean mIsBlockingShape = false; // itself allowed // RunTimeConfiguration given by the caller of the ODF Adapter private int mMaxAllowedColumnCount; private int mMaxAllowedRowCount; private int mMaxAllowedCellCount; /** * LO/AOO/Calligra are applying to Hyperlinks the "Internet_20_link" style, without writing out * the dependency into XML. Therefore whenever a Hyperlink exists without character style * properties, the reference will be set. */ private static final String HYERLINK_DEFAULT_STYLE = "Internet_20_link"; private boolean mHasHyperlinkTemplateStyle = false; /** Properties for the HEADER_DEFAULT and FOOTER_DEFAULT page area. Defining the page layout */ private String mMasterPageStyleName = null; private String mPageLayoutName = null; /** ODF attribute on pageLayout */ private String mPageStyleUsage = null; /** indication of being a first page */ private boolean mHasNextMasterPage = false; private JSONObject headerAttrs = null; private JSONObject footerAttrs = null; /** * In the beginning it is only the styleId of the masterPage plus "HeaderDefault" or * "FooterDefault" */ private String mContextName = null; public static final String CONTEXT_DELIMITER = "_"; PageArea mPageArea = null; /** * "footer_default_" "footer_even_" "footer_first_" "header_default_" "header_even_" * "header_first_" */ /** The document might be of different types */ String mMediaType = null; /** * Required as the order of linked-list is important! All xml:ids of a connected/linked lists are * put into a single list. This collection is used to get the correct reference to the xml:id of * the preceding list and have to be updated, when linked lists are created, deleted or moved. * Only the text:continue-list of a new list will be evaluated */ class ContinuedList { private String mListId; private List mSortedIds = null; public ContinuedList(String precedingListId, String currentListId) { if (precedingListId != null && !precedingListId.isEmpty()) { mListId = precedingListId; } else { if (currentListId != null && !currentListId.isEmpty()) { mListId = currentListId; } } mSortedIds = new LinkedList(); } public void add(String listId) { mSortedIds.add(listId); } public List getListIds() { return mSortedIds; } public String getListId() { return mListId; } } /** * Checks if the preceding list is already part of a continued list, otherwise creates a new * continued list and adds both ids to it */ ContinuedList newContinuedList(String precedingListId, String currentListId) { ContinuedList continuedList; if (!mLinkedLists.containsKey(precedingListId)) { continuedList = new ContinuedList(precedingListId, currentListId); continuedList.add(precedingListId); mLinkedLists.put(precedingListId, continuedList); } else { continuedList = mLinkedLists.get(precedingListId); } if (currentListId != null && !currentListId.isEmpty()) { continuedList.add(currentListId); mLinkedLists.put(currentListId, continuedList); } return continuedList; } /** * Checks if the preceding list is already part of a continued list, otherwise creates a new * continued list and adds both id to it */ ContinuedList newContinuedList(String currentListId) { ContinuedList continuedList = null; if (currentListId != null && !currentListId.isEmpty()) { if (!mLinkedLists.containsKey(currentListId)) { continuedList = new ContinuedList(null, currentListId); mLinkedLists.put(currentListId, continuedList); } else { continuedList = mLinkedLists.get(currentListId); } } return continuedList; } /** * The whitespace status of a text container (ie. paragraph or heading). Required for whitespace * handling */ class WhitespaceStatus { WhitespaceStatus(boolean isParagraphIgnored, int depth) { mDepth = depth; // mIsParagraphIgnored = isParagraphIgnored; } int mDepth = -1; public int getParagraphDepth() { return mDepth; } boolean mOnlyWhiteSpaceSoFar = true; int mFirstSpaceCharPosition = -1; public boolean hasOnlyWhiteSpace() { return mOnlyWhiteSpaceSoFar; } public void setOnlyWhiteSpace(boolean onlyWhiteSpace) { mOnlyWhiteSpaceSoFar = onlyWhiteSpace; } /** During parsing the first character of space siblings. -1 if there is no space sibling */ public int getFirstSpaceCharPosition() { return mFirstSpaceCharPosition; } /** During parsing the first character of space siblings. -1 if there is no space sibling */ public void setFirstSpaceCharPosition(int currentSpaceCharPosition) { mFirstSpaceCharPosition = currentSpaceCharPosition; } /** @return true if the previous character was a white space character */ public boolean hasSpaceBefore() { return mFirstSpaceCharPosition > -1; } } OdfSchemaDocument mSchemaDoc = null; // Candidate Component Mode // Some components consist of multiple XML elements. // Even some ODF components start with the same // 2DO - DRAGON BOOK - Parser Look-ahead does not work with SAX? ;) // private boolean isCandidateComponentMode = true; public ChangesFileSaxHandler(Node rootNode) throws SAXException { super(rootNode); // Initialize starting DOM node if (rootNode instanceof OdfFileDom) { mFileDom = (OdfFileDom) rootNode; } else { mFileDom = (OdfFileDom) rootNode.getOwnerDocument(); } mCurrentNode = rootNode; // *** COMPONENT HANDLING *** // Initialize starting Component // Make the root of component tree (to be created) accessible via the ODF schema document mSchemaDoc = (OdfSchemaDocument) mFileDom.getDocument(); if (mSchemaDoc != null) { // cash the unfinished DOM otherwise, styles.xml might be tried to be parsed again if (mFileDom instanceof OdfContentDom) { mSchemaDoc.setContentDom((OdfContentDom) mFileDom); } else if (mFileDom instanceof OdfStylesDom) { mSchemaDoc.setStylesDom((OdfStylesDom) mFileDom); } else if (mFileDom instanceof OdfMetaDom) { mSchemaDoc.setMetaDom((OdfMetaDom) mFileDom); } else if (mFileDom instanceof OdfSettingsDom) { mSchemaDoc.setSettingsDom((OdfSettingsDom) mFileDom); } } // The current component is the root component mCurrentComponent = null; // Getting Configuration Map configuration = mSchemaDoc.getPackage().getRunTimeConfiguration(); mMaxAllowedColumnCount = OperationConstants.MAX_SUPPORTED_COLUMNS_NUMBER; mMaxAllowedRowCount = OperationConstants.MAX_SUPPORTED_ROWS_NUMBER; mMaxAllowedCellCount = OperationConstants.MAX_SUPPORTED_CELLS_NUMBER; mMediaType = mSchemaDoc.getMediaTypeString(); if (configuration != null) { if (configuration.containsKey(CONFIG_MAX_TABLE_COLUMNS)) { mMaxAllowedColumnCount = (Integer) configuration.get(CONFIG_MAX_TABLE_COLUMNS); } if (configuration.containsKey(CONFIG_MAX_TABLE_ROWS)) { mMaxAllowedRowCount = (Integer) configuration.get(CONFIG_MAX_TABLE_ROWS); } if (configuration.containsKey(CONFIG_MAX_TABLE_CELLS)) { mMaxAllowedCellCount = (Integer) configuration.get(CONFIG_MAX_TABLE_CELLS); } } LOG.log(Level.FINEST, "mMaxTableColumnCount{0}", mMaxAllowedColumnCount); LOG.log(Level.FINEST, "mMaxTableRowCount{0}", mMaxAllowedRowCount); LOG.log(Level.FINEST, "mMaxTableCellCount{0}", mMaxAllowedCellCount); // Make the Operation Queue to be created accessible via the Schema Document mJsonOperationProducer = mSchemaDoc.getJsonOperationQueue(); if (mJsonOperationProducer == null) { // temporary initiated here as all the tests are not using the OperationTextDocument mJsonOperationProducer = new JsonOperationProducer(); mSchemaDoc.setJsonOperationQueue(mJsonOperationProducer); } mAutoListStyles = new HashMap(); mUserFieldDecls = new HashMap(); // Stack to remember/track the nested delimiters not being components (spans) open-up by SAX // events mTextSelectionStack = new ArrayDeque(); mListStyleStack = new ArrayDeque(); // mShapePropertiesStack = new ArrayDeque(); mWhitespaceStatusStack = new ArrayDeque(); } @Override public void startDocument() throws SAXException {} @Override public void endDocument() throws SAXException {} /** * There are areas that are not allowed to addChild further components beyond. All further * operations have to be blocked, but the creation of the DOM tree must not be disturbed. */ private boolean isBlockedSubTree() { return mNoOperationsAllowed; } /** * There are areas that are not allowed to addChild further components beyond. All further * operations have to be blocked, but the creation of the DOM tree must not be disturbed. */ private boolean checkEndOfBlockedSubTree(String uri, String localName) { boolean isBlocked = mNoOperationsAllowed; if (mNoOperationsAllowed) { isBlocked = isBlockedSubTree(uri, localName, false); } mElementDepth--; return isBlocked; } private boolean checkStartOfBlockedSubTree(String uri, String localName) { mElementDepth++; boolean isBlocked = mNoOperationsAllowed; if (!mNoOperationsAllowed) { isBlocked = isBlockedSubTree(uri, localName, true); } else if (mIsBlockingFrame) { if (mBlockingElementDepth == mElementDepth - 1 && !localName.equals("table")) { isBlocked = false; } else { isBlocked = true; } } return isBlocked; } // ToDo: Differentiate if there is a shapeBlock, ImageBlock or ParagraphBlock private boolean isBlockedSubTree(String uri, String localName, boolean isStart) { // within a paragraph within a paragraph boolean isBlocked = mNoOperationsAllowed; boolean isMasterPage = uri != null && uri.equals(StyleMasterPageElement.ELEMENT_NAME.getUri()) && localName.equals(StyleMasterPageElement.ELEMENT_NAME.getLocalName()); if (isStart) { // if it is a second text component (ie. text:p or text:h element) if ( /*!mWhitespaceStatusStack.isEmpty() && Component.isTextComponentRoot(uri, localName) || */ OdfElement .isIgnoredElement(uri, localName) || ((isMasterPage || Component.isHeaderRoot(uri, localName) || Component.isFooterRoot(uri, localName)) && OdfDocument.OdfMediaType.TEXT.getMediaTypeString() != mMediaType && OdfDocument.OdfMediaType.SPREADSHEET.getMediaTypeString() != mMediaType)) { isBlocked = true; mNoOperationsAllowed = true; mIsIgnoredElement = true; mBlockingElementDepth = mElementDepth; // if it is a } } else { // if this is the closing event of an element if (mNoOperationsAllowed) { if (mBlockingElementDepth == mElementDepth) { if (mIsIgnoredElement && ( /*!mWhitespaceStatusStack.isEmpty() && Component.isTextComponentRoot(uri, localName) || */ OdfElement .isIgnoredElement(uri, localName)) || ((isMasterPage || Component.isHeaderRoot(uri, localName) || Component.isFooterRoot(uri, localName)) && OdfDocument.OdfMediaType.TEXT.getMediaTypeString() != mMediaType && OdfDocument.OdfMediaType.SPREADSHEET.getMediaTypeString() != mMediaType)) { mIsIgnoredElement = false; mBlockingElementDepth = -1; mNoOperationsAllowed = false; isBlocked = true; // if it is a } } else if (mIsBlockingFrame && mBlockingElementDepth == mElementDepth - 1 && !localName.equals("table")) { isBlocked = false; } } else { // closing will never enabled a blocking if (mIsIgnoredElement || mIsBlockingShape) { // close this element, but afterwards mNoOperationsAllowed = true; isBlocked = false; } else if (mIsBlockingFrame) { if (mBlockingElementDepth == mElementDepth - 1 && !localName.equals("table")) { isBlocked = false; } else { isBlocked = true; } } } } return isBlocked; } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { flushTextAtStart(uri, localName, qName); // if there is a specilized handler on the stack, dispatch the event OdfElement element = null; // ToDo: Should be able to create operations without creating the DOM Tree // ToDo: Are there use-cases that text:s still resides in the DOM? if(isWhiteSpaceElement) ?? // If Paragraph is not being edited, it will be saved as it is.. if (domCreationEnabled) { if (uri.equals(Constants.EMPTY_STRING) || qName.equals(Constants.EMPTY_STRING)) { element = mFileDom.createElement(localName); } else { // == correct: if localName is the same object as qName, there is a default namespace set if (localName == qName) { element = mFileDom.createElementNS( OdfName.getOdfName(OdfNamespace.newNamespace(null, uri), localName)); } else { element = mFileDom.createElementNS(uri, qName); } } addAttributes(element, attributes); } // if it is the last page bound object then move all the nodes to a temporary location if (mComponentDepth < 0 && m_cachedPageShapes.size() > 0 && (localName.equals("p") || localName.equals("h") || localName.equals("table"))) { // move nodes Node bodyNode = mCurrentNode.getParentNode(); Iterator it = m_cachedPageShapes.iterator(); while (it.hasNext()) { ShapeProperties component = it.next(); bodyNode.insertBefore(component.mOwnNode, bodyNode.getFirstChild()); } mLastComponentPositions.clear(); } // Font declarations are before the component if (element instanceof StyleFontFaceElement) { String fontName = element.getAttributeNS(OdfDocumentNamespace.STYLE.getUri(), "name"); if (fontName != null && !fontName.isEmpty()) { Set fontNames = ((OdfDocument) mSchemaDoc).getFontNames(); if (!fontNames.contains(fontName)) { mJsonOperationProducer.addFontData( fontName, null, element.getAttributeNS(OdfDocumentNamespace.SVG.getUri(), "font-family"), element.getAttributeNS(OdfDocumentNamespace.STYLE.getUri(), "font-family-generic"), element.getAttributeNS(OdfDocumentNamespace.STYLE.getUri(), "font-pitch"), element.getAttributeNS(OdfDocumentNamespace.SVG.getUri(), "panose-1")); fontNames.add(fontName); } } } if (element instanceof TextListStyleElement) { // We need the reference for later gettin the list styles TextListStyleElement listStyle = (TextListStyleElement) element; String styleName = listStyle.getAttributeNS(OdfDocumentNamespace.STYLE.getUri(), "name"); if (styleName != null && !styleName.isEmpty()) { mAutoListStyles.put(styleName, listStyle); } } else if (element instanceof TextUserFieldDeclElement) { TextUserFieldDeclElement fieldDecl = (TextUserFieldDeclElement) element; mUserFieldDecls.put(fieldDecl.getAttribute("text:name"), fieldDecl); } if (!checkStartOfBlockedSubTree(uri, localName)) { if (Component.isComponentRoot( uri, localName)) { // || Component.isCoveredComponentRoot(uri, localName)) { // It is not allowed to addChild further components.., // within a paragraph within a paragraph // ToDo ? -- HashMap with KEY - URL+localname, VALUE - ComponentName if (element instanceof TextPElement || element instanceof TextHElement) { // Paragraphs that are not child of a known component should be ignored, otherwise the // client gets into trouble with nested paragraphs boolean isNestedParagraph = false; if (!isNestedParagraph) { mComponentDepth++; TextParagraphElementBase p = (TextParagraphElementBase) element; Map hardFormatting = mJsonOperationProducer.getHardStyles(p); if (hardFormatting == null) { hardFormatting = new HashMap(); } if (element instanceof TextHElement || !mListStyleStack.isEmpty()) { if (!hardFormatting.containsKey("paragraph")) { // if there are absolute styles, but not the main property set, where the // templateStyleId should be placed in hardFormatting.put("paragraph", new JSONObject()); } JSONObject paraProps = (JSONObject) hardFormatting.get("paragraph"); try { if (!mListStyleStack.isEmpty()) { paraProps.put("listLevel", mListStyleStack.size() - 1); // Only the first paragraph within a list item should show a label! ParagraphListProperties listProps = mListStyleStack.getLast(); if (listProps.hasListLabel()) { listProps.showListLabel(Boolean.FALSE); } else { paraProps.put("listLabelHidden", Boolean.TRUE); } String listId = listProps.getListId(); if (listId != null && !listId.isEmpty()) { paraProps.put("listId", listId); } boolean foundListXmlId = false; boolean foundListItemXmlId = false; Iterator listPropsIter = mListStyleStack.descendingIterator(); while ((!foundListXmlId || !foundListItemXmlId) && listPropsIter.hasNext()) { ParagraphListProperties currentListProp = listPropsIter.next(); String listXmlId = currentListProp.getListXmlId(); if (!foundListXmlId && listXmlId != null && !listXmlId.isEmpty()) { foundListXmlId = true; paraProps.put("listXmlId", listXmlId); } String listItemXmlId = currentListProp.getListItemXmlId(); if (!foundListItemXmlId && listItemXmlId != null && !listItemXmlId.isEmpty()) { foundListItemXmlId = true; paraProps.put("listItemXmlId", listItemXmlId); } } if (listProps.isListStart()) { paraProps.put("listStart", Boolean.TRUE); } String listStyleId = JsonOperationProducer.getListStyle(mListStyleStack, p); if (listStyleId != null && !listStyleId.isEmpty()) { mJsonOperationProducer.addListStyle(mSchemaDoc, mAutoListStyles, listStyleId); paraProps.put("listStyleId", listStyleId); } else { paraProps.put("listStyleId", Constants.ODFTK_DEFAULT_LIST); } if (mListStartValue != -1) { paraProps.put("listStartValue", mListStartValue); mListStartValue = -1; } } // Add heading outline numbering if (element instanceof TextHElement) { Integer outlineLevel = ((TextHElement) element).getTextOutlineLevelAttribute(); if (outlineLevel != null) { paraProps.put("outlineLevel", outlineLevel); } } } catch (JSONException ex) { Logger.getLogger(ChangesFileSaxHandler.class.getName()).log(Level.SEVERE, null, ex); } } List position = updateComponentPosition(); OdfStyle templateStyle = p.getDocumentStyle(); String styleId = null; if (templateStyle != null) { styleId = templateStyle.getStyleNameAttribute(); if (styleId != null && !styleId.isEmpty()) { hardFormatting.put(OPK_STYLE_ID, styleId); } } mCurrentComponent = mCurrentComponent.createChildComponent(p); boolean paragraphOpCreated = false; if (!mPageBoundObjectsRelocated && !m_cachedPageShapes.isEmpty()) { // first document paragraph might be inside of a table boolean isFirstDocumentParagraph = mComponentStack.empty() || mComponentStack.peek() instanceof CachedTable; if (isFirstDocumentParagraph && m_cachedPageShapes.size() > 0) { cacheOperation( false, OperationConstants.PARAGRAPH, position, false, hardFormatting, mContextName); paragraphOpCreated = true; Iterator it = m_cachedPageShapes.iterator(); while (it.hasNext()) { ShapeProperties component = it.next(); Component frameComponent = component.getDrawFrameElement().getComponent(); Component frameComponentParent = frameComponent.getParent(); int framePosition = frameComponentParent.indexOf(frameComponent); frameComponentParent.remove(framePosition); element.appendChild(component.mOwnNode); component.mShapePosition.addAll(0, position); component.createShapeOperation( this, mComponentStack, component.mDescription, component.hasImageSibling() ? ShapeType.ImageShape : component.isGroupShape() ? ShapeType.GroupShape : ShapeType.NormalShape, component.mContext); Iterator opIter = component.iterator(); while (opIter.hasNext()) { CachedOperation op = opIter.next(); List start = op.mStart; if (!op.mAbsolutePosition) { if (op.mComponentType.equals(OperationConstants.ATTRIBUTES)) { @SuppressWarnings("unchecked") List end = (List) op.mComponentProperties[0]; // TODO: add _real_ position of the current paragraph (could be in a // table...) end.addAll(0, position); } // TODO: add _real_ position of the current paragraph (could be in a table...) start.addAll(0, position); } cacheOperation( false, op.mComponentType, start, false, op.mHardFormattingProperties, op.mComponentProperties); } } m_cachedPageShapes.clear(); } mPageBoundObjectsRelocated |= isFirstDocumentParagraph; } if (!paragraphOpCreated) { cacheOperation( false, OperationConstants.PARAGRAPH, position, false, hardFormatting, mContextName); } // For each new paragraph/heading addChild a new context information for their // whitespace, required for normalization mWhitespaceStatusStack.add(new WhitespaceStatus(false, mComponentDepth)); element.markAsComponentRoot(true); // ToDo: NEW COMPONENTS - SECTION // } else if (element instanceof TextSectionElement) { // mJsonOperationProducer.addChild("Section", position); // mCurrentComponent = mCurrentComponent.addChild((TextSectionElement) // element); } else { // a nested text component without known component in-between // ignore nested paragraph content mWhitespaceStatusStack.add(new WhitespaceStatus(true, mComponentDepth)); element.ignoredComponent(true); } } else if (element instanceof DrawFrameElement || Component.isShapeElement(uri, localName)) { OdfElement shape = element; Map hardFormatting = null; if (element instanceof OdfStyleableShapeElement) { hardFormatting = mJsonOperationProducer.getHardStyles((OdfStyleableShapeElement) shape); } if (hardFormatting == null || !hardFormatting.containsKey("drawing")) { // if there are absolute styles, but not the main property set, where the // templateStyleId should be placed in if (hardFormatting == null) { hardFormatting = new HashMap(); } hardFormatting.put("drawing", new JSONObject()); } JSONObject drawingProps = (JSONObject) hardFormatting.get("drawing"); if (hardFormatting == null || !hardFormatting.containsKey("image")) { // if there are absolute styles, but not the main property set, where the // templateStyleId should be placed in if (hardFormatting == null) { hardFormatting = new HashMap(); } hardFormatting.put("image", new JSONObject()); } int anchorHorOffset = 0; int anchorVertOffset = 0; int anchorLayerOrder = 0; int width = 0; int height = 0; if (shape instanceof DrawShapeElementBase) { Integer zIndex = ((DrawShapeElementBase) shape).getDrawZIndexAttribute(); if (null != zIndex) { anchorLayerOrder = zIndex; } } if (element instanceof DrawLineElement || element instanceof DrawConnectorElement || element instanceof DrawMeasureElement) { if (shape.hasAttributeNS(OdfDocumentNamespace.SVG.getUri(), "y1") && shape.hasAttributeNS(OdfDocumentNamespace.SVG.getUri(), "x1") && shape.hasAttributeNS(OdfDocumentNamespace.SVG.getUri(), "y2") && shape.hasAttributeNS(OdfDocumentNamespace.SVG.getUri(), "x2")) { int x1 = MapHelper.normalizeLength( shape.getAttributeNS(OdfDocumentNamespace.SVG.getUri(), "x1")); int x2 = MapHelper.normalizeLength( shape.getAttributeNS(OdfDocumentNamespace.SVG.getUri(), "x2")); int y1 = MapHelper.normalizeLength( shape.getAttributeNS(OdfDocumentNamespace.SVG.getUri(), "y1")); int y2 = MapHelper.normalizeLength( shape.getAttributeNS(OdfDocumentNamespace.SVG.getUri(), "y2")); anchorHorOffset = Math.min(x1, x2); width = Math.abs(x2 - x1) + 1; anchorVertOffset = Math.min(y1, y2); height = Math.abs(y2 - y1) + 1; } } else { if (shape.hasAttributeNS(OdfDocumentNamespace.SVG.getUri(), "width")) { width = MapHelper.normalizeLength( shape.getAttributeNS(OdfDocumentNamespace.SVG.getUri(), "width")); } if (shape.hasAttributeNS(OdfDocumentNamespace.SVG.getUri(), "height")) { height = MapHelper.normalizeLength( shape.getAttributeNS(OdfDocumentNamespace.SVG.getUri(), "height")); } if (shape.hasAttributeNS(OdfDocumentNamespace.SVG.getUri(), "x")) { anchorHorOffset = MapHelper.normalizeLength( shape.getAttributeNS(OdfDocumentNamespace.SVG.getUri(), "x")); } if (shape.hasAttributeNS(OdfDocumentNamespace.SVG.getUri(), "y")) { anchorVertOffset = MapHelper.normalizeLength( shape.getAttributeNS(OdfDocumentNamespace.SVG.getUri(), "y")); } } try { if (height != 0) { drawingProps.put("height", height); } if (width != 0) { drawingProps.put("width", width); } if (anchorHorOffset != 0) { drawingProps.put("anchorHorOffset", anchorHorOffset); drawingProps.put("left", anchorHorOffset); } if (anchorVertOffset != 0) { drawingProps.put("anchorVertOffset", anchorVertOffset); drawingProps.put("top", anchorVertOffset); } if (anchorLayerOrder != 0) { drawingProps.put("anchorLayerOrder", anchorLayerOrder); } } catch (JSONException ex) { Logger.getLogger(ChangesFileSaxHandler.class.getName()).log(Level.SEVERE, null, ex); } if (shape.hasAttributeNS(OdfDocumentNamespace.DRAW.getUri(), "transform")) { try { String transform = shape.getAttributeNS(OdfDocumentNamespace.DRAW.getUri(), "transform"); int index = transform.indexOf("translate"); if (index >= 0) { index = transform.indexOf('(', index); transform = transform.substring(index, transform.length()); int separator = transform.indexOf(' '); String leftValue = transform.substring(1, separator); index = transform.indexOf(')', separator); String rightValue = transform.substring(separator + 1, index); anchorHorOffset += MapHelper.normalizeLength(leftValue); anchorVertOffset += MapHelper.normalizeLength(rightValue); } if (anchorVertOffset != 0) { drawingProps.put("anchorVertOffset", anchorVertOffset); } if (anchorHorOffset != 0) { drawingProps.put("anchorHorOffset", anchorHorOffset); } } catch (IndexOutOfBoundsException ex) { Logger.getLogger(ChangesFileSaxHandler.class.getName()).log(Level.SEVERE, null, ex); } catch (JSONException ex) { Logger.getLogger(ChangesFileSaxHandler.class.getName()).log(Level.SEVERE, null, ex); } } // // // page // frame // paragraph // char // as-char // // /* API: anchorHorBase: Horizontal anchor mode: One of 'margin', 'page', 'column', 'character', 'leftMargin', 'rightMargin', 'insideMargin', or 'outsideMargin'. /* @text:anchor-type: h=anchorHorBase & v=anchorVerBase page => h=page v=page frame => h=column v=margin paragraph => h=column v=paragraph char => h=character v=paragraph as-char => inline & h & v weglassen*/ if (shape.hasAttributeNS(OdfDocumentNamespace.TEXT.getUri(), "anchor-type")) { try { String anchorVertBase = null; String anchorHorBase = null; String anchorType = shape.getAttributeNS(OdfDocumentNamespace.TEXT.getUri(), "anchor-type"); if (anchorType.equals("page")) { // Changes API: true: image as character, false: floating mode drawingProps.put("inline", Boolean.FALSE); // page anchor requires page relation drawingProps.put("anchorHorBase", "page"); drawingProps.put("anchorVertBase", "page"); } else if (anchorType.equals("frame")) { // Changes API: true: image as character, false: floating mode drawingProps.put("inline", Boolean.FALSE); anchorVertBase = "column"; anchorVertBase = "margin"; } else if (anchorType.equals("paragraph")) { // Changes API: true: image as character, false: floating mode drawingProps.put("inline", Boolean.FALSE); anchorHorBase = "column"; anchorVertBase = "paragraph"; } else if (anchorType.equals("char")) { // Changes API: true: image as character, true: floating mode drawingProps.put("inline", Boolean.FALSE); anchorHorBase = "character"; anchorVertBase = "paragraph"; } else if (anchorType.equals("as-char")) { // Changes API: true: image as character, false: floating mode drawingProps.put("inline", Boolean.TRUE); } if (anchorVertBase != null && !drawingProps.has("anchorVertBase")) { drawingProps.put("anchorVertBase", anchorVertBase); } if (anchorHorBase != null && !drawingProps.has("anchorHorBase")) { drawingProps.put("anchorHorBase", anchorHorBase); } } catch (JSONException ex) { Logger.getLogger(ChangesFileSaxHandler.class.getName()).log(Level.SEVERE, null, ex); } } hardFormatting.put("drawing", drawingProps); mComponentDepth++; List pos = updateComponentPosition(); // the delay of the operation was not the solution, as the children would be added fist // instead a setAttribute would be more appropriate // even if there is a automatic style, only the template style is required if (element instanceof OdfStyleableShapeElement) { String styleId = ((OdfStyleableShapeElement) shape).getDocumentStyleName(); if (styleId != null && !styleId.isEmpty()) { hardFormatting.put(OPK_STYLE_ID, styleId); } } ShapeProperties shapeProps = new ShapeProperties(pos, hardFormatting); // special handling for frames as together with the image child they are a single user // component if (element instanceof DrawFrameElement) { shapeProps.setDrawFrameElement((DrawFrameElement) shape); if (!mComponentStack.isEmpty()) { final CachedComponent comp = mComponentStack.peek(); if (comp instanceof ShapeProperties && ((ShapeProperties) comp).getDrawFrameElement() != null) { LOG.warning("Feature 'Frame attached to Frame' yet unsupported"); } } } else if (element instanceof DrawGElement) { shapeProps.setGroupShape(); element.markAsComponentRoot(true); } if (mCurrentComponent != null) { mComponentStack.push(shapeProps); mCurrentComponent = mCurrentComponent.createChildComponent(element); } // mShapePropertiesStack.push(shapeProps); // table component (table within a text document or a spreadsheet) } else if (element instanceof TableTableElement) { mComponentDepth++; // The table will be created with column width, after columns are parsed (just before // first row!) updateComponentPosition(); // tables are not written out directly, but its operation collected and only flushed // if they are not exceeding a maximum size isTableNew = true; mTableElement = (TableTableElement) element; mCurrentComponent = mCurrentComponent.createChildComponent(mTableElement); // initialize a new list for the relative column widths // ToDo: Receive the styles from the root component // ToDo: If I do not want a DOM, do I have to parse the styles and addChild them to // component? // Do I have to parse the styles.xml first to get the props as maps (hashmaps)? if (mTableElement.hasAttributeNS(OdfDocumentNamespace.TABLE.getUri(), "style-name")) { mTableHardFormatting = mJsonOperationProducer.getHardStyles(mTableElement); String styleId = mTableElement.getDocumentStyleName(); if (styleId != null && !styleId.isEmpty()) { if (mTableHardFormatting == null) { mTableHardFormatting = new HashMap<>(); } mTableHardFormatting.put(OPK_STYLE_ID, styleId); // All ODF styles are hard formatted // JSONObject tableProps = mTableHardFormatting.get("table"); // mTableHardFormatting.put("templateStyleId", // table.getDocumentStyle().getStyleNameAttribute()); // OdfStyle tableStyle = table.getDocumentStyle(); // if(tableStyle != null){ // mTableDisplayName = tableStyle.getAttributeNS(OdfDocumentNamespace.STYLE.getUri(), // "display-name"); } } else { mTableHardFormatting = new HashMap<>(); } mTableName = mTableElement.getAttributeNS(OdfDocumentNamespace.TABLE.getUri(), "name"); mColumnRelWidths = new LinkedList<>(); element.markAsComponentRoot(true); } else if (element instanceof TableTableRowElement) { mComponentDepth++; if (isTableNew) { // In case neiter relative nor absolute table column width were given, all column are // equal sized (given rel size of '1') mColumnRelWidths = Table.collectColumnWidths(mTableElement, mColumns); mColumns.clear(); if (mColumnRelWidths != null && mColumnRelWidths.isEmpty()) { for (int i = 0; i < mColumnCount; i++) { mColumnRelWidths.add(ONE); } } // The grid is known after columns had been parsed, updating later to row positino List tablePosition = new LinkedList(mLastComponentPositions); cacheTableOperation( OperationConstants.TABLE, tablePosition, mTableHardFormatting, mColumnRelWidths, mTableName); mTableHardFormatting = null; isTableNew = false; mTableName = null; mColumnCount = 0; mColumnRelWidths = null; } List position = updateComponentPosition(); TableTableRowElement row = (TableTableRowElement) element; mCurrentComponent = mCurrentComponent.createChildComponent(row); // repeatition can cause a different positioning int repeatedRows = 1; if (row.hasAttributeNS(OdfDocumentNamespace.TABLE.getUri(), "number-rows-repeated")) { repeatedRows = Integer.parseInt( row.getAttributeNS( OdfDocumentNamespace.TABLE.getUri(), "number-rows-repeated")); mCurrentComponent.hasRepeated(true); } boolean isVisible = Boolean.TRUE; if (row.hasAttributeNS(OdfDocumentNamespace.TABLE.getUri(), "visibility")) { isVisible = Constants.VISIBLE.equals( row.getAttributeNS(OdfDocumentNamespace.TABLE.getUri(), "visibility")); } Map hardFormatting = mJsonOperationProducer.getHardStyles(row); OdfStyle templateStyle = row.getDocumentStyle(); String styleId = null; if (templateStyle != null) { styleId = templateStyle.getStyleNameAttribute(); if (styleId != null && !styleId.isEmpty()) { hardFormatting.put(OPK_STYLE_ID, styleId); } } if (!isVisible) { JSONObject rowProps; if (hardFormatting == null) { // if there are absolute styles, but not the main property set, where the // templateStyleId should be placed in if (hardFormatting == null) { hardFormatting = new HashMap(); } } if (!hardFormatting.containsKey("row")) { rowProps = new JSONObject(); hardFormatting.put("row", rowProps); } else { rowProps = (JSONObject) hardFormatting.get("row"); if (rowProps == null) { rowProps = new JSONObject(); } } try { rowProps.put("visible", Boolean.FALSE); } catch (JSONException ex) { Logger.getLogger(ChangesFileSaxHandler.class.getName()).log(Level.SEVERE, null, ex); } } cacheTableOperation(OperationConstants.ROWS, position, hardFormatting, repeatedRows); element.markAsComponentRoot(true); } else if (element instanceof TableTableCellElement || element instanceof TableCoveredTableCellElement) { boolean covered = element instanceof TableCoveredTableCellElement; mComponentDepth++; TableTableCellElement cell = covered ? null : (TableTableCellElement) element; if (cell != null) { mCurrentComponent = mCurrentComponent.createChildComponent(cell); } else { mCurrentComponent = mCurrentComponent.createChildComponent(element); } CachedTable cachedTableOps = (CachedTable) mComponentStack.peek(); cachedTableOps.setCellRepetition(1); int repetition = 1; Map hardFormatting = null; if (!covered) { hardFormatting = mJsonOperationProducer.getHardStyles(cell); } // repeatition and covering can cause a different positioning // ToDo: To make DOM optional, work on the component instead of the element. Check // directly SAX attributes parameter! if (element.hasAttributeNS( OdfDocumentNamespace.TABLE.getUri(), "number-columns-repeated")) { // cellProps.put("repeatedColumns", // cell.getAttributeNS(OdfDocumentNamespace.TABLE.getUri(), // "number-columns-repeatedColumns")); cachedTableOps.setCellRepetition( Integer.parseInt( element.getAttributeNS( OdfDocumentNamespace.TABLE.getUri(), "number-columns-repeated"))); repetition = Integer.parseInt( element.getAttributeNS( OdfDocumentNamespace.TABLE.getUri(), "number-columns-repeated")); mCurrentComponent.hasRepeated(true); } if (cell != null && cell.hasAttributes()) { try { // if there are absolute styles, but not the main property set, where the // templateStyleId should be placed in if (hardFormatting == null || !hardFormatting.containsKey("cell")) { if (hardFormatting == null) { hardFormatting = new HashMap(); } } JSONObject cellProps = (JSONObject) hardFormatting.get("cell"); if (cellProps == null) { cellProps = new JSONObject(); } if (cell.hasAttributeNS( OdfDocumentNamespace.TABLE.getUri(), "number-columns-spanned")) { cellProps.put( COLUMN_SPAN, Integer.parseInt( cell.getAttributeNS( OdfDocumentNamespace.TABLE.getUri(), "number-columns-spanned"))); } if (cell.hasAttributeNS(OdfDocumentNamespace.TABLE.getUri(), "number-rows-spanned")) { cellProps.put( ROW_SPAN, Integer.parseInt( cell.getAttributeNS( OdfDocumentNamespace.TABLE.getUri(), "number-rows-spanned"))); } if (cellProps.length() != 0) { hardFormatting.put("cell", cellProps); } } catch (JSONException ex) { Logger.getLogger(ChangesFileSaxHandler.class.getName()).log(Level.SEVERE, null, ex); } } List position = updateComponentPosition(); OdfStyle templateStyle = covered ? null : cell.getDocumentStyle(); if (templateStyle != null) { String styleId = templateStyle.getStyleNameAttribute(); if (styleId != null && !styleId.isEmpty()) { hardFormatting.put(OPK_STYLE_ID, styleId); } } cacheTableOperation( OperationConstants.CELLS, position, hardFormatting, mCurrentComponent, repetition); element.markAsComponentRoot(true); } else if (element instanceof TextLineBreakElement) { mComponentDepth++; TextLineBreakElement lineBreak = (TextLineBreakElement) element; List position = updateComponentPosition(); mCurrentComponent = mCurrentComponent.createChildComponent(lineBreak); cacheOperation(false, OperationConstants.LINE_BREAK, position, false, null, null, null); element.markAsComponentRoot(true); } else if (element instanceof TextTabElement) { mComponentDepth++; TextTabElement tab = (TextTabElement) element; List position = updateComponentPosition(); mCurrentComponent = mCurrentComponent.createChildComponent(tab); cacheOperation(false, OperationConstants.TAB, position, false, null, null, null); element.markAsComponentRoot(true); } else if (Component.isField(uri, localName)) { mComponentDepth++; List position = updateComponentPosition(); mCurrentComponent = mCurrentComponent.createChildComponent(element); TextFieldSelection selection = null; if (element.hasAttributeNS(LIBRE_OFFICE_MS_INTEROP_NAMESPACE, "type") && element .getAttributeNS(LIBRE_OFFICE_MS_INTEROP_NAMESPACE, "type") .equals(LIBRE_OFFICE_MS_INTEROP_TYPE_CHECKBOX)) { selection = new TextFieldSelection(element, position, LIBRE_OFFICE_MS_INTEROP_CHECKBOX_UNICODE); } else { if (mFileDom instanceof OdfContentDom) { selection = new TextFieldSelection( element, position, ((OdfContentDom) mFileDom).getAutomaticStyles(), mUserFieldDecls); } else { selection = new TextFieldSelection( element, position, ((OdfStylesDom) mFileDom).getAutomaticStyles(), mUserFieldDecls); } // kann auch (OdfStylesDom) sein! // element.getParentNode(); // TextTimeElement telem = (TextTimeElement)element; // Map hardFormatting = // mJsonOperationProducer.getHardStyles(telem); } mTextSelectionStack.add(selection); } else if (element instanceof OfficeAnnotationElement) { ++mComponentDepth; if (mIsCharsBeginning) { updateTextPosition(); } mCurrentComponent = mCurrentComponent.createChildComponent(element); String annotationName = ((OfficeAnnotationElement) element).getOfficeNameAttribute(); if (annotationName == null) { // annotations without range don't have a name attribute annotationName = ((OdfDocument) mSchemaDoc).getUniqueAnnotationName(); } CommentComponent commentProps = new CommentComponent(mLastComponentPositions, annotationName); ((OdfDocument) mSchemaDoc) .addAnnotation(annotationName, ((OfficeAnnotationElement) element)); mComponentStack.push(commentProps); element.markAsComponentRoot(true); } else if (element instanceof OfficeAnnotationEndElement) { mComponentDepth++; List position = updateComponentPosition(); String id = COMMENT_PREFIX; id += ((OfficeAnnotationEndElement) element).getOfficeNameAttribute(); cacheOperation( false, OperationConstants.COMMENTRANGE, position, false, null, id, mContextName); mCurrentComponent = mCurrentComponent.createChildComponent(element); element.markAsComponentRoot(true); } else { mComponentDepth++; element.markAsComponentRoot(true); } } else if (element instanceof TextSpanElement) { // Span will be triggering an operation after the text content is parsed TextSpanSelection selection = new TextSpanSelection((TextSpanElement) element, getTextPosition()); mTextSelectionStack.add(selection); } else if (element instanceof TextAElement) { TextHyperlinkSelection selection = new TextHyperlinkSelection((TextAElement) element, getTextPosition()); mTextSelectionStack.add(selection); } else if (element instanceof TextSElement) { // IMPROVABLE: Currently no component, as it will be removed anyway and // would burden removal from automatic path counting mComponentDepth++; List position = updateComponentPosition(); if (mIsCharsBeginning) { mCharsStartPosition = position; mIsCharsBeginning = false; } // No operation triggering as client knows only space characters. We keep the // parsing/mapping to the more performant server TextSElement spaces = (TextSElement) element; mCurrentComponent = mCurrentComponent.createChildComponent(spaces); Integer quantity = spaces.getTextCAttribute(); if (quantity == null) { addText(/*mCachedTableOps, */ "\u0020"); // mCharsForOperation.append('\u0020'); } else { for (int i = 0; i < quantity; i++) { mCharsForOperation.append('\u0020'); } addText(/*mCachedTableOps, */ mCharsForOperation); } } else if (element instanceof TableTableColumnElement) { // Columns can be grouped by and , these // would addChild metadata to the following columns // Column command should be triggered when one of the grouping starts or closes or if the // first row arrives TableTableColumnElement column = (TableTableColumnElement) element; // Adjust Column Count mColumnCount++; int repeatedColumns = 1; if (column.hasAttributeNS(OdfDocumentNamespace.TABLE.getUri(), "number-columns-repeated")) { repeatedColumns = Integer.parseInt( column.getAttributeNS( OdfDocumentNamespace.TABLE.getUri(), "number-columns-repeated")); if (repeatedColumns > 1) { mColumnCount += (repeatedColumns - 1); } } if (mColumns == null) { mColumns = new ArrayList(); } mColumns.add(column); } else if (element instanceof TextListElement) { TextListElement list = (TextListElement) element; // in case it is a new list if (mListStyleStack.isEmpty()) { // Add always a style, so it can be popped of the stack EVERY time a list element ends ParagraphListProperties paragraphListProps = new ParagraphListProperties(); paragraphListProps.setListStart(true); // There are two continuation mechanisms for lists in ODF. // ODF 1.0/1.1 uses @text:continue-numbering using true/false // ODF 1.2 added @text:continue-list using an IDRef to an xml:id of another list. String continuedListId = list.getTextContinueListAttribute(); String listXmlId = list.getXmlIdAttribute(); if (continuedListId != null && !continuedListId.isEmpty()) { paragraphListProps.setListId(newContinuedList(continuedListId, listXmlId).getListId()); } else if (listXmlId != null && !listXmlId.isEmpty()) { paragraphListProps.setListId(newContinuedList(listXmlId).getListId()); } if (listXmlId != null && !listXmlId.isEmpty()) { paragraphListProps.setListXmlId(listXmlId); } else { paragraphListProps.setListXmlId(null); } mListStyleStack.add(paragraphListProps); } else { // Add always a style, so it can be popped of the stack EVERY time a list element ends mListStyleStack.add(new ParagraphListProperties()); } // @text:continue-numbering LATER // @text:continue-list LATER // @xml-id - LATER // @text:style-name is the given list style unless overwritten by a decendent list // Check if the list style was used already in the document, // if not, map the list properties. Check first in auto than in template. // (Due to MSO issue the style might be even in auto in styles.xml - shall I move them back // to content?) if (list.hasAttributeNS(OdfDocumentNamespace.TEXT.getUri(), "style-name")) { String listStyle = list.getAttributeNS(OdfDocumentNamespace.TEXT.getUri(), "style-name"); mListStyleStack.getLast().setListStyleName(listStyle); } } else if (element instanceof TextListItemElement || element instanceof TextListHeaderElement) { ParagraphListProperties paragraphListStyle = mListStyleStack.getLast(); OdfElement listItem = element; if (listItem instanceof TextListHeaderElement) { // list header never show a label paragraphListStyle.showListLabel(false); } else { // As a new list item starts, the next paragraph needs to provide the list label paragraphListStyle.showListLabel(true); } // @text:start-value is provided to the first paragraph only if (listItem.hasAttributeNS(OdfDocumentNamespace.TEXT.getUri(), "start-value")) { mListStartValue = Integer.parseInt( listItem.getAttributeNS(OdfDocumentNamespace.TEXT.getUri(), "start-value")); } // @text:style-override overrides within this list item the list style if (listItem.hasAttributeNS(OdfDocumentNamespace.TEXT.getUri(), "style-override")) { String styleOverride = listItem.getAttributeNS(OdfDocumentNamespace.TEXT.getUri(), "style-override"); if (styleOverride != null && !styleOverride.isEmpty()) { paragraphListStyle.overrideListStyle(styleOverride); } else { paragraphListStyle.overrideListStyle(null); } } else { paragraphListStyle.overrideListStyle(null); } // @xml-id String listXmlId = null; if (listItem instanceof TextListItemElement) { listXmlId = ((TextListItemElement) listItem).getXmlIdAttribute(); } else if (listItem instanceof TextListHeaderElement) { listXmlId = ((TextListHeaderElement) listItem).getXmlIdAttribute(); } if (listXmlId != null && !listXmlId.isEmpty()) { mListStyleStack.getLast().setListItemXmlId(listXmlId); } else { mListStyleStack.getLast().setListItemXmlId(null); } // } else if (element instanceof StyleMasterPageElement) { StyleMasterPageElement masterPage = (StyleMasterPageElement) element; mMasterPageStyleName = masterPage.getStyleNameAttribute(); mPageLayoutName = masterPage.getStylePageLayoutNameAttribute(); footerAttrs = headerAttrs = null; if (mPageLayoutName != null) { OdfStylesDom stylesDom; try { stylesDom = mSchemaDoc.getStylesDom(); OdfOfficeAutomaticStyles autoStyles = stylesDom.getAutomaticStyles(); if (autoStyles != null) { OdfStylePageLayout pageLayout = autoStyles.getPageLayout(mPageLayoutName); if (pageLayout != null) { mPageStyleUsage = pageLayout.getStylePageUsageAttribute(); headerAttrs = getHeaderFooterAttrs( (OdfElement) pageLayout.getChildElement( StyleHeaderStyleElement.ELEMENT_NAME.getUri(), "header-style")); footerAttrs = getHeaderFooterAttrs( (OdfElement) pageLayout.getChildElement( StyleFooterStyleElement.ELEMENT_NAME.getUri(), "footer-style")); } } } catch (IOException ex) { Logger.getLogger(ChangesFileSaxHandler.class.getName()).log(Level.SEVERE, null, ex); } } String nextMasterPageStyle = masterPage.getStyleNextStyleNameAttribute(); if (nextMasterPageStyle != null && !nextMasterPageStyle.isEmpty()) { mHasNextMasterPage = true; } else { mHasNextMasterPage = false; } } else if (Component.isHeaderRoot(uri, localName)) { PageArea pageArea = null; if (localName.equals("header")) { pageArea = HEADER_DEFAULT; } else if (localName.equals("header-left")) { pageArea = HEADER_EVEN; } else { pageArea = HEADER_FIRST; } mContextName = pageArea.getPageAreaName() + CONTEXT_DELIMITER + mMasterPageStyleName; // insert the Header style // {"name":"addHeaderFooter","id":"Standard_header_default","type":"header_default"} mJsonOperationProducer.addHeaderFooter(mContextName, pageArea, headerAttrs); } else if (Component.isFooterRoot(uri, localName)) { // insert the Footer style PageArea pageArea = null; if (localName.equals("footer")) { pageArea = FOOTER_DEFAULT; } else if (localName.equals("footer-left")) { pageArea = FOOTER_EVEN; } else { pageArea = FOOTER_FIRST; } mContextName = pageArea.getPageAreaName() + CONTEXT_DELIMITER + mMasterPageStyleName; mJsonOperationProducer.addHeaderFooter(mContextName, pageArea, footerAttrs); } else if (element instanceof DrawImageElement) { DrawImageElement image = (DrawImageElement) element; ShapeProperties frameProps = (ShapeProperties) mComponentStack.peek(); // ShapeProperties frameProps = mShapePropertiesStack.peekFirst(); int childNo = frameProps.incrementChildNumber(); if (childNo == 1) { Map hardFormatting = new HashMap(); hardFormatting.putAll(frameProps.getShapeHardFormatting()); JSONObject drawingProps = (JSONObject) hardFormatting.get("drawing"); JSONObject imageProps = (JSONObject) hardFormatting.get("image"); if (image.hasAttributeNS(OdfDocumentNamespace.XLINK.getUri(), "href")) { try { String href = image.getAttributeNS(OdfDocumentNamespace.XLINK.getUri(), "href"); imageProps.put("imageUrl", href); // if there is cropping from the frame, we need to do further calculation based on // real graphic size if (imageProps.has("cropRight") && (imageProps.has("height") || imageProps.has("width"))) { JsonOperationProducer.calculateCrops(image, href, imageProps); } } catch (JSONException ex) { Logger.getLogger(ChangesFileSaxHandler.class.getName()).log(Level.SEVERE, null, ex); } } if (image.hasAttributeNS(OdfDocumentNamespace.XML.getUri(), "id")) { try { drawingProps.put( "imageXmlId", image.getAttributeNS(OdfDocumentNamespace.XML.getUri(), "id")); } catch (JSONException ex) { Logger.getLogger(ChangesFileSaxHandler.class.getName()).log(Level.SEVERE, null, ex); } } // ToDo: Need test document with child element having office:binary-data with base64 // content DrawFrameElement frameElement = frameProps.getDrawFrameElement(); frameElement.markAsComponentRoot(true); mComponentStack.pop(); // mShapePropertiesStack.pollFirst(); mComponentStack.push(frameProps); // mShapePropertiesStack.addFirst(frameProps); frameProps.declareImage(); hardFormatting.put("drawing", drawingProps); // }else { // drawingProps.put("viewAlternative", childNo - 1); } // if (!frameProps.hasImageSibling()) { // mComponentDepth++; // frameProps.setFramePosition(updateComponentPosition()); // mCurrentComponent = mCurrentComponent.createChildComponent(frameElement); // // position.set(position.size() - 1, position.get(position.size() - 1) +1); // } // if (childNo == 1) { // DISABLING REPLACEMENT IMAGE FEATURE AS LONG CLIENT DOES NOT // SUPPORT IT // ToDo: Dependencies for frame replacement feature has to be updated in // OdfElement.raiseComponentSize() for every Frame child/feature enabled // frameProps.saveShapeProps(frameProps.getShapePosition(), hardFormatting); // } } else if (element instanceof DrawTextBoxElement) { // element.getAttributeNodeNS(namespaceURI, localName); // Map hardFormatting = null; // if (element instanceof OdfStyleableShapeElement) { // hardFormatting = // mJsonOperationProducer.getHardStyles((OdfStyleableShapeElement) element); // } // JSONObject drawingProps = (JSONObject) hardFormatting.get("drawing"); // JSONObject drawingProps = new JSONObject(); if (!mComponentStack.empty()) { ShapeProperties parentShapeProps = (ShapeProperties) mComponentStack.peek(); JSONObject originalDrawingProps = (JSONObject) parentShapeProps.mShapeHardFormatations.get("drawing"); if (originalDrawingProps != null && !originalDrawingProps.has("height")) { try { if (!parentShapeProps.mShapeHardFormatations.containsKey("shape")) { parentShapeProps.mShapeHardFormatations.put("shape", new JSONObject()); } JSONObject originalShapeProps = (JSONObject) parentShapeProps.mShapeHardFormatations.get("shape"); originalShapeProps.put("autoResizeHeight", "true"); } catch (JSONException ex) { Logger.getLogger(ChangesFileSaxHandler.class.getName()).log(Level.SEVERE, null, ex); } } } } // but within a shape being a frame, the child elements are still of interest (currently only // supported) // } else if (!mShapePropertiesStack.isEmpty() && // mShapePropertiesStack.peekLast().mDrawFrameElement != null) { if (Component.isDocumentRoot(uri, localName) || Component.isHeaderRoot(uri, localName) || Component.isFooterRoot(uri, localName)) { // temporary initated here as all the tests are not using the OperationTextDocument mCurrentComponent = new Component(element); mSchemaDoc.setRootComponent(mCurrentComponent); // for every header and footer restart counting if (Component.isHeaderRoot(uri, localName) || Component.isFooterRoot(uri, localName)) { mLastComponentPositions.clear(); } else { mPageArea = PageArea.BODY; } } } else { if (element instanceof OdfElement) { element.ignoredComponent(true); } } // add the new element as child & make it the current context node mCurrentNode = mCurrentNode.appendChild(element); } @Override public void endElement(String uri, String localName, String qName) throws SAXException { flushTextAtEnd(uri, localName, qName); // at the end of a table check if it can be flushed if (uri != null && localName != null && localName.equals(TableTableElement.ELEMENT_NAME.getLocalName()) && uri.equals(OdfDocumentNamespace.TABLE.getUri())) { endTableSizeEvaluation(); } // office:styles only exist in styles.xml if (qName.equals("office:styles")) { if (mFileDom instanceof OdfStylesDom) { Integer defaultTabStopWidth = null; JSONObject defaultPageStyles = null; OdfStylesDom stylesDom = (OdfStylesDom) mFileDom; // reset the position context used for header/footer mContextName = null; OdfOfficeStyles officeStyles = (OdfOfficeStyles) mCurrentNode; if (officeStyles != null) { // check if the default hyperlinkstyle do exist mHasHyperlinkTemplateStyle = officeStyles.getStyle(HYERLINK_DEFAULT_STYLE, OdfStyleFamily.Text) != null; final Iterator paragraphStyleIter = officeStyles.getStylesForFamily(OdfStyleFamily.Paragraph).iterator(); // The sort is for testing purpose to receive across different JDK an equal result Integer _defaultTabStopWidth = null; while (paragraphStyleIter.hasNext()) { // defaulTableWidth is part of the paragraph default style (optional) _defaultTabStopWidth = mJsonOperationProducer.triggerStyleHierarchyOps( officeStyles, OdfStyleFamily.Paragraph, paragraphStyleIter.next()); if (_defaultTabStopWidth != null) { defaultTabStopWidth = _defaultTabStopWidth; } } final Iterator textStyleIter = officeStyles.getStylesForFamily(OdfStyleFamily.Text).iterator(); while (textStyleIter.hasNext()) { mJsonOperationProducer.triggerStyleHierarchyOps( officeStyles, OdfStyleFamily.Text, textStyleIter.next()); } final Iterator graphicStyleIter = officeStyles.getStylesForFamily(OdfStyleFamily.Graphic).iterator(); while (graphicStyleIter.hasNext()) { mJsonOperationProducer.triggerStyleHierarchyOps( officeStyles, OdfStyleFamily.Graphic, graphicStyleIter.next()); } // always generate graphic default style mJsonOperationProducer.triggerDefaultStyleOp( OdfStyleFamily.Graphic, officeStyles.getDefaultStyle(OdfStyleFamily.Graphic)); // for(OdfStyle style : officeStyles.getStylesForFamily(OdfStyleFamily.Table)){ // mJsonOperationProducer.triggerStyleHierarchyOps(officeStyles, // OdfStyleFamily.Table, style); // } // for(OdfStyle style : officeStyles.getStylesForFamily(OdfStyleFamily.TableRow)){ // mJsonOperationProducer.triggerStyleHierarchyOps(officeStyles, // OdfStyleFamily.TableRow, style); // } // for(OdfStyle style : // officeStyles.getStylesForFamily(OdfStyleFamily.TableColumn)){ // mJsonOperationProducer.triggerStyleHierarchyOps(officeStyles, // OdfStyleFamily.TableColumn, style); // } for (OdfStyle style : officeStyles.getStylesForFamily(OdfStyleFamily.TableCell)) { mJsonOperationProducer.triggerStyleHierarchyOps( officeStyles, OdfStyleFamily.TableCell, style); // mJsonOperationProducer.triggerStyleHierarchyOps(officeStyles, // OdfStyleFamily.TableCell, (OdfStyleBase) // officeStyles.getDefaultStyle(OdfStyleFamily.TableCell)); } // for(OdfStyle style : officeStyles.getStylesForFamily(OdfStyleFamily.Section)){ // mJsonOperationProducer.triggerStyleHierarchyOps(officeStyles, // OdfStyleFamily.Section, style); // } // for(OdfStyle style : officeStyles.getStylesForFamily(OdfStyleFamily.List)){ // mJsonOperationProducer.triggerStyleHierarchyOps(officeStyles, // OdfStyleFamily.List, style); // } final Iterator textListStyleIter = officeStyles.getListStyles().iterator(); while (textListStyleIter.hasNext()) { mJsonOperationProducer.addListStyle(textListStyleIter.next()); } // maps page properties, but returns the default page properties defaultPageStyles = mJsonOperationProducer.addPageProperties(stylesDom); // dispatches default document attributes mJsonOperationProducer.addDocumentProperties( stylesDom, defaultTabStopWidth, defaultPageStyles); } else { mJsonOperationProducer.addDocumentProperties(stylesDom, null, null); } } } // if we remove the current element, the current node shall not be changed in the end boolean selectionNormalization = false; // SPECIAL HANDLING FOR DESCRIPTION OF SHAPES: draw:frame as the shape is one of the children, // e.g a draw:image child if (!checkEndOfBlockedSubTree( uri, localName) /*&& !(mContextName != null && localName.equals("annotation-end"))*/) { boolean isImageComponent = false; if ((uri != null && uri.equals(DrawFrameElement.ELEMENT_NAME.getUri()) && localName.equals(DrawFrameElement.ELEMENT_NAME.getLocalName()) || Component.isShapeElement(uri, localName))) { if (!mComponentStack.empty()) { ShapeProperties shapeProps = (ShapeProperties) mComponentStack.pop(); mComponentDepth--; // Check for description of shape/frame about to be closed // ShapeProperties shapeProps = mShapePropertiesStack.removeLast(); isImageComponent = shapeProps.hasImageSibling(); NodeList descList = mCurrentComponent.mRootElement.getElementsByTagNameNS( OdfDocumentNamespace.SVG.getUri(), SvgDescElement.ELEMENT_NAME.getLocalName()); String description = null; if (descList.getLength() > 0) { SvgDescElement desc = (SvgDescElement) descList.item(0); Node descText = desc.getFirstChild(); if (descText != null && descText instanceof Text) { description = ((Text) descText).getTextContent(); } } // it is root shape if the parent office:text shapeProps.createShapeOperation( this, mComponentStack, description, isImageComponent ? ShapeType.ImageShape : shapeProps.isGroupShape() ? ShapeType.GroupShape : ShapeType.NormalShape, mContextName); if (shapeProps.isGroupShape()) { mCurrentNode.setUserData( "groupWidth", (shapeProps.mHoriOffsetMax - (shapeProps.mHoriOffsetMin == null ? 0 : shapeProps.mHoriOffsetMin)), null); mCurrentNode.setUserData( "groupHeight", (shapeProps.mVertOffsetMax - (shapeProps.mVertOffsetMin == null ? 0 : shapeProps.mVertOffsetMin)), null); } // flush the inner operations of the shape Iterator opIter = shapeProps.iterator(); while (opIter.hasNext()) { CachedOperation op = opIter.next(); cacheOperation( true, op.mComponentType, op.mStart, false, op.mHardFormattingProperties, op.mComponentProperties); } mCurrentComponent = mCurrentComponent.getParent(); } // } else if (Component.isCoveredComponentRoot(uri, localName)) { // adjust counting for // table cells without numbering // //ToDO: Instead to count the covered someone should count the spanning (BUT this is // against OOXML cell numbering!) // mComponentDepth--; } else if (isSpaceElement(uri, localName)) { mComponentDepth--; mCurrentComponent = mCurrentComponent.getParent(); // } else if (uri != null && uri.equals(DrawImageElement.ELEMENT_NAME.getUri()) && // localName.equals(DrawImageElement.ELEMENT_NAME.getLocalName())) { // mFramePropertiesStack.getFirst().decrementChildNumber(); } else if (uri != null && Component.isComponentRoot(uri, localName)) { // if (Component.isTextComponentRoot(uri, localName) && // mWhitespaceStatusStack.size() > 0 && // mWhitespaceStatusStack.getLast().mIsParagraphIgnored) { /* no ignored paragraphs anymore if (Component.isTextComponentRoot(uri, localName) && mWhitespaceStatusStack.size() > 0 && mWhitespaceStatusStack.getLast().mIsParagraphIgnored) {) { // do nothing for a ignored paragraph mWhitespaceStatusStack.removeLast(); // SPECIAL HANDLING FOR IMAGE: draw:image (not a component root T- replacement for draw:frame) } else */ if (localName.equals(DrawFrameElement.ELEMENT_NAME.getLocalName()) && uri.equals(DrawFrameElement.ELEMENT_NAME.getUri()) && isImageComponent || !(localName.equals(DrawFrameElement.ELEMENT_NAME.getLocalName()) && uri.equals(DrawFrameElement.ELEMENT_NAME.getUri()))) { // if the current component is a text container flush spans if (Component.isTextComponentRoot(mCurrentNode)) { Collection selections = ((TextParagraphElementBase) mCurrentNode).getTextSelections(); if (selections != null) { for (TextSelection s : selections) { OdfStylableElement selectionElement = (OdfStylableElement) s.getSelectionElement(); Map hardFormatting = mJsonOperationProducer.getHardStyles(selectionElement); String styleId = null; OdfStyle templateStyle = selectionElement.getDocumentStyle(); if (templateStyle != null) { styleId = templateStyle.getStyleNameAttribute(); } if (s.hasUrl() || styleId != null) { try { JSONObject charProps; if (hardFormatting == null) { // if there are absolute styles, but not the main property set, where the // templateStyleId should be placed in if (hardFormatting == null) { hardFormatting = new HashMap(); } } if (s.hasUrl()) { if (!hardFormatting.containsKey("character")) { charProps = new JSONObject(); hardFormatting.put("character", charProps); } else { charProps = (JSONObject) hardFormatting.get("character"); } charProps.put("url", s.getURL()); } if (styleId != null && !styleId.isEmpty()) { hardFormatting.put(OPK_STYLE_ID, styleId); } else { // add the implicit by LO/AOO used hyperlink style if (mHasHyperlinkTemplateStyle) { hardFormatting.put(OPK_STYLE_ID, HYERLINK_DEFAULT_STYLE); } } } catch (JSONException ex) { Logger.getLogger(ChangesFileSaxHandler.class.getName()) .log(Level.SEVERE, null, ex); } } if (hardFormatting != null) { // if (mWithinTable) { cacheOperation( false, OperationConstants.ATTRIBUTES, s.getStartPosition(), false, hardFormatting, s.getEndPosition(), mContextName); } } } // // in this case check if the closing descendent (this element) // // had any none whitespace text and apply if necessary the change // // remove the current whitespace properties from the stack // int depth = mWhitespaceStatusStack.size(); // boolean childHasWhiteSpace = true; // boolean parentHasOnlyWhiteSpace; // if there is a parent text container if (mWhitespaceStatusStack.size() > 0) { mWhitespaceStatusStack.removeLast(); // BEFORE WE DID NOT ALLOWED TO FOLLOWING PARAGRAPHS // // see if the child only had whitespaces // childHasWhiteSpace = mWhitespaceStatusStack.getLast().hasOnlyWhiteSpace(); // // switch to parent // mWhitespaceStatusStack.pop(); // // see if the parent had only whitespaces // WhitespaceStatus parentWhiteSpaceStatus = mWhitespaceStatusStack.getLast(); // parentHasOnlyWhiteSpace = parentWhiteSpaceStatus.hasOnlyWhiteSpace(); // // if the parent had only whitespaces, but not the child // if (parentHasOnlyWhiteSpace && !childHasWhiteSpace) { // // remove the only whitespace modus from the parent // parentWhiteSpaceStatus.setOnlyWhiteSpace(childHasWhiteSpace); // } // } else { // // otherwise just end the state collection of this paragraph/heading // mWhitespaceStatusStack.pop(); } } // removing the last in the list of positions, when a component is closed if (localName.equals("annotation")) { CommentComponent commProps = (CommentComponent) mComponentStack.pop(); if (!commProps.isInHeaderFooter()) { String id = COMMENT_PREFIX; id += commProps.getCommentName(); cacheOperation( false, OperationConstants.COMMENT, commProps.getComponentPosition(), false, null, id, commProps.getAuthor(), commProps.getDate(), mContextName); int parentPosSize = commProps.getComponentPosition().size(); for (CachedOperation op : commProps) { // TODO: add id as target, remove comments own position from op.mStart; CachedOperation newOp = op.clone(); for (int r = 0; r < parentPosSize; ++r) { newOp.mStart.remove(0); } ArrayList componentProperties = new ArrayList(); int propIndex = 0; while (newOp.mComponentProperties.length > propIndex && newOp.mComponentProperties[propIndex] != null) { if (propIndex == 0 && newOp.mComponentType.equals(OperationConstants.ATTRIBUTES)) { @SuppressWarnings("unchecked") List endArray = (List) newOp.mComponentProperties[propIndex]; for (int r = 0; r < parentPosSize; ++r) { endArray.remove(0); } componentProperties.add(endArray); } else if (mContextName == null || !newOp.mComponentProperties[propIndex].equals(mContextName)) { componentProperties.add(newOp.mComponentProperties[propIndex]); } ++propIndex; } componentProperties.add(id); componentProperties.add(null); cacheOperation( false, newOp.mComponentType, newOp.mStart, true, newOp.mHardFormattingProperties, componentProperties.toArray()); } } // } else if (localName.equals("annotation-end")){ //no action // required } if (mCurrentComponent.hasRepeated()) { // if it is a cell or row not in spreadsheets all operations back from addCell/addRows // need to be repeated with incremented positions (TODO: needs to be recursive! ) boolean isCell = localName.equals("table-cell"); boolean isRow = localName.equals("table-row"); if (isRow || isCell) { CachedTable currentTable = (CachedTable) mComponentStack.peek(); int opSize = currentTable.size(); int pos = 0; for (pos = opSize - 1; pos >= 0; --pos) { CachedOperation op = currentTable.get(pos); if (op.mComponentType.equals( isRow ? OperationConstants.ROWS : OperationConstants.CELLS)) { break; } } // now we have a start index - add all ops from pos to opSize- 1 again // repetition-times with modified position if (pos > 0) { CachedOperation cellInsertOp = currentTable.get(pos); int incrementPos = cellInsertOp.mStart.size() - 1; int repetition = (Integer) cellInsertOp.mComponentProperties[isRow ? 0 : 1]; for (int rep = 0; rep < repetition - 1; ++rep) { for (int opPos = pos; opPos < opSize; ++opPos) { CachedOperation newOp = currentTable.get(opPos).clone(); if (newOp.mStart != null) { int oldIndex = newOp.mStart.get(incrementPos); newOp.mStart.set(incrementPos, oldIndex + rep + 1); } cacheOperation( false, newOp.mComponentType, newOp.mStart, false, newOp.mHardFormattingProperties, newOp.mComponentProperties); } } } } mLastComponentPositions.set( mComponentDepth, mLastComponentPositions.get(mComponentDepth) + mCurrentComponent.repetition() - 1); } mComponentDepth--; mCurrentComponent = mCurrentComponent.getParent(); } } // if text delimiter - addChild text else if (Component.isTextSelection(mCurrentNode)) { // dropping the last (most inner) selection from the stack TextSelection textSelection = mTextSelectionStack.pollLast(); if (textSelection != null) { textSelection.setEndPosition(getTextPosition()); OdfElement root = ((OdfElement) mCurrentNode).getComponentRoot(); if (Component.isTextComponentRoot(root)) { // sometimes when spans share the same text area, they are condensed to one. The // remaining one is being returned, or in case of an empty element the parent mCurrentNode = ((TextContainingElement) root).appendTextSelection(textSelection); // selectionNormalization might delete an element in this case the change of the current // node would be an error! selectionNormalization = true; } } } else if (uri.equals(OdfDocumentNamespace.TEXT.getUri()) && localName.equals("list-item")) { mListStyleStack.getLast().overrideListStyle(null); } else if (uri.equals(OdfDocumentNamespace.TEXT.getUri()) && localName.equals("list")) { // POP UP NEW LIST STYLE mListStyleStack.removeLast(); } else if (localName.equals("creator")) { CachedComponent commProps = mComponentStack.isEmpty() ? null : mComponentStack.peek(); if (commProps != null && commProps instanceof CommentComponent) { ((CommentComponent) commProps).setAuthor(mCurrentNode.getTextContent()); } } else if (localName.equals("date")) { CachedComponent commProps = mComponentStack.isEmpty() ? null : mComponentStack.peek(); if (commProps != null && commProps instanceof CommentComponent) { ((CommentComponent) commProps).setDate(mCurrentNode.getTextContent()); } } } // selectionNormalization might delete an element in this case the change of the current node // would be an error! if (mCurrentNode != null && !selectionNormalization) { // pop to the parent node mCurrentNode = mCurrentNode.getParentNode(); } } private void addAttributes(Element element, Attributes attributes) { String attrQname; String attrURL; OdfAttribute attr; for (int i = 0; i < attributes.getLength(); i++) { attrURL = attributes.getURI(i); attrQname = attributes.getQName(i); // if no namespace exists if (attrURL.equals(Constants.EMPTY_STRING) || attrQname.equals(Constants.EMPTY_STRING)) { // create attribute without prefix attr = mFileDom.createAttribute(attributes.getLocalName(i)); } else { if (attrQname.startsWith("xmlns:")) { // in case of xmlns prefix we have to create a new OdfNamespace OdfNamespace namespace = mFileDom.setNamespace(attributes.getLocalName(i), attributes.getValue(i)); // if the file Dom is already associated to parsed XML addChild the new namespace to the // root element Element root = mFileDom.getRootElement(); if (root == null) { root = element; } root.setAttributeNS( "http://www.w3.org/2000/xmlns/", "xmlns:" + namespace.getPrefix(), namespace.getUri()); } // create all attributes, even namespace attributes attr = mFileDom.createAttributeNS(attrURL, attrQname); } // namespace attributes will not be created and return null if (attr != null) { element.setAttributeNodeNS(attr); try { // set Value in the attribute to allow validation in the attribute attr.setValue(attributes.getValue(i)); } // if we detect an attribute with invalid value: remove attribute node catch (IllegalArgumentException e) { ErrorHandler errorHandler = mFileDom.getDocument().getPackage().getErrorHandler(); if (errorHandler != null) { try { errorHandler.error( new OdfValidationException( OdfSchemaConstraint.DOCUMENT_XML_INVALID_ATTRIBUTE_VALUE, attr.getValue(), attr.getPrefix() + ":" + attr.getLocalName())); } catch (SAXException ex) { Logger.getLogger(StyleStyleElement.class.getName()).log(Level.SEVERE, null, ex); } } else { LOG.severe( "ERROR / EXCEPTION DURING XML PARSING: INVALID ATTRIBUTE: '" + attr.getPrefix() + ":" + attr.getLocalName() + "' with value '" + attr.getValue() + "'!"); } element.removeAttributeNode(attr); } } } } private void flushTextAtStart(String uri, String localName, String qName) { flushText(uri, localName, qName, false); } private void flushTextAtEnd(String uri, String localName, String qName) { flushText(uri, localName, qName, true); } /** * Consumers shall collapse white space characters that occur in * *
    *
  • a or element (so called paragraph elements), and *
  • in their descendant elements, if the OpenDocument schema permits the inclusion of * character data for the element itself and all its ancestor elements up to the paragraph * element. *
* * Collapsing white space characters is defined by the following algorithm: 1)The following * [UNICODE] characters are replaced by a " " (U+0020, SPACE) character: \ue570HORIZONTAL * TABULATION (U+0009) \ue570CARRIAGE RETURN (U+000D) \ue570LINE FEED (U+000A) 2)The character * data of the paragraph element and of all descendant elements for which the OpenDocument schema * permits the inclusion of character data for the element itself and all its ancestor elements up * to the paragraph element, is concatenated in document order. 3)Leading " " (U+0020, SPACE) * characters at the start of the resulting text and trailing SPACE characters at the end of the * resulting text are removed. 4)Sequences of " " (U+0020, SPACE) characters are replaced by a * single " " (U+0020, SPACE) character. */ private void flushText(String uri, String localName, String qName, boolean isEndOfElement) { // check if there is was text found to be added to the element if (mCharsForElement.length() > 0) { // every text will be kept from the XML file (e.g. indent) String newString = mCharsForElement.toString(); mCharsForElement.setLength(0); Text text = mFileDom.createTextNode(newString); if (isEndOfElement && Component.isField(uri, localName)) { TextSelection textSelection = mTextSelectionStack.pollLast(); if (!isBlockedSubTree()) { // Currently only check-box have an UTF-8 square as replacementText TextFieldSelection textFieldSelection = (TextFieldSelection) textSelection; String replacementText = textFieldSelection.getReplacementText(); Map attrMap = textFieldSelection.getAttributes(); cacheOperation( false, OperationConstants.FIELD, textSelection.getStartPosition(), false, null, localName, replacementText != null ? replacementText : newString, attrMap, mContextName); } mCurrentNode.appendChild(text); } else { // if the text is within a text aware component if (mCurrentNode instanceof OdfElement) { // ToDo: Uncertain what with text should happen that is not within a text component? // Neglectable by // if ((Component.isTextComponentRoot(mCurrentNode) || // Component.isTextComponentRoot(((OdfElement) mCurrentNode).getComponentRoot())) && // !isBlockedSubTree() && mWhitespaceStatusStack.size() > 0 && // !mWhitespaceStatusStack.getLast().isParagraphIgnored() && !(mCurrentNode instanceof // TextNoteCitationElement)) { if ((Component.isTextComponentRoot(mCurrentNode) || Component.isTextComponentRoot(((OdfElement) mCurrentNode).getComponentRoot())) && !isBlockedSubTree() && mWhitespaceStatusStack.size() > 0 && !(mCurrentNode instanceof TextNoteCitationElement)) { mComponentDepth++; if (mIsCharsBeginning) { mCharsStartPosition = updateTextPosition(); mIsCharsBeginning = false; } mComponentDepth--; // The new charPosition adds the text lenght, but inserted will be without // 1) insertion addText(/*mCachedTableOps, */ newString); // the following would cumulate the text of a paragraph to a single large string // mCharsForOperation.append(newString); // muss ich rekursiv die gr\u00f6sse nach oben reichen? f\u00fcr jeden none component // descendant? K\u00f6nnte ich in OdfElement implemenentieren! // \u00fcberschreibe alle addChild/delete Funktionalit\u00e4t! // Merge/split/delete Text Funktionalit\u00e4t f\u00fcr alle ELEMENTE? Komponenten // m\u00fcssen mit einbezogen werden! Reuse of Recursion -- ACTION KLASSE.operate() // aufruf!?!? // OdfElement element = (OdfElement) mCurrentNode; // element.appendChild(text); } if (isSpaceElement(mCurrentNode)) { mCurrentNode.getParentNode().appendChild(text); } else { mCurrentNode.appendChild(text); } } } } else { if (isEndOfElement && Component.isField(uri, localName)) { TextSelection textSelection = mTextSelectionStack.pollLast(); if (!isBlockedSubTree()) { // Currently only check-box have an UTF-8 square as replacementText TextFieldSelection textFieldSelection = (TextFieldSelection) textSelection; String replacementText = textFieldSelection.getReplacementText(); Map attrMap = textFieldSelection.getAttributes(); if (replacementText == null) { replacementText = new String(); } cacheOperation( false, OperationConstants.FIELD, textSelection.getStartPosition(), false, null, localName, replacementText, attrMap, mContextName); } } } if (mCharsForOperation.length() > 0 && Component.isComponentRoot(uri, localName) && !isSpaceElement(uri, localName)) { addText(/*mCachedTableOps, */ mCharsForOperation); } } private void addText(CharSequence newText) { cacheOperation( false, OperationConstants.TEXT, mCharsStartPosition, false, null, newText.toString(), mContextName); mCharsForOperation.setLength(0); mIsCharsBeginning = true; } static boolean isSpaceElement(Node node) { return node instanceof TextSElement; } static boolean isSpaceElement(String uri, String localName) { return uri != null && uri.equals(TextSElement.ELEMENT_NAME.getUri()) && localName.equals(TextSElement.ELEMENT_NAME.getLocalName()); } @Override /** * http://xerces.apache.org/xerces2-j/faq-sax.html#faq-2 : SAX may deliver contiguous text as * multiple calls to characters, for reasons having to do with parser efficiency and input * buffering. It is the programmer's responsibility to deal with that appropriately, e.g. by * accumulating text until the next non-characters event. This method will finalize the text of an * element, by flushing/appending it to the element node. It is called at the beginning of * startElement/endElement. In case of startElement the text will be referred to the previous * element node (before the new started). In case of endElement the text will be referred to the * current element node. */ public void characters(char[] ch, int startPosition, int length) { if (mCurrentComponent instanceof TextContainer) { // ODF Whitespacehandling WhitespaceStatus currentWhiteSpaceStatus = mWhitespaceStatusStack.getLast(); // Note: The delta between startPosition and endPosition marks the text to be written out // startPosition will only be raised to endposition, when characters have to be skipped! int endPosition = startPosition; int lastPos = startPosition + length; boolean previousContentWritten = false; char c; // Go through all characters found by the parser.. for (int i = startPosition; i < lastPos; i++) { c = ch[i]; // first part is trimming in the beginning of the element if (currentWhiteSpaceStatus.hasOnlyWhiteSpace()) { // \t (tabulator = 0x09) if (c == '\u0020' // space || c == '\t' // \r (carriage return = 0x0D) || c == '\r' // \n (line feed = 0x0A) || c == '\n') { // skipt this character, keeping the difference between start & end (length) equal startPosition++; endPosition++; } else { // first character being found worth to be written currentWhiteSpaceStatus.setOnlyWhiteSpace(false); endPosition++; } // second part is about collapsing multiple whitespaces } else { if (c == '\u0020' // space || c == '\t' // \t (tabulator = 0x09) // \r (carriage return = 0x0D) || c == '\r' // \n (line feed = 0x0A) || c == '\n') { // if we have aleady a preceding whitespace character if (currentWhiteSpaceStatus.hasSpaceBefore()) { if (!previousContentWritten) { // as we have to skip a character in the array, write what we have if (endPosition - startPosition > 0) { mCharsForElement.append(ch, startPosition, endPosition - startPosition); } previousContentWritten = true; } // NOT including this character endPosition++; startPosition = endPosition; } else { currentWhiteSpaceStatus.setFirstSpaceCharPosition(i); ch[i] = '\u0020'; // overwrite all endPosition++; } } else { if (currentWhiteSpaceStatus.hasSpaceBefore()) { currentWhiteSpaceStatus.setFirstSpaceCharPosition(-1); } endPosition++; // including this character } } } if (endPosition - startPosition > 0) { mCharsForElement.append(ch, startPosition, endPosition - startPosition); } } else { /* * ToDo: The following will be ignored for now: In addition, OpenDocument Consumers shall ignore all element children ([RNG] section 5, Data Model) of elements defined in this specification that are strings consisting entirely of whitespace characters and which do not satisfy a pattern of the OpenDocument schema definition for the element. */ // See // http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#a3_18White_Space_Processing_and_EOL_Handling mCharsForElement.append(ch, startPosition, length); } } @Override public InputSource resolveEntity(String publicId, String systemId) throws IOException, SAXException { return super.resolveEntity(publicId, systemId); } /** * This method is being called whenever an element being the root element is found, within * startElement function. It considers mComponentDepth and the size of the list * mLastComponentPositions. It updates the position (ie. list mLastComponentPositions) by adding a * child or a sibling. This ONLY works if mComponentDepth has already been updated before this * method is being called. Currently at the beginning of startElement, when realizing it is a * component root element. * *

The only reason to update the position (mLastComponentPositions) outside of this function is * to handle attributes representing repeatedColumns components * (i.e. for cells & rows). * * @return the path of the current found component as Integer array */ private List updateComponentPosition() { List pos = updatePosition(true); return pos; } /** * This method is being called whenever an element being the root element is found, within * startElement function. It considers mComponentDepth and the size of the list * mLastComponentPositions. It updates the position (ie. list mLastComponentPositions) by adding a * child or a sibling. This ONLY works if mComponentDepth has already been updated before this * method is being called. Currently at the beginning of startElement, when realizing it is a * component root element. * *

The only reason to update the position (mLastComponentPositions) outside of this function is * to handle attributes representing repeatedColumns components * (i.e. for cells & rows). * * @return the path of the current found component as Integer array */ private List updateTextPosition() { return updatePosition(false); } /** * This method is being called whenever an element being the root element is found, within * startElement function. It considers mComponentDepth and the size of the list * mLastComponentPositions. It updates the position (ie. list mLastComponentPositions) by adding a * child or a sibling. This ONLY works if mComponentDepth has already been updated before this * method is being called. Currently at the beginning of startElement, when realizing it is a * component root element. * *

The only reason to update the position (mLastComponentPositions) outside of this function is * to handle attributes representing repeatedColumns components * (i.e. for cells & rows). * * @return the path of the current found component as Integer array */ private List updatePosition(boolean isComponent) { /** * Used components being siblings of text, where the position is determined by the text count, * not by the previous component number */ // take care of position handling // NEW COMPONENT CHILD: If the component Level is deeper than the last position // Actually at the first component: componentDepth start with 0, but the size with 1 if (mComponentDepth == mLastComponentPositions.size()) { // addChild a new level mLastComponentPositions.add(mComponentDepth, 0); // NEW SIBLING: If the depth is "equal" (ie. lower) than addChild a sibling } else if (mComponentDepth == mLastComponentPositions.size() - 1) { int positionUpdate; if (mCurrentComponent instanceof TextContainer) { // increment the last position number as a sibling was added positionUpdate = mCurrentComponent.size(); } else if (mCurrentComponent instanceof Cell) { positionUpdate = mLastComponentPositions.get(mComponentDepth) + 1; // } else if (mCurrentComponent instanceof Table) { // positionUpdate =mCurrentComponent.size(); } else if (mCurrentComponent.getRootElement() instanceof OfficeAnnotationEndElement) { positionUpdate = mCurrentComponent.getParent().size(); } else { positionUpdate = mLastComponentPositions.get(mComponentDepth) + 1; } mLastComponentPositions.set(mComponentDepth, positionUpdate); // FINISHED COMPONENT - REMOVE DEPTH - If a component was closed and the position needds to be // added } else if (mComponentDepth < mLastComponentPositions.size() - 1) { // remove the last position and addChild a new one mLastComponentPositions.removeLast(); updatePosition(isComponent); } else { LOG.warning("Houston, we have a problem.."); } // ToDo: Do I need a new LIST for every component? Or may I addChild a position to the // component? return new LinkedList(mLastComponentPositions); // ToDo: Below a bad idea? // return Collections.unmodifiableList(mLastComponentPositions); } /** @return the path of the current found component as Integer array */ private List getTextPosition() { // The text is one level further down mComponentDepth++; List position = updateTextPosition(); mComponentDepth--; return position; } // CachedTable mCachedTableOps = null; // based on document position the tables and subtables are being flushed after each end // HashMap, CachedTable> mAllTables = null; // private ArrayList mTableStack; // boolean mWithinTable = false; //TODO: Detect via type of top element of mComponentStack // private int mNumberOfNestedTables = 0; Stack mComponentStack = new Stack(); // cache objects bound to page that are on top level of the document ArrayDeque m_cachedPageShapes = new ArrayDeque(); boolean mPageBoundObjectsRelocated = false; HashMap mTopLevelTables = new HashMap(); // mapping of spreadsheet index to their names @SuppressWarnings("unchecked") public void cacheOperation( boolean fillCacheOnly, String componentType, List start, boolean absolutePosition, Map hardFormattingProperties, Object... componentProperties) { if (mComponentStack.empty()) { // send to producer if (componentType.equals(OperationConstants.TEXT)) { String text = (String) componentProperties[0]; String context = (String) componentProperties[1]; mJsonOperationProducer.addText(start, text, context); } else if (componentType.equals(OperationConstants.PARAGRAPH)) { String context = (String) componentProperties[0]; mJsonOperationProducer.add(componentType, start, hardFormattingProperties, context); } else if (componentType.equals(OperationConstants.TABLE)) { List tableGrid = (List) componentProperties[0]; String tableName = (String) componentProperties[1]; String context = (String) componentProperties[2]; mJsonOperationProducer.addTable( start, hardFormattingProperties, tableGrid, tableName, context); } else if (componentType.equals(OperationConstants.EXCEEDEDTABLE)) { int columns = (Integer) componentProperties[0]; int rows = (Integer) componentProperties[1]; List tableGrid = (List) componentProperties[2]; String context = (String) componentProperties[3]; // addExceededTable(final List start, int columns, int rows, final List // tableGrid) { mJsonOperationProducer.addExceededTable(start, columns, rows, tableGrid, context); } else if (componentType.equals(OperationConstants.ATTRIBUTES)) { List end = (List) componentProperties[0]; String context = (String) componentProperties[1]; mJsonOperationProducer.format(start, end, hardFormattingProperties, context); } else if (componentType.equals(OperationConstants.FORMATROWS)) { Integer firstRow = (Integer) componentProperties[0]; Integer lastRow = (Integer) componentProperties[1]; String context = (String) componentProperties[3]; Integer repeatedRowOffset = (Integer) componentProperties[2]; mJsonOperationProducer.formatRows( start, hardFormattingProperties, firstRow, lastRow, repeatedRowOffset, context); } else if (componentType.equals(OperationConstants.FORMATCOLUMNS)) { Integer firstColumn = (Integer) componentProperties[0]; Integer lastColumn = (Integer) componentProperties[1]; String context = (String) componentProperties[2]; mJsonOperationProducer.formatColumns( start, hardFormattingProperties, firstColumn, lastColumn, context); } else if (componentType.equals(OperationConstants.SHAPE) || componentType.equals(OperationConstants.SHAPE_GROUP)) { String context = (String) componentProperties[0]; mJsonOperationProducer.addShape( start, hardFormattingProperties, context, componentType.equals(OperationConstants.SHAPE_GROUP)); } else if (componentType.equals(OperationConstants.IMAGE)) { String context = (String) componentProperties[0]; mJsonOperationProducer.addImage(start, hardFormattingProperties, context); } else if (componentType.equals(OperationConstants.FIELD)) { String fieldType = (String) componentProperties[0]; String fieldContent = (String) componentProperties[1]; Map fieldAttributes = (Map) componentProperties[2]; String context = (String) componentProperties[3]; mJsonOperationProducer.addField(start, fieldType, fieldContent, fieldAttributes, context); } else if (componentType.equals(OperationConstants.COMMENT)) { String id = (String) componentProperties[0]; String author = (String) componentProperties[1]; String date = (String) componentProperties[2]; String target = (String) componentProperties[3]; mJsonOperationProducer.addAnnotation(start, id, author, date, target); } else if (componentType.equals(OperationConstants.COMMENTRANGE)) { String id = (String) componentProperties[0]; String target = (String) componentProperties[1]; mJsonOperationProducer.addRange(start, id, target); } else if (componentType.equals(OperationConstants.TAB)) { String target = (String) componentProperties[0]; mJsonOperationProducer.add( componentType, start, hardFormattingProperties, target != null ? target : mContextName); } else { String target = (String) componentProperties[0]; mJsonOperationProducer.add( componentType, start, hardFormattingProperties, target != null ? target : mContextName); } } else { CachedComponent topComponent = mComponentStack.peek(); if (!fillCacheOnly && topComponent instanceof CachedTable) { cacheTableOperation(componentType, start, hardFormattingProperties, componentProperties); } else { // collect the operations at the CachedComponent LinkedList position = null; if (start != null) { position = new LinkedList<>(start); } topComponent.add( new CachedOperation( componentType, position, absolutePosition, hardFormattingProperties, componentProperties)); } } } /** Table operation are being cached in one of two caches */ @SuppressWarnings("unchecked") private void /*CachedTable */ cacheTableOperation( /*CachedTable currentTable,*/ String componentType, List start, Map hardFormattingProperties, Object... componentProperties) { LinkedList position = null; CachedTable currentTable = mComponentStack.empty() ? null : (mComponentStack.peek() instanceof CachedTable) ? (CachedTable) mComponentStack.peek() : null; if (start != null) { position = new LinkedList(start); } if (componentType.equals(OperationConstants.CELLS)) { // does not effect a spreadsheet currentTable.mCellCount++; if (mMaxAllowedCellCount != 0 && currentTable.mCellCount > mMaxAllowedCellCount) { currentTable.mIsTooLarge = true; } } else if (componentType.equals(OperationConstants.ROWS)) { // Counting Rows currentTable.mRowCount++; // no repeated in writer if (mMaxAllowedRowCount != 0 && currentTable.mRowCount > mMaxAllowedRowCount) { currentTable.mIsTooLarge = true; } } else if (componentType.equals( OperationConstants.TABLE)) { // all formats (Text & Spreadsheet atm) List tableGrid = (List) componentProperties[0]; CachedTable newCachedTable = startTableSizeEvaluation(position, tableGrid); if (newCachedTable.mTableGrid != null) { if (mMaxAllowedColumnCount != 0 && newCachedTable.mTableGrid.size() > mMaxAllowedColumnCount) { newCachedTable.mIsTooLarge = true; // adding the table now, as it would not be below as already too large.. } else { newCachedTable.mColumnCount = newCachedTable.mTableGrid.size(); } } // if it is the first root table if (currentTable == null) { // only for the root table the table itself will be added at the beginning currentTable = newCachedTable; // ++currentTable.mNumberOfNestedTables; mComponentStack.push(currentTable); currentTable.add( new CachedInnerTableOperation( componentType, position, false, hardFormattingProperties, componentProperties)); } else { // as subtable the table will be added twice: // once for the parent as notifier currentTable.addSubTable(newCachedTable, start); currentTable.add( new CachedInnerTableOperation( componentType, position, false, hardFormattingProperties, componentProperties)); currentTable = newCachedTable; // once for the child to create currentTable.add( new CachedInnerTableOperation( componentType, position, false, hardFormattingProperties, componentProperties)); } } if (!currentTable.mIsTooLarge && !componentType.equals(OperationConstants.TABLE) && !componentType.equals(OperationConstants.COLUMNS)) { currentTable.add( new CachedInnerTableOperation( componentType, position, false, hardFormattingProperties, componentProperties)); } } /** * According to user run-time configuration only tables of a certain size are allowed to be * created. Tables exceeding the limit are being shown by a replacement object, otherwise the * client performance might not be sufficient. */ private CachedTable startTableSizeEvaluation(List position, List tableGrid) { CachedTable cachedTable = null; cachedTable = new CachedTable(); cachedTable.mTableGrid = tableGrid; return cachedTable; } /** * According to user run-time configuration only tables of a certain size are allowed to be * created. Tables exceeding the limit are being shown by a replacement object, otherwise the * client performance might not be sufficient. * *

As the limit is being checked on sub table level, the complete table have to be parsed * before giving green light for any table. On the opposite, if a subtable is already too large, * it can be neglected collecting operations for that subtable. * * @throws SAXException */ private void endTableSizeEvaluation() throws SAXException { CachedTable cachedTableOps = (CachedTable) mComponentStack.peek(); if (cachedTableOps.getSubTableCount() == 0) { cachedTableOps.mMostUsedColumnStyle = getMostUsedStyle(cachedTableOps.columnStyleOccurrence); cachedTableOps.mMostUsedRowStyle = getMostUsedStyle(cachedTableOps.rowStyleOccurrence); mComponentStack.pop(); flushTableOperations(cachedTableOps, true); if (cachedTableOps != null && cachedTableOps.mCachedTableContentOps != null) { cachedTableOps.mCachedTableContentOps = null; cachedTableOps.lastRowFormatOperation = null; } } else if (cachedTableOps.getSubTableCount() > 0) { // when leaving a table, continue with the parent table cachedTableOps.removeSubTable(); } else { // below zero might appear, when table had started in blocked area // TODO: is it really possible to reach this point? if (cachedTableOps != null && cachedTableOps.mCachedTableContentOps != null) { cachedTableOps.mCachedTableContentOps = null; cachedTableOps.lastRowFormatOperation = null; } mComponentStack.pop(); } } @SuppressWarnings("rawtypes") private void flushTableOperations(CachedTable currentTable, boolean isStartOfTable) throws SAXException { boolean putPageBreak = false; boolean isBreakBefore = true; ListIterator cachedOperationIterator = currentTable.listIterator(); while (cachedOperationIterator.hasNext()) { CachedOperation operation = cachedOperationIterator.next(); if (operation instanceof CachedInnerTableOperation && operation.mComponentType.equals(OperationConstants.TABLE)) { if (isStartOfTable) { isStartOfTable = false; if (currentTable.mIsTooLarge) { // replacement table cacheOperation( false, OperationConstants.EXCEEDEDTABLE, operation.mStart, false, null, ((List) operation.mComponentProperties[0]).size(), currentTable.mRowCount, operation.mComponentProperties[0], mContextName); break; } else { if ((mMaxAllowedRowCount != 0 && currentTable.mRowCount > mMaxAllowedRowCount) || (mMaxAllowedColumnCount != 0 && currentTable.mColumnCount > mMaxAllowedColumnCount) || (mMaxAllowedCellCount != 0 && currentTable.mCellCount > mMaxAllowedCellCount)) { // TODO: Exceeded table operation name cacheOperation( false, OperationConstants.EXCEEDEDTABLE, operation.mStart, false, null, ((List) operation.mComponentProperties[0]).size(), currentTable.mRowCount, operation.mComponentProperties[0], mContextName); break; } else { // the last parameter are: mColumnRelWidths, mTableName, mIsTableVisible); JSONObject tableAttr = null; if (operation.mHardFormattingProperties.containsKey("table") && (((tableAttr = (JSONObject) operation.mHardFormattingProperties.get("table")) .has("pageBreakBefore")) || tableAttr.has("pageBreakAfter"))) { isBreakBefore = tableAttr.has("pageBreakBefore"); String breakString = isBreakBefore ? "pageBreakBefore" : "pageBreakAfter"; boolean breakAttr = tableAttr.getBoolean(breakString); if (breakAttr) { putPageBreak = true; } tableAttr.remove(breakString); } cacheOperation( false, OperationConstants.TABLE, operation.mStart, false, operation.mHardFormattingProperties, operation.mComponentProperties[0], operation.mComponentProperties[1], mContextName); } } } else { flushTableOperations(currentTable.getSubTable(operation.mStart), true); } } else if (operation.mComponentType.equals(OperationConstants.TEXT)) { String context = mContextName; if (operation.mComponentProperties.length > 1 && operation.mComponentProperties[1] != null) { context = (String) operation.mComponentProperties[1]; } cacheOperation( false, operation.mComponentType, operation.mStart, false, null, operation.mComponentProperties[0], context); } else if (operation.mComponentType.equals(OperationConstants.ATTRIBUTES)) { String context = mContextName; if (operation.mComponentProperties.length > 1 && operation.mComponentProperties[1] != null) { context = (String) operation.mComponentProperties[1]; } cacheOperation( false, OperationConstants.ATTRIBUTES, operation.mStart, false, operation.mHardFormattingProperties, operation.mComponentProperties[0], context); } else if (operation.mComponentType.equals(OperationConstants.SHAPE) || operation.mComponentType.equals(OperationConstants.IMAGE) || operation.mComponentType.equals(OperationConstants.SHAPE_GROUP)) { cacheOperation( false, operation.mComponentType, operation.mStart, false, operation.mHardFormattingProperties, mContextName); } else if (operation.mComponentType.equals(OperationConstants.FIELD)) { // TODO: Why do I have to check for map<> casts but not with String casts? @SuppressWarnings("unchecked") Map attrMap = (Map) operation.mComponentProperties[2]; cacheOperation( false, operation.mComponentType, operation.mStart, false, null, operation.mComponentProperties[0], operation.mComponentProperties[1], attrMap, mContextName); } else if (operation.mComponentType.equals(OperationConstants.TABLE) || operation.mComponentType.equals(OperationConstants.COMMENT) || operation.mComponentType.equals(OperationConstants.COMMENTRANGE)) { cacheOperation( false, operation.mComponentType, operation.mStart, false, operation.mHardFormattingProperties, operation.mComponentProperties); } else if (operation.mComponentType.equals(OperationConstants.COMMENT) || operation.mComponentType.equals(OperationConstants.COMMENTRANGE)) { cacheOperation( false, operation.mComponentType, operation.mStart, false, operation.mHardFormattingProperties, operation.mComponentProperties); } else { boolean isParagraphOperation = operation.mComponentType.equals(OperationConstants.PARAGRAPH); if (putPageBreak && isParagraphOperation) { JSONObject paraProps = null; if (operation.mHardFormattingProperties == null) { operation.mHardFormattingProperties = new HashMap(); } if (!operation.mHardFormattingProperties.containsKey("paragraph")) { paraProps = new JSONObject(); } else { paraProps = (JSONObject) operation.mHardFormattingProperties.get("paragraph"); } paraProps.put(isBreakBefore ? "pageBreakBefore" : "pageBreakAfter", true); operation.mHardFormattingProperties.put("paragraph", paraProps); putPageBreak = false; } String context = mContextName; if (isParagraphOperation && operation.mComponentProperties[0] != null) { context = (String) operation.mComponentProperties[0]; } cacheOperation( false, operation.mComponentType, operation.mStart, false, operation.mHardFormattingProperties, context); } } } private static String getMostUsedStyle(Map styleOccurrances) { String mostUsedStyleName = null; if (styleOccurrances != null) { Set> entrySet = styleOccurrances.entrySet(); Iterator> iter = entrySet.iterator(); Integer styleOccurance = null; Integer styleOccuranceMax = null; while (iter.hasNext()) { Entry entry = iter.next(); styleOccurance = entry.getValue(); if (styleOccuranceMax == null || styleOccuranceMax < styleOccurance) { styleOccuranceMax = styleOccurance; mostUsedStyleName = entry.getKey(); } } // if there is a most used style if (mostUsedStyleName != null) { // make sure it is not by coincidence the most single used one.. if (styleOccurrances.get(mostUsedStyleName) == 1) { mostUsedStyleName = null; } } } return mostUsedStyleName; } private JSONObject getHeaderFooterAttrs(OdfElement e) { JSONObject attrs = null; JSONObject pageAttrs = null; if (e != null) { final Element p = e.getChildElement( StyleHeaderFooterPropertiesElement.ELEMENT_NAME.getUri(), "header-footer-properties"); if (p != null) { pageAttrs = new JSONObject(3); final String sMinHeight = p.getAttribute("fo:min-height"); if (!sMinHeight.isEmpty()) { pageAttrs.put("minHeight", MapHelper.normalizeLength(sMinHeight)); } final String sHeight = p.getAttribute("svg:height"); if (!sHeight.isEmpty()) { pageAttrs.put("height", MapHelper.normalizeLength(sHeight)); } final String sMarginTop = p.getAttribute("fo:margin-top"); if (!sMarginTop.isEmpty()) { pageAttrs.put("marginTop", MapHelper.normalizeLength(sMarginTop)); } final String sMarginBottom = p.getAttribute("fo:margin-bottom"); if (!sMarginBottom.isEmpty()) { pageAttrs.put("marginBottom", MapHelper.normalizeLength(sMarginBottom)); } final String sMarginLeft = p.getAttribute("fo:margin-left"); if (!sMarginLeft.isEmpty()) { pageAttrs.put("marginLeft", MapHelper.normalizeLength(sMarginLeft)); } final String sMarginRight = p.getAttribute("fo:margin-right"); if (!sMarginRight.isEmpty()) { pageAttrs.put("marginRight", MapHelper.normalizeLength(sMarginRight)); } } } if (pageAttrs != null && pageAttrs.length() != 0) { attrs = new JSONObject(1); attrs.put("page", pageAttrs); } return attrs; } /** * Optimizes the operations of spreadsheet cells neglecting starting/trailing empty cells and for * cells with content or style bundling similar cells to single operations. * *

Repeated rows are automatically a range. * *

There are three pointer (variables), that are updated during parsing the spreadsheet: * mCurrentCellNo is the actual column number mFirstContentCellNo is mFirstEqualCellNo is set to * the first cell to be written, after a cell was written out or an empty precessor * *

ToDo: Refactoring - As soon every component got its own parser, the tableOps. have to be * replaced by the Context of the component * *

private CachedTable evaluateSimilarCells(CachedTable tableOps, CachedInnerTableOperation * cellOperation, JSONObject currentCell, boolean isRow) { // An Operation will always be * triggered in the end of the function boolean triggerOperation = false; * *

// every repeatedColumns row will result into a fillRange operation int * previousContentRepetition = 1; * *

// if the previous cells are equal if (tableOps.mFirstEqualCellNo > -1) { * previousContentRepetition = tableOps.mCurrentColumnNo - tableOps.mFirstEqualCellNo; } * *

boolean isRepeatedRow = tableOps.mLastRow != null && * !tableOps.mFirstRow.equals(tableOps.mLastRow); * *

// do not trigger the operation if the spreadsheetRow is null and its only member is null if * (tableOps.mSheetNo == null && cellOperation != null && cellOperation.mStart != null) { // we * have a cell position and require the two above (first parent row, afterwards cell) => already * -2 // and an additional - 1 as size of 1 would result in zero position ==> finally -3 * tableOps.mSheetNo = cellOperation.mStart.get(cellOperation.mStart.size() - 3); } // ** There * are four variations for previous/current cell we have to check: // 1) Current Content Cell, * Previous Content empty if (currentCell != null && tableOps.mPreviousCell != null) { // if the * two cells are NOT the same if (!currentCell.equals(tableOps.mPreviousCell)) { if * (previousContentRepetition > MIN_REPEATING_CONTENT_CELLS) { triggerOperation = true; } else { * if (tableOps.mCurrentRange == null) { tableOps.mCurrentRange = new JSONArray(); } // Resolving * mColumnRepetition, explicitly adding cells to the range for (int i = 0; * tableOps.mCurrentColumnNo - tableOps.mFirstEqualCellNo > i; i++) { * tableOps.mCurrentRange.put(tableOps.mPreviousCell); } // if the row is being repeated, there * are always vertical spans (fill same content multiple times if (tableOps.mLastRow - * tableOps.mFirstRow > 0) { triggerOperation = true; } // there is an upcoming fill operation, * the previous content has to be flushed if (tableOps.getCellRepetition() > * MIN_REPEATING_CONTENT_CELLS) { triggerOperation = true; } tableOps.mFirstEqualCellNo = * tableOps.mCurrentColumnNo; } } // 2) Current Content Cell, Previous Empty Cell (never have * saved anything) } else if (currentCell != null && tableOps.mPreviousCell == null) { // && * tableOps.mCurrentRange == null tableOps.mFirstEqualCellNo = tableOps.mCurrentColumnNo; if * (tableOps.mFirstContentCellNo == -1) { // reset the empty cell counter - if previous was empty * tableOps.mFirstContentCellNo = tableOps.mCurrentColumnNo; tableOps.mEmptyCellCount = 0; } else * { if (tableOps.getCellRepetition() > MIN_REPEATING_CONTENT_CELLS) { triggerOperation = true; } * else { if (tableOps.mCurrentRange == null) { tableOps.mCurrentRange = new JSONArray(); } for * (int i = 0; tableOps.mEmptyCellCount > i; i++) { tableOps.mCurrentRange.put(JSONObject.NULL); } * tableOps.mEmptyCellCount = 0; } } // 3) Content Cell empty, Previo Cell full } else if * (currentCell == null && tableOps.mPreviousCell != null) { tableOps.mEmptyCellCount += * tableOps.getCellRepetition(); // as there had been previously content // check if it was * repeating content if (previousContentRepetition > MIN_REPEATING_CONTENT_CELLS) { * triggerOperation = true; } else { if (tableOps.mCurrentRange == null) { tableOps.mCurrentRange * = new JSONArray(); } // save the previous cell for later compressed output for (int i = 0; * tableOps.mCurrentColumnNo - tableOps.mFirstEqualCellNo > i; i++) { * tableOps.mCurrentRange.put(tableOps.mPreviousCell); } // if the row is being repeated, there * are always vertical spans (fill same content multiple times if (tableOps.mLastRow - * tableOps.mFirstRow > 0) { triggerOperation = true; } } tableOps.mFirstEqualCellNo = -1; // if * there was previously repeating content cells // 4) Both are null } else if (currentCell == null * && tableOps.mPreviousCell == null & !isRow) { // note that an empty cell was passed * tableOps.mEmptyCellCount += tableOps.getCellRepetition(); // if this is the first empty cell if * (tableOps.mFirstEmptyCell == -1) { // remember when it started tableOps.mFirstEmptyCell = * tableOps.mCurrentColumnNo; * *

// else check if the maximum repeated empty cells was reached and existing content has to be * dispatched as an operation } else if (tableOps.mFirstContentCellNo != -1 && * MAX_REPEATING_EMPTY_CELLS > tableOps.mEmptyCellCount) { triggerOperation = true; } } * *

// RANGE CREATION: for every row we flush previous content OR if we want to flush for other * reasons if (isRow && tableOps.mFirstContentCellNo > -1 || triggerOperation) { // WRITING * WHITESPACE TO ROW // if the last cell used content, but there was previous whitespace, the * whitespace has to be explicitly set if (tableOps.mEmptyCellCount > 0 && currentCell != null) { * for (int i = 0; tableOps.mEmptyCellCount > i; i++) { if (tableOps.mCurrentRange == null) { * tableOps.mCurrentRange = new JSONArray(); } tableOps.mCurrentRange.put(JSONObject.NULL); } * tableOps.mEmptyCellCount = 0; } // WRITING CELL TO ROW // if content to flush exist and the * operation was triggered // OR there is horizontal repeated content // OR there is vertical * repeated content if (tableOps.mCurrentRange != null || previousContentRepetition > * MIN_REPEATING_CONTENT_CELLS || isRepeatedRow) { if(tableOps.mCurrentRange != null && * !tableOps.mCurrentRange.isEmpty()) { Component rootComponent = mSchemaDoc.getRootComponent(); * TableTableElement sheet = (TableTableElement)rootComponent.getChildNode(tableOps.mSheetNo); } * *

mJsonOperationProducer.addRange(tableOps.mSheetNo, tableOps.mFirstRow, tableOps.mLastRow, * tableOps.mPreviousRepeatedRows, tableOps.mFirstContentCellNo, previousContentRepetition, * tableOps.mPreviousCell, tableOps.mCurrentRange, previousContentRepetition > * MIN_REPEATING_CONTENT_CELLS); } // if a fill sufficent repeating is now after a content, the * previous content was flushed if (tableOps.mFirstEqualCellNo == tableOps.mCurrentColumnNo) { // * but still a content and repeating content exits tableOps.mFirstContentCellNo = * tableOps.mFirstEqualCellNo; } else { if (currentCell != null) { tableOps.mFirstContentCellNo = * tableOps.mCurrentColumnNo; tableOps.mFirstEqualCellNo = tableOps.mCurrentColumnNo; } else { * tableOps.mFirstContentCellNo = -1; tableOps.mFirstEqualCellNo = -1; } } tableOps.mCurrentRange * = null; } if (!isRow) { // Making the current cell the previous for next round * tableOps.mPreviousCell = currentCell; tableOps.mCurrentColumnNo += * tableOps.getCellRepetition(); tableOps.setCellRepetition(1); } else { // after the end of a row * reset all values tableOps.mSheetNo = null; tableOps.mCurrentRange = null; * tableOps.mPreviousCell = null; tableOps.mFirstContentCellNo = -1; tableOps.mFirstEqualCellNo = * -1; tableOps.mCurrentColumnNo = 0; tableOps.mEmptyCellCount = 0; tableOps.mFirstEmptyCell = -1; * tableOps.setCellRepetition(1); } return tableOps; } * *

static void stashColumnWidths(TableTableElement tableElement) { * List existingColumnList = getTableColumnElements(tableElement, new * LinkedList()); List tableColumWidths = * OdfFileSaxHandler.collectColumnWidths(tableElement, existingColumnList); * tableElement.pushTableGrid(tableColumWidths); } * *

static List collectColumnWidths(TableTableElement tableElement, * List columns) { boolean hasRelColumnWidth = false; boolean * hasAbsColumnWidth = false; boolean hasColumnWithoutWidth = false; List columnRelWidths * = new ArrayList(); for (TableTableColumnElement column : columns) { if * (column.hasAttributeNS(OdfDocumentNamespace.TABLE.getUri(), "style-name")) { Length tableWidth * = getPropertyLength(StyleTablePropertiesElement.Width, tableElement); * *

int repeatedColumns = 1; if (column.hasAttributeNS(OdfDocumentNamespace.TABLE.getUri(), * "number-columns-repeated")) { repeatedColumns = * Integer.parseInt(column.getAttributeNS(OdfDocumentNamespace.TABLE.getUri(), * "number-columns-repeated")); } * *

String columnRelWidth = getProperty(StyleTableColumnPropertiesElement.RelColumnWidth, * column); * *

// it is being assumed, when the columnRelWidth is once set, it is always set if * (columnRelWidth != null && !columnRelWidth.isEmpty()) { hasRelColumnWidth = true; if * (hasAbsColumnWidth) { LOG.warning("******* BEWARE: Absolute and relative width are not supposed * to be mixed!! ***********"); } columnRelWidth = columnRelWidth.substring(0, * columnRelWidth.indexOf('*')); Integer relWidth = Integer.parseInt(columnRelWidth); for (int i = * 0; i < repeatedColumns; i++) { columnRelWidths.add(relWidth); } } else { // if there is no * relative column width if (hasRelColumnWidth) { LOG.warning("******* BEWARE: Absolute and * relative width are not supposed to be mixed!! ***********"); } * *

Length columnWidth = getPropertyLength(StyleTableColumnPropertiesElement.ColumnWidth, * column); // there can be only table width and .. if (tableWidth != null) { // columnwidth, with * a single one missing if (columnWidth != null) { hasAbsColumnWidth = true; int widthFactor = * (int) Math.round((columnWidth.getMillimeters() * 100) / tableWidth.getMillimeters()); for (int * i = 0; i < repeatedColumns; i++) { columnRelWidths.add(widthFactor); } } else { if * (hasColumnWithoutWidth) { LOG.warning("******* BEWARE: Two columns without width and no column * width are not expected!! ***********"); } hasColumnWithoutWidth = true; } // if the table is * not set, it will always be unset.. } else { if (columnWidth != null) { hasAbsColumnWidth = * true; int widthFactor = (int) Math.round((columnWidth.getMicrometer() * 10)); for (int i = 0; i * < repeatedColumns; i++) { columnRelWidths.add(widthFactor); } } else { LOG.warning("******* * BEWARE: Two columns without width and no column width are not expected!! ***********"); } } } } * } return columnRelWidths; } /* Returns all TableTableColumn descendants that exist within the * tableElement, even within groups, columns and header elements * *

static List getTableColumnElements(Element parent, List columns) { * NodeList children = parent.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { * Node child = children.item(i); if (child instanceof Element) { if (child instanceof * TableTableColumnElement) { columns.add(child); } else if (child instanceof * TableTableColumnGroupElement || child instanceof TableTableHeaderColumnsElement || child * instanceof TableTableColumnsElement) { columns = getTableColumnElements((Element) child, * columns); } else if (child instanceof TableTableRowGroupElement || child instanceof * TableTableHeaderRowsElement || child instanceof TableTableRowElement || child instanceof * TableTableRowsElement) { break; } } } return columns; } */ }