| /* ==================================================================== |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You 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.apache.poi.ooxml.util; |
| |
| import java.util.Locale; |
| |
| import javax.xml.XMLConstants; |
| import javax.xml.namespace.QName; |
| import javax.xml.xpath.XPathFactory; |
| |
| import com.microsoft.schemas.compatibility.AlternateContentDocument; |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| import org.apache.poi.util.Internal; |
| import org.apache.poi.xslf.usermodel.XSLFShape; |
| import org.apache.xmlbeans.XmlCursor; |
| import org.apache.xmlbeans.XmlException; |
| import org.apache.xmlbeans.XmlObject; |
| import org.apache.xmlbeans.impl.values.XmlAnyTypeImpl; |
| |
| public final class XPathHelper { |
| private static final Logger LOG = LogManager.getLogger(XPathHelper.class); |
| |
| private static final String OSGI_ERROR = |
| "Schemas (*.xsb) for <CLASS> can't be loaded - usually this happens when OSGI " + |
| "loading is used and the thread context classloader has no reference to " + |
| "the xmlbeans classes - please either verify if the <XSB>.xsb is on the " + |
| "classpath or alternatively try to use the poi-ooxml-full-x.x.jar"; |
| |
| private static final String MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006"; |
| private static final String MAC_DML_NS = "http://schemas.microsoft.com/office/mac/drawingml/2008/main"; |
| private static final QName ALTERNATE_CONTENT_TAG = new QName(MC_NS, "AlternateContent"); |
| // AlternateContentDocument.AlternateContent.type.getName(); |
| |
| private XPathHelper() {} |
| |
| static final XPathFactory xpathFactory = XPathFactory.newInstance(); |
| static { |
| trySetFeature(xpathFactory, XMLConstants.FEATURE_SECURE_PROCESSING, true); |
| } |
| |
| public static XPathFactory getFactory() { |
| return xpathFactory; |
| } |
| |
| private static void trySetFeature(XPathFactory xpf, String feature, boolean enabled) { |
| try { |
| xpf.setFeature(feature, enabled); |
| } catch (Exception e) { |
| LOG.atWarn().withThrowable(e).log("XPathFactory Feature ({}) unsupported", feature); |
| } catch (AbstractMethodError ame) { |
| LOG.atWarn().withThrowable(ame).log("Cannot set XPathFactory feature ({}) because outdated XML parser in classpath", feature); |
| } |
| } |
| |
| |
| |
| /** |
| * Internal code - API may change any time! |
| * <p> |
| * The {@link #selectProperty(Class, String)} xquery method has some performance penalties, |
| * which can be workaround by using {@link XmlCursor}. This method also takes into account |
| * that {@code AlternateContent} tags can occur anywhere on the given path. |
| * <p> |
| * It returns the first element found - the search order is: |
| * <ul> |
| * <li>searching for a direct child</li> |
| * <li>searching for a AlternateContent.Choice child</li> |
| * <li>searching for a AlternateContent.Fallback child</li> |
| * </ul> |
| * Currently POI OOXML is based on the first edition of the ECMA 376 schema, which doesn't |
| * allow AlternateContent tags to show up everywhere. The factory flag is |
| * a workaround to process files based on a later edition. But it comes with the drawback: |
| * any change on the returned XmlObject aren't saved back to the underlying document - |
| * so it's a non updatable clone. If factory is null, a XmlException is |
| * thrown if the AlternateContent is not allowed by the surrounding element or if the |
| * extracted object is of the generic type XmlAnyTypeImpl. |
| * |
| * @param resultClass the requested result class |
| * @param factory a factory parse method reference to allow reparsing of elements |
| * extracted from AlternateContent elements. Usually the enclosing XmlBeans type needs to be used |
| * to parse the stream |
| * @param path the elements path, each array must contain at least 1 QName, |
| * but can contain additional alternative tags |
| * @return the xml object at the path location, or null if not found |
| * |
| * @throws XmlException If factory is null, a XmlException is |
| * thrown if the AlternateContent is not allowed by the surrounding element or if the |
| * extracted object is of the generic type XmlAnyTypeImpl. |
| * |
| * @since POI 4.1.2 |
| */ |
| @SuppressWarnings("unchecked") |
| @Internal |
| public static <T extends XmlObject> T selectProperty(XmlObject startObject, Class<T> resultClass, XSLFShape.ReparseFactory<T> factory, QName[]... path) |
| throws XmlException { |
| XmlObject xo = startObject; |
| XmlCursor cur = xo.newCursor(); |
| XmlCursor innerCur = null; |
| try { |
| innerCur = selectProperty(cur, path, 0, factory != null, false); |
| if (innerCur == null) { |
| return null; |
| } |
| |
| // Pesky XmlBeans bug - see Bugzilla #49934 |
| // it never happens when using poi-ooxml-full jar but may happen with the abridged poi-ooxml-lite jar |
| xo = innerCur.getObject(); |
| if (xo instanceof XmlAnyTypeImpl) { |
| String errorTxt = OSGI_ERROR |
| .replace("<CLASS>", resultClass.getSimpleName()) |
| .replace("<XSB>", resultClass.getSimpleName().toLowerCase(Locale.ROOT)+"*"); |
| if (factory == null) { |
| throw new XmlException(errorTxt); |
| } else { |
| xo = factory.parse(innerCur.newXMLStreamReader()); |
| } |
| } |
| |
| return (T)xo; |
| } finally { |
| cur.dispose(); |
| if (innerCur != null) { |
| innerCur.dispose(); |
| } |
| } |
| } |
| |
| private static XmlCursor selectProperty(final XmlCursor cur, final QName[][] path, final int offset, final boolean reparseAlternate, final boolean isAlternate) |
| throws XmlException { |
| // first try the direct children |
| for (QName qn : path[offset]) { |
| for (boolean found = cur.toChild(qn); found; found = cur.toNextSibling(qn)) { |
| if (offset == path.length-1) { |
| return cur; |
| } |
| cur.push(); |
| XmlCursor innerCur = selectProperty(cur, path, offset+1, reparseAlternate, false); |
| if (innerCur != null) { |
| return innerCur; |
| } |
| cur.pop(); |
| } |
| } |
| // if we were called inside an alternate content handling don't look for alternates again |
| if (isAlternate || !cur.toChild(ALTERNATE_CONTENT_TAG)) { |
| return null; |
| } |
| |
| // otherwise check first the choice then the fallback content |
| XmlObject xo = cur.getObject(); |
| AlternateContentDocument.AlternateContent alterCont; |
| if (xo instanceof AlternateContentDocument.AlternateContent) { |
| alterCont = (AlternateContentDocument.AlternateContent)xo; |
| } else { |
| // Pesky XmlBeans bug - see Bugzilla #49934 |
| // it never happens when using poi-ooxml-full jar but may happen with the abridged poi-ooxml-lite jar |
| if (!reparseAlternate) { |
| throw new XmlException(OSGI_ERROR |
| .replace("<CLASS>", "AlternateContent") |
| .replace("<XSB>", "alternatecontentelement") |
| ); |
| } |
| try { |
| AlternateContentDocument acd = AlternateContentDocument.Factory.parse(cur.newXMLStreamReader()); |
| alterCont = acd.getAlternateContent(); |
| } catch (XmlException e) { |
| throw new XmlException("unable to parse AlternateContent element", e); |
| } |
| } |
| |
| final int choices = alterCont.sizeOfChoiceArray(); |
| for (int i=0; i<choices; i++) { |
| // TODO: check [Requires] attribute of [Choice] element, if we can handle the content |
| AlternateContentDocument.AlternateContent.Choice choice = alterCont.getChoiceArray(i); |
| XmlCursor cCur = choice.newCursor(); |
| XmlCursor innerCur = null; |
| try { |
| String requiresNS = cCur.namespaceForPrefix(choice.getRequires()); |
| if (MAC_DML_NS.equalsIgnoreCase(requiresNS)) { |
| // Mac DML usually contains PDFs ... |
| continue; |
| } |
| innerCur = selectProperty(cCur, path, offset, reparseAlternate, true); |
| if (innerCur != null) { |
| return innerCur; |
| } |
| } finally { |
| if (innerCur != cCur) { |
| cCur.dispose(); |
| } |
| } |
| } |
| |
| if (!alterCont.isSetFallback()) { |
| return null; |
| } |
| |
| XmlCursor fCur = alterCont.getFallback().newCursor(); |
| XmlCursor innerCur = null; |
| try { |
| innerCur = selectProperty(fCur, path, offset, reparseAlternate, true); |
| return innerCur; |
| } finally { |
| if (innerCur != fCur) { |
| fCur.dispose(); |
| } |
| } |
| } |
| |
| } |