blob: 264d1bf0f5acb1c7fb2e5e436008373df7eaab27 [file] [log] [blame]
/*
* Copyright (c) 2022 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 com.sun.enterprise.util.LocalStringManagerImpl;
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 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
* @version
*/
@Service
@PerLookup
public class SaxParserHandler extends DefaultHandler {
public static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
public static final String JAXP_SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource";
public static final String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";
private static final String TRUE_STR = "true";
private static final String FALSE_STR = "false";
private static final class MappingStuff {
public final ConcurrentMap<String, Boolean> mBundleRegistrationStatus = new ConcurrentHashMap<>();
public final ConcurrentMap<String,String> mMapping = new ConcurrentHashMap<>();
private final ConcurrentMap<String,Class> mRootNodesMutable;
public final Map<String,Class> mRootNodes;
private final CopyOnWriteArraySet<String> mElementsAllowingEmptyValuesMutable;
public final Collection<String> mElementsAllowingEmptyValues;
private final CopyOnWriteArraySet<String> mElementsPreservingWhiteSpaceMutable;
public final Collection<String> mElementsPreservingWhiteSpace;
private final Map<String, List<Class>> mVersionUpgradeClasses;
private final Map<String, List<VersionUpgrade>> 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<>();
}
}
private static final MappingStuff _mappingStuff = new MappingStuff();
private final List<XMLNode> nodes = new ArrayList<>();
public XMLNode topNode = null;
protected String publicID = null;
private StringBuffer elementData = null;
private Map<String, String> prefixMapping = null;
private boolean stopOnXMLErrors = false;
private boolean pushedNamespaceContext=false;
private final NamespaceSupport namespaces = new NamespaceSupport();
private final Stack elementStack = new Stack();
private static final LocalStringManagerImpl localStrings = new LocalStringManagerImpl(SaxParserHandler.class);
protected static Map<String,String> getMapping() {
return _mappingStuff.mMapping;
}
protected static List<VersionUpgrade> getVersionUpgrades(String key) {
List<VersionUpgrade> versionUpgradeList = _mappingStuff.mVersionUpgrades.get(key);
if (versionUpgradeList != null) {
return versionUpgradeList;
}
List<Class> 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).newInstance();
} catch (Exception ex) {
}
if (versionUpgrade != null) {
versionUpgradeList.add(versionUpgrade);
}
}
_mappingStuff.mVersionUpgrades.put(key, versionUpgradeList);
return versionUpgradeList;
}
protected static Collection<String> getElementsAllowingEmptyValues() {
return _mappingStuff.mElementsAllowingEmptyValues;
}
protected static Collection<String> getElementsPreservingWhiteSpace() {
return _mappingStuff.mElementsPreservingWhiteSpace;
}
private String rootElement = null;
private List<VersionUpgrade> versionUpgradeList = null;
private boolean doDelete = false;
public static void registerBundleNode(BundleNode bn, 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)) {
return;
}
final HashMap<String, String> dtdMapping = new HashMap<>();
final Map<String, List<Class>> versionUpgrades = new HashMap<>();
String rootNodeKey = bn.registerBundle(dtdMapping);
_mappingStuff.mRootNodesMutable.putIfAbsent(rootNodeKey, bn.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(bn.registerRuntimeBundle(dtdMapping, versionUpgrades));
_mappingStuff.mVersionUpgradeClasses.putAll(versionUpgrades);
// let's remove the URL from the DTD so we use local copies...
for (Map.Entry<String, String> 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);
}
}
/*
* This node might know of elements which should permit empty values,
* or elements for which we should preserve white space. Track them.
*/
Collection<String> c = bn.elementsAllowingEmptyValue();
if (c.size() > 0) {
_mappingStuff.mElementsAllowingEmptyValuesMutable.addAll(c);
}
c = bn.elementsPreservingWhiteSpace();
if (c.size() > 0) {
_mappingStuff.mElementsPreservingWhiteSpaceMutable.addAll(c);
}
_mappingStuff.mBundleRegistrationStatus.put(rootNodeKey, Boolean.TRUE);
}
@Override
public InputSource resolveEntity(String publicID, String systemID) throws SAXException {
try {
if(DOLUtils.getDefaultLogger().isLoggable(Level.FINE)) {
DOLUtils.getDefaultLogger().fine("Asked to resolve " + publicID + " system id = " + 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))));
} else {
// 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 fileName = null;
String namespaceResolution = resolveSchemaNamespace(systemID);
if (namespaceResolution != null) {
fileName = getSchemaURLFor(namespaceResolution);
} else {
fileName = getSchemaURLFor(systemID.substring(systemID.lastIndexOf('/')+1));
}
// if this is not a request for a schema located in our repository, we fail the deployment
if (fileName == null) {
throw new SAXException(localStrings.getLocalString(
"invalid.schema",
"Requested schema is not found in local repository, please ensure that there are no typos in the XML namespace declaration."));
}
if (DOLUtils.getDefaultLogger().isLoggable(Level.FINE)) {
DOLUtils.getDefaultLogger().fine("Resolved to " + fileName);
}
return new InputSource(fileName);
}
} catch (SAXException e) {
DOLUtils.getDefaultLogger().log(Level.SEVERE, e.getMessage(), e);
throw e;
} catch(Exception ioe) {
DOLUtils.getDefaultLogger().log(Level.SEVERE, ioe.getMessage(), 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 {
DOLUtils.getDefaultLogger().log(Level.SEVERE, DOLUtils.INVALILD_DESCRIPTOR,
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 {
DOLUtils.getDefaultLogger().log(Level.SEVERE, DOLUtils.INVALILD_DESCRIPTOR,
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) {
DOLUtils.getDefaultLogger().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 f.toURI().toURL().toString();
} else {
return null;
}
}
/**
* @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 {
if (DOLUtils.getDefaultLogger().isLoggable(Level.FINE)) {
DOLUtils.getDefaultLogger().fine("Getting Schema " + schemaSystemID);
}
String schemaLoc = DTDRegistry.SCHEMA_LOCATION.replace('/', File.separatorChar);
File f = new File(schemaLoc +File.separatorChar+ schemaSystemID);
if (!f.exists()) {
DOLUtils.getDefaultLogger().fine("Cannot find schema " + schemaSystemID);
return null;
}
return f;
}
/**
* 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<String> 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<String> 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 (DOLUtils.getDefaultLogger().isLoggable(Level.FINE)) {
DOLUtils.getDefaultLogger().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) {
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 = (String) 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<String, String> matchXPath = versionUpgrade.getMatchXPath();
int entriesMatched = 0;
for (Map.Entry<String, String> entry : matchXPath.entrySet()) {
if (entry.getKey().equals(lastElement)) {
entry.setValue(elementData.toString());
++entriesMatched;
}
}
if (entriesMatched == matchXPath.size()) {
doDelete = true;
break;
}
}
}
}
if (DOLUtils.getDefaultLogger().isLoggable(Level.FINER)) {
DOLUtils.getDefaultLogger().finer("start of element " + uri + " with local name "+ localName + " and " + qName);
}
XMLNode node = null;
elementData = new StringBuffer();
if (nodes.isEmpty()) {
// this must be a root element...
Class rootNodeClass = _mappingStuff.mRootNodes.get(localName);
if (rootNodeClass == null) {
DOLUtils.getDefaultLogger().log(Level.SEVERE, DOLUtils.INVALID_DESC_MAPPING,
new Object[] {localName, " not supported !"});
if (stopOnXMLErrors) {
throw new IllegalArgumentException(localStrings.getLocalString("invalid.root.element",
"{0} Element [{1}] is not a valid root element",
new Object[] {errorReportingString, localName}));
}
} else {
try {
node = (XMLNode) rootNodeClass.newInstance();
if (DOLUtils.getDefaultLogger().isLoggable(Level.FINE)) {
DOLUtils.getDefaultLogger().fine("Instanciating " + node);
}
if (node instanceof RootXMLNode) {
if (publicID != null) {
((RootXMLNode) node).setDocType(publicID);
}
addPrefixMapping(node);
}
nodes.add(node);
topNode = node;
node.getDescriptor();
} catch (Exception e) {
DOLUtils.getDefaultLogger().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 (DOLUtils.getDefaultLogger().isLoggable(Level.FINE)) {
DOLUtils.getDefaultLogger().fine("Asking for new handler for " + element + " to " + node);
}
XMLNode newNode = node.getHandlerFor(element);
if (DOLUtils.getDefaultLogger().isLoggable(Level.FINE)) {
DOLUtils.getDefaultLogger().fine("Got " + 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 = (String)elementStack.peek();
} catch (EmptyStackException ex) {
}
if(DOLUtils.getDefaultLogger().isLoggable(Level.FINER)) {
DOLUtils.getDefaultLogger().finer("End of element " + uri + " local name "+ localName + " and " + qName + " value " + elementData);
}
if (nodes.size()==0) {
// no more nodes to pop
elementData=null;
return;
}
XMLElement element = new XMLElement(qName, namespaces);
XMLNode topNode = nodes.get(nodes.size()-1);
if (elementData != null && (elementData.length() != 0 || allowsEmptyValue(element.getQName()))) {
if (DOLUtils.getDefaultLogger().isLoggable(Level.FINER)) {
DOLUtils.getDefaultLogger().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<String, String> matchXPath = versionUpgrade.getMatchXPath();
int entriesMatched = 0;
for (Map.Entry<String, String> 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 {
StringBuffer buf = new StringBuffer();
String errorString = "Invalid upgrade from <";
buf.append(errorString);
for (Map.Entry<String, String> entry : matchXPath.entrySet()) {
buf.append(entry.getKey() + " " + entry.getValue() + " >");
}
errorString = buf.toString();
DOLUtils.getDefaultLogger().log(Level.SEVERE, errorString);
// 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 != null && 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 (envProp.getType() != null && (envProp.getType().equals("java.lang.String")
|| envProp.getType().equals("java.lang.Character"))) {
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_STR.equalsIgnoreCase(val)) {
topNode.setElementValue(element, val.toLowerCase(Locale.US));
} else if (FALSE_STR.equalsIgnoreCase(val)) {
topNode.setElementValue(element, val.toLowerCase(Locale.US));
} else {
topNode.setElementValue(element, val);
}
}
elementData = null;
}
if (topNode.endElement(element)) {
if (DOLUtils.getDefaultLogger().isLoggable(Level.FINE)) {
DOLUtils.getDefaultLogger().fine("Removing top node " + topNode);
}
nodes.remove(nodes.size()-1);
}
namespaces.popContext();
pushedNamespaceContext=false;
try {
lastElement = (String) 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<String, String> entry : prefixMapping.entrySet()) {
node.addPrefixMapping(entry.getKey(), entry.getValue());
}
prefixMapping = null;
}
}
private String errorReportingString = "";
/**
* Sets the error reporting context string
*/
public void setErrorReportingString(String s) {
errorReportingString = s;
}
/**
* Indicates whether the element name is one for which empty values should
* be recorded.
* <p>
* 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.
* <p>
* 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);
}
}