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

com.sun.enterprise.deployment.node.SaxParserHandler Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2022, 2024 Contributors to the Eclipse Foundation
 * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package com.sun.enterprise.deployment.node;

import com.sun.enterprise.deployment.EnvironmentProperty;
import com.sun.enterprise.deployment.util.DOLUtils;
import com.sun.enterprise.deployment.xml.DTDRegistry;
import com.sun.enterprise.deployment.xml.TagNames;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EmptyStackException;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.glassfish.hk2.api.PerLookup;
import org.jvnet.hk2.annotations.Service;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.NamespaceSupport;


/**
 * This class implements all the callbacks for the SAX Parser in JAXP 1.1
 *
 * @author Jerome Dochez
 */
@Service
@PerLookup
public class SaxParserHandler extends DefaultHandler {

    private static final Logger LOG = DOLUtils.getDefaultLogger();
    private static final MappingStuff mappingStuff = new MappingStuff();

    private final List> nodes = new ArrayList<>();
    private XMLNode topNode;
    // FIXME: Used as a local variable
    protected String publicID;
    private StringBuffer elementData;
    private Map prefixMapping;

    private boolean stopOnXMLErrors;

    private boolean pushedNamespaceContext;
    private final NamespaceSupport namespaces = new NamespaceSupport();
    private final Stack elementStack = new Stack<>();

    private String rootElement;

    private List versionUpgradeList;

    private boolean doDelete;
    private String errorReportingString = "";


    protected static Map getMapping() {
        return mappingStuff.mMapping;
    }

    protected static List getVersionUpgrades(String key) {
        List versionUpgradeList = mappingStuff.mVersionUpgrades.get(key);
        if (versionUpgradeList != null) {
            return versionUpgradeList;
        }
        List> classList = mappingStuff.mVersionUpgradeClasses.get(key);
        if (classList == null) {
            return null;
        }
        versionUpgradeList = new ArrayList<>();
        for (int n = 0; n < classList.size(); ++n) {
            VersionUpgrade versionUpgrade = null;
            try {
                versionUpgrade = (VersionUpgrade) classList.get(n).getDeclaredConstructor().newInstance();
            } catch (Exception ex) {
            }
            if (versionUpgrade != null) {
                versionUpgradeList.add(versionUpgrade);
            }
        }
        mappingStuff.mVersionUpgrades.put(key, versionUpgradeList);
        return versionUpgradeList;
    }

    protected static Collection getElementsAllowingEmptyValues() {
        return mappingStuff.mElementsAllowingEmptyValues;
    }

    protected static Collection getElementsPreservingWhiteSpace() {
        return mappingStuff.mElementsPreservingWhiteSpace;
    }

    public static void registerBundleNode(BundleNode bundleNode, String bundleTagName) {
        /*
        * There is exactly one standard node object for each descriptor type.
        * The node's registerBundle method itself adds the publicID-to-DTD
        * entry to the mapping.  This method needs to add the tag-to-node class
        * entry to the rootNodes map.
        */
        if (mappingStuff.mBundleRegistrationStatus.containsKey(bundleTagName)) {
            LOG.log(Level.FINEST, "Mapping already contains entry for tag {0}, returning.", bundleTagName);
            return;
        }

        final Map dtdMapping = new HashMap<>();
        final Map>> versionUpgrades = new HashMap<>();

        String rootNodeKey = bundleNode.registerBundle(dtdMapping);
        mappingStuff.mRootNodesMutable.putIfAbsent(rootNodeKey, bundleNode.getClass());

        /*
        * There can be multiple runtime nodes (for example, sun-xxx and
        * glassfish-xxx).  So the BundleNode's registerRuntimeBundle
        * updates the publicID-to-DTD map and returns a map of tags to
        * runtime node classes.
        */
        mappingStuff.mRootNodesMutable.putAll(bundleNode.registerRuntimeBundle(dtdMapping, versionUpgrades));

        mappingStuff.mVersionUpgradeClasses.putAll(versionUpgrades);

        // let's remove the URL from the DTD so we use local copies...
        for (Map.Entry entry : dtdMapping.entrySet()) {
            final String publicID = entry.getKey();
            final String dtd = entry.getValue();
            String systemIDResolution = resolvePublicID(publicID, dtd);
            if (systemIDResolution == null) {
                mappingStuff.mMapping.put(publicID, dtd.substring(dtd.lastIndexOf('/') + 1));
            } else {
                mappingStuff.mMapping.put(publicID, systemIDResolution);
            }
        }
        LOG.log(Level.FINER, "Final mapping keys for root node key {0}:\n {1}",
            new Object[] {rootNodeKey, mappingStuff.mMapping.keySet()});

        // This node might know of elements which should permit empty values,
        // or elements for which we should preserve white space.  Track them.
        Collection c = bundleNode.elementsAllowingEmptyValue();
        if (!c.isEmpty()) {
            mappingStuff.mElementsAllowingEmptyValuesMutable.addAll(c);
        }

        c = bundleNode.elementsPreservingWhiteSpace();
        if (!c.isEmpty()) {
            mappingStuff.mElementsPreservingWhiteSpaceMutable.addAll(c);
        }

        mappingStuff.mBundleRegistrationStatus.put(rootNodeKey, Boolean.TRUE);
    }

    // It creates the InputSource
    @SuppressWarnings("resource")
    @Override
    public InputSource resolveEntity(String publicID, String systemID) throws SAXException {
        try {
            LOG.log(Level.FINEST, "resolveEntity, publicID={0}, systemID={1}", new Object[] {publicID, systemID});
            // If public ID is there and is present in our map, use it
            if (publicID != null && getMapping().containsKey(publicID)) {
                this.publicID = publicID;
                return new InputSource(new BufferedInputStream(getDTDUrlFor(getMapping().get(publicID))));
            }
            // In case invalid public ID is given (or) public ID is null, use system ID to resolve
            // unspecified schema
            if (systemID == null || systemID.lastIndexOf('/') == systemID.length()) {
                return null;
            }

            String namespaceResolution = resolveSchemaNamespace(systemID);
            final String fileName;
            if (namespaceResolution == null) {
                fileName = getSchemaURLFor(systemID.substring(systemID.lastIndexOf('/') + 1));
            } else {
                fileName = getSchemaURLFor(namespaceResolution);
            }
            // if this is not a request for a schema located in our repository, we fail the
            // deployment
            if (fileName == null) {
                throw new SAXException("Requested schema " + systemID + " is not found in local repository, please"
                    + " ensure that there are no typos in the XML namespace declaration.");
            }
            LOG.log(Level.FINE, "Resolved publicID={0} and systemID={1} to {2}",
                new Object[] {publicID, systemID, fileName});
            return new InputSource(fileName);
        } catch (SAXException e) {
            throw e;
        } catch (Exception ioe) {
            throw new SAXException(ioe);
        }
    }

    /**
     * Sets if the parser should stop parsing and generate an SAXPArseException
     * when the xml parsed contains errors in regards to validation
     */
    public void setStopOnError(boolean stop) {
        stopOnXMLErrors = stop;
    }


    @Override
    public void error(SAXParseException spe) throws SAXParseException {
        LOG.log(Level.SEVERE, DOLUtils.INVALILD_DESCRIPTOR_LONG,
            new Object[] {errorReportingString, String.valueOf(spe.getLineNumber()),
                String.valueOf(spe.getColumnNumber()), spe.getLocalizedMessage()});
        if (stopOnXMLErrors) {
            throw spe;
        }
    }


    @Override
    public void fatalError(SAXParseException spe) throws SAXParseException {
        LOG.log(Level.SEVERE, DOLUtils.INVALILD_DESCRIPTOR_LONG,
            new Object[] {errorReportingString, String.valueOf(spe.getLineNumber()),
                String.valueOf(spe.getColumnNumber()), spe.getLocalizedMessage()});
        if (stopOnXMLErrors) {
            throw spe;
        }
    }

    /**
     * @return the input stream for a DTD public ID
     */
     protected InputStream getDTDUrlFor(String dtdFileName) {
        String dtdLoc = DTDRegistry.DTD_LOCATION.replace('/', File.separatorChar);
        File f = new File(dtdLoc + File.separatorChar + dtdFileName);
        try {
            return new BufferedInputStream(new FileInputStream(f));
        } catch(FileNotFoundException fnfe) {
            LOG.fine("Cannot find DTD " + dtdFileName);
            return null;
        }
     }

    /**
     * @return an URL for the schema location for a schema indentified by the
     * passed parameter
     * @param schemaSystemID the system id for the schema
     */
    public static String getSchemaURLFor(String schemaSystemID) throws IOException {
        File f = getSchemaFileFor(schemaSystemID);
        if (f == null) {
            return null;
        }
        return f.toURI().toURL().toString();
    }

    /**
     * @return a File pointer to the localtion of the schema indentified by the
     * passed parameter
     * @param schemaSystemID the system id for the schema
     */
    public static File getSchemaFileFor(String schemaSystemID) throws IOException {
        LOG.log(Level.FINE, "Getting Schema {0}", schemaSystemID);
        String schemaLoc = DTDRegistry.SCHEMA_LOCATION.replace('/', File.separatorChar);
        File f = new File(schemaLoc + File.separatorChar + schemaSystemID);
        if (f.exists()) {
            return f;
        }
        LOG.log(Level.INFO, "Cannot find the schema file {0}", f);
        return null;
    }


    /**
     * Determine whether the syatemID starts with a known namespace.
     * If so, strip off that namespace and return the rest.
     * Otherwise, return null
     * @param systemID The systemID to examine
     * @return the part if the namespace to find in the file system
     * or null if the systemID does not start with a known namespace
     */
    public static String resolveSchemaNamespace(String systemID) {
        List namespaces = DOLUtils.getProprietarySchemaNamespaces();
        for (String namespace : namespaces) {
            if (systemID.startsWith(namespace)) {
                return systemID.substring(namespace.length());
            }
        }
        return null;
    }

    /**
     * Determine whether the publicID starts with a known proprietary value.
     * If so, strip off that value and return the rest.
     * Otherwise, return null
     * @param publicID The publicID to examine
     * @return the part if the namespace to find in the file system
     * or null if the publicID does not start with a known namespace
     */
    public static String resolvePublicID(String publicID, String dtd) {
        List dtdStarts = DOLUtils.getProprietaryDTDStart();
        for (String dtdStart : dtdStarts) {
            if (dtd.startsWith(dtdStart)) {
                return dtd.substring(dtdStart.length());
            }
        }
        return null;
    }

    @Override
    public void notationDecl(java.lang.String name,
                         java.lang.String publicId,
                         java.lang.String systemId)
                         throws SAXException {
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Received notation " + name + " :=: " + publicId + " :=: " + systemId);
        }
    }


    @Override
    public void startPrefixMapping(String prefix,
                               String uri)
                        throws SAXException {

        if (prefixMapping==null) {
            prefixMapping = new HashMap<>();
        }

        // We need one namespace context per element, but any prefix mapping
        // callbacks occur *before* startElement is called.  So, push a
        // context on the first startPrefixMapping callback per element.
        if (!pushedNamespaceContext) {
            namespaces.pushContext();
            pushedNamespaceContext = true;
        }
        namespaces.declarePrefix(prefix,uri);
        prefixMapping.put(prefix, uri);
    }


    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) {
        LOG.log(Level.FINEST, "startElement(qName={0})", qName);
        if (!pushedNamespaceContext) {
            // We need one namespae context per element, so push a context
            // if there weren't any prefix mappings defined.
            namespaces.pushContext();
        }
        // Always reset flag since next callback could be startPrefixMapping
        // OR another startElement.
        pushedNamespaceContext = false;

        doDelete = false;
        String lastElement = null;
        try {
            lastElement = elementStack.pop();
        } catch (EmptyStackException ex) {
        }
        if (lastElement == null) {
            rootElement = localName;
            versionUpgradeList = getVersionUpgrades(rootElement);
            if (versionUpgradeList != null) {
                for (VersionUpgrade versionUpgrade : versionUpgradeList) {
                    versionUpgrade.init();
                }
            }
            elementStack.push(localName);
        } else {
            lastElement += "/" + localName;
            elementStack.push(lastElement);
        }

        if (versionUpgradeList != null) {
            for (VersionUpgrade versionUpgrade : versionUpgradeList) {
                if (VersionUpgrade.UpgradeType.REMOVE_ELEMENT == versionUpgrade.getUpgradeType()) {
                    Map matchXPath = versionUpgrade.getMatchXPath();
                    int entriesMatched = 0;
                    for (Map.Entry entry : matchXPath.entrySet()) {
                        if (entry.getKey().equals(lastElement)) {
                            entry.setValue(elementData.toString());
                            ++entriesMatched;
                        }
                    }
                    if (entriesMatched == matchXPath.size()) {
                        doDelete = true;
                        break;
                    }
                }
            }
        }

        LOG.finer(() -> "Start of element with qName=" + qName + ", localName=" + localName + ", uri=" + uri);
        XMLNode node = null;
        elementData = new StringBuffer();

        if (nodes.isEmpty()) {
            // this must be a root element...
            Class rootNodeClass = mappingStuff.mRootNodes.get(localName);
            if (rootNodeClass == null) {
                LOG.log(Level.SEVERE, "The " + localName + " is not supported!");
                if (stopOnXMLErrors) {
                    throw new IllegalArgumentException(
                        errorReportingString + " Element [" + localName + "] is not a valid root element");
                }
            } else {
                try {
                    node = (XMLNode) rootNodeClass.getDeclaredConstructor().newInstance();
                    LOG.log(Level.FINE, "Instantiating {0}", node);
                    if (node instanceof RootXMLNode) {
                        if (publicID != null) {
                            ((RootXMLNode) node).setDocType(publicID);
                        }
                        addPrefixMapping(node);
                    }
                    nodes.add(node);
                    topNode = node;
                    node.getDescriptor();
                } catch (Exception e) {
                    LOG.log(Level.WARNING, "Error occurred", e);
                    return;
                }
            }
        } else {
            node = nodes.get(nodes.size() - 1);
        }
        if (node != null) {
            XMLElement element = new XMLElement(qName, namespaces);
            if (node.handlesElement(element)) {
                node.startElement(element, attributes);
            } else {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("Asking for new handler for " + element + " to " + node);
                }
                XMLNode newNode = node.getHandlerFor(element);
                LOG.log(Level.FINE, "Got new node: {0}", newNode);
                nodes.add(newNode);
                addPrefixMapping(newNode);
                newNode.startElement(element, attributes);
            }
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) {

        String lastElement = null;
        try {
            lastElement = elementStack.peek();
        } catch (EmptyStackException ex) {
        }

        if (LOG.isLoggable(Level.FINER)) {
            LOG.finer("End of element with qName=" + qName + ", localName=" + localName + ", uri=" + uri + " and value="
                + elementData);
        }
        if (nodes.isEmpty()) {
            // no more nodes to pop
            elementData = null;
            return;
        }
        XMLElement element = new XMLElement(qName, namespaces);
        XMLNode topNode = nodes.get(nodes.size() - 1);
        boolean ignoredBecauseEmpty = elementData != null && elementData.length() == 0
            && !allowsEmptyValue(element.getQName());
        if (elementData != null && ignoredBecauseEmpty) {
            LOG.fine(() -> "Ignoring empty element with qName=" + qName);
        }
        if (elementData != null && !ignoredBecauseEmpty) {
            if (LOG.isLoggable(Level.FINER)) {
                LOG.finer("For element " + element.getQName() + " and value " + elementData);
            }
            boolean doReplace = false;
            String replacementName = null;
            String replacementValue = null;
            if (versionUpgradeList != null) {
                for (VersionUpgrade versionUpgrade : versionUpgradeList) {
                    if (VersionUpgrade.UpgradeType.REPLACE_ELEMENT == versionUpgrade.getUpgradeType()) {
                        Map matchXPath = versionUpgrade.getMatchXPath();
                        int entriesMatched = 0;
                        for (Map.Entry entry : matchXPath.entrySet()) {
                            if (entry.getKey().equals(lastElement)) {
                                entry.setValue(elementData.toString());
                                ++entriesMatched;
                            }
                        }
                        if (entriesMatched == matchXPath.size()) {
                            if (versionUpgrade.isValid()) {
                                doReplace = true;
                                replacementName = versionUpgrade.getReplacementElementName();
                                replacementValue = versionUpgrade.getReplacementElementValue();
                            } else {
                                StringBuilder buf = new StringBuilder();
                                buf.append("Invalid upgrade from <");
                                for (Map.Entry entry : matchXPath.entrySet()) {
                                    buf.append(entry.getKey()).append("  ").append(entry.getValue()).append(" >");
                                }
                                LOG.log(Level.SEVERE, buf.toString());
                                // Since the elements are not replaced,
                                // there should be a parsing error
                            }
                            break;
                        }
                    }
                }
            }
            if (doReplace) {
                element = new XMLElement(replacementName, namespaces);
                topNode.setElementValue(element, replacementValue);
            } else if (doDelete) {
                // don't set a value so that the element is not written out
            } else if (getElementsPreservingWhiteSpace().contains(element.getQName())) {
                topNode.setElementValue(element, elementData.toString());
            } else if (element.getQName().equals(TagNames.ENVIRONMENT_PROPERTY_VALUE)) {
                Object envEntryDesc = topNode.getDescriptor();
                if (envEntryDesc instanceof EnvironmentProperty) {
                    EnvironmentProperty envProp = (EnvironmentProperty) envEntryDesc;
                    // we need to preserve white space for env-entry-value
                    // if the env-entry-type is java.lang.String or
                    // java.lang.Character
                    if (String.class.getName().equals(envProp.getType())
                        || Character.class.getName().equals(envProp.getType())) {
                        topNode.setElementValue(element, elementData.toString());
                    } else {
                        topNode.setElementValue(element, elementData.toString().trim());
                    }
                } else {
                    topNode.setElementValue(element, elementData.toString().trim());
                }
            } else {
                // Allow any case for true/false & convert to lower case
                String val = elementData.toString().trim();
                if ("true".equalsIgnoreCase(val)) {
                    topNode.setElementValue(element, val.toLowerCase(Locale.US));
                } else if ("false".equalsIgnoreCase(val)) {
                    topNode.setElementValue(element, val.toLowerCase(Locale.US));
                } else {
                    topNode.setElementValue(element, val);
                }
            }
            elementData = null;
        }
        if (topNode.endElement(element)) {
            LOG.log(Level.FINE, "Removing top node {0}", topNode);
            nodes.remove(nodes.size()-1);
        }

        namespaces.popContext();
        pushedNamespaceContext=false;

        try {
            lastElement = elementStack.pop();
        } catch (EmptyStackException ex) {
        }
        if (lastElement != null) {
            if (lastElement.lastIndexOf("/") >= 0) {
                lastElement = lastElement.substring(0, lastElement.lastIndexOf("/"));
                elementStack.push(lastElement);
            }
        }
    }

    @Override
    public void characters(char[] ch, int start, int stop) {
        if (elementData!=null) {
            elementData = elementData.append(ch,start, stop);
        }
    }

    public XMLNode getTopNode() {
        return topNode;
    }

    public void setTopNode(XMLNode node) {
        topNode = node;
        nodes.add(node);
    }

    private void addPrefixMapping(XMLNode node) {
        if (prefixMapping != null) {
            for (Map.Entry entry : prefixMapping.entrySet()) {
                node.addPrefixMapping(entry.getKey(), entry.getValue());
            }
            prefixMapping = null;
        }
    }

    /**
     * Sets the error reporting context string
     */
    public void setErrorReportingString(String errorReportingString) {
        this.errorReportingString = errorReportingString;
    }

    /**
     * Indicates whether the element name is one for which empty values should
     * be recorded.
     * 

* If there were many tags that support empty values, it might make sense to * have a constant list that contains all those tag names. Then this method * would search the list for the target elementName. Because this code * is potentially invoked for many elements that do not support empty values, * and because the list is very small at the moment, the current * implementation uses an inelegant but fast equals test. *

* If the set of tags that should support empty values grows a little, * extending the expression to * * elementName.equals(TAG_1) || elementName.equals(TAG_2) || ... * * might make sense. If the set of such tags grows sufficiently large, then * a list-based approach might make more sense even though it might prove * to be slower. * @param elementName the name of the element * @return boolean indicating whether empty values should be recorded for this element */ private boolean allowsEmptyValue(String elementName) { return getElementsAllowingEmptyValues().contains(elementName); } private static final class MappingStuff { public final ConcurrentMap mBundleRegistrationStatus = new ConcurrentHashMap<>(); public final ConcurrentMap mMapping = new ConcurrentHashMap<>(); private final ConcurrentMap> mRootNodesMutable; public final Map> mRootNodes; private final CopyOnWriteArraySet mElementsAllowingEmptyValuesMutable; public final Collection mElementsAllowingEmptyValues; private final CopyOnWriteArraySet mElementsPreservingWhiteSpaceMutable; public final Collection mElementsPreservingWhiteSpace; private final Map>> mVersionUpgradeClasses; private final Map> mVersionUpgrades; MappingStuff() { mRootNodesMutable = new ConcurrentHashMap<>(); mRootNodes = Collections.unmodifiableMap(mRootNodesMutable); mElementsAllowingEmptyValuesMutable = new CopyOnWriteArraySet<>(); mElementsAllowingEmptyValues = Collections.unmodifiableSet(mElementsAllowingEmptyValuesMutable); mElementsPreservingWhiteSpaceMutable = new CopyOnWriteArraySet<>(); mElementsPreservingWhiteSpace = Collections.unmodifiableSet(mElementsPreservingWhiteSpaceMutable); mVersionUpgradeClasses = new ConcurrentHashMap<>(); mVersionUpgrades = new ConcurrentHashMap<>(); } }}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy