blob: 4e600c32e558e89b81e2a63b6b6d9151c52c23b8 [file] [log] [blame]
/*
* Copyright (c) 1997, 2016, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.xml.internal.xsom.impl.parser;
import com.sun.xml.internal.xsom.XSDeclaration;
import com.sun.xml.internal.xsom.XmlString;
import com.sun.xml.internal.xsom.XSSimpleType;
import com.sun.xml.internal.xsom.impl.ForeignAttributesImpl;
import com.sun.xml.internal.xsom.impl.SchemaImpl;
import com.sun.xml.internal.xsom.impl.UName;
import com.sun.xml.internal.xsom.impl.Const;
import com.sun.xml.internal.xsom.impl.parser.state.NGCCRuntime;
import com.sun.xml.internal.xsom.impl.parser.state.Schema;
import com.sun.xml.internal.xsom.impl.util.Uri;
import com.sun.xml.internal.xsom.parser.AnnotationParser;
import com.sun.xml.internal.org.relaxng.datatype.ValidationContext;
import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.LocatorImpl;
import java.io.IOException;
import java.net.URI;
import java.text.MessageFormat;
import java.util.Stack;
/**
* NGCCRuntime extended with various utility methods for
* parsing XML Schema.
*
* @author Kohsuke Kawaguchi (kohsuke.kawaguchi@sun.com)
*/
public class NGCCRuntimeEx extends NGCCRuntime implements PatcherManager {
/** coordinator. */
public final ParserContext parser;
/** The schema currently being parsed. */
public SchemaImpl currentSchema;
/** The @finalDefault value of the current schema. */
public int finalDefault = 0;
/** The @blockDefault value of the current schema. */
public int blockDefault = 0;
/**
* The @elementFormDefault value of the current schema.
* True if local elements are qualified by default.
*/
public boolean elementFormDefault = false;
/**
* The @attributeFormDefault value of the current schema.
* True if local attributes are qualified by default.
*/
public boolean attributeFormDefault = false;
/**
* True if the current schema is in a chameleon mode.
* This changes the way QNames are interpreted.
*
* Life is very miserable with XML Schema, as you see.
*/
public boolean chameleonMode = false;
/**
* URI that identifies the schema document.
* Maybe null if the system ID is not available.
*/
private String documentSystemId;
/**
* Keep the local name of elements encountered so far.
* This information is passed to AnnotationParser as
* context information
*/
private final Stack<String> elementNames = new Stack<String>();
/**
* Points to the schema document (the parser of it) that included/imported
* this schema.
*/
private final NGCCRuntimeEx referer;
/**
* Points to the {@link SchemaDocumentImpl} that represents the
* schema document being parsed.
*/
public SchemaDocumentImpl document;
NGCCRuntimeEx( ParserContext _parser ) {
this(_parser,false,null);
}
private NGCCRuntimeEx( ParserContext _parser, boolean chameleonMode, NGCCRuntimeEx referer ) {
this.parser = _parser;
this.chameleonMode = chameleonMode;
this.referer = referer;
// set up the default namespace binding
currentContext = new Context("","",null);
currentContext = new Context("xml","http://www.w3.org/XML/1998/namespace",currentContext);
}
public void checkDoubleDefError( XSDeclaration c ) throws SAXException {
if(c==null || ignorableDuplicateComponent(c)) return;
reportError( Messages.format(Messages.ERR_DOUBLE_DEFINITION,c.getName()) );
reportError( Messages.format(Messages.ERR_DOUBLE_DEFINITION_ORIGINAL), c.getLocator() );
}
public static boolean ignorableDuplicateComponent(XSDeclaration c) {
if(c.getTargetNamespace().equals(Const.schemaNamespace)) {
if(c instanceof XSSimpleType)
// hide artificial "double definitions" on simple types
return true;
if(c.isGlobal() && c.getName().equals("anyType"))
return true; // ditto for anyType
}
return false;
}
/* registers a patcher that will run after all the parsing has finished. */
public void addPatcher( Patch patcher ) {
parser.patcherManager.addPatcher(patcher);
}
public void addErrorChecker( Patch patcher ) {
parser.patcherManager.addErrorChecker(patcher);
}
public void reportError( String msg, Locator loc ) throws SAXException {
parser.patcherManager.reportError(msg,loc);
}
public void reportError( String msg ) throws SAXException {
reportError(msg,getLocator());
}
/**
* Resolves relative URI found in the document.
*
* @param namespaceURI
* passed to the entity resolver.
* @param relativeUri
* value of the schemaLocation attribute. Can be null.
*
* @return
* non-null if {@link EntityResolver} returned an {@link InputSource},
* or if the relativeUri parameter seems to be pointing to something.
* Otherwise it returns null, in which case import/include should be abandoned.
*/
private InputSource resolveRelativeURL( String namespaceURI, String relativeUri ) throws SAXException {
try {
String baseUri = getLocator().getSystemId();
if(baseUri==null)
// if the base URI is not available, the document system ID is
// better than nothing.
baseUri=documentSystemId;
EntityResolver er = parser.getEntityResolver();
String systemId = null;
if (relativeUri!=null)
systemId = Uri.resolve(baseUri,relativeUri);
if (er!=null) {
InputSource is = er.resolveEntity(namespaceURI,systemId);
if (is == null) {
try {
String normalizedSystemId = URI.create(systemId).normalize().toASCIIString();
is = er.resolveEntity(namespaceURI,normalizedSystemId);
} catch (Exception e) {
// just ignore, this is a second try, return the fallback if this breaks
}
}
if (is != null) {
return is;
}
}
if (systemId!=null)
return new InputSource(systemId);
else
return null;
} catch (IOException e) {
SAXParseException se = new SAXParseException(e.getMessage(),getLocator(),e);
parser.errorHandler.error(se);
return null;
}
}
/** Includes the specified schema. */
public void includeSchema( String schemaLocation ) throws SAXException {
NGCCRuntimeEx runtime = new NGCCRuntimeEx(parser,chameleonMode,this);
runtime.currentSchema = this.currentSchema;
runtime.blockDefault = this.blockDefault;
runtime.finalDefault = this.finalDefault;
if( schemaLocation==null ) {
SAXParseException e = new SAXParseException(
Messages.format( Messages.ERR_MISSING_SCHEMALOCATION ), getLocator() );
parser.errorHandler.fatalError(e);
throw e;
}
runtime.parseEntity( resolveRelativeURL(null,schemaLocation),
true, currentSchema.getTargetNamespace(), getLocator() );
}
/** Imports the specified schema. */
public void importSchema( String ns, String schemaLocation ) throws SAXException {
NGCCRuntimeEx newRuntime = new NGCCRuntimeEx(parser,false,this);
InputSource source = resolveRelativeURL(ns,schemaLocation);
if(source!=null)
newRuntime.parseEntity( source, false, ns, getLocator() );
// if source == null,
// we can't locate this document. Let's just hope that
// we already have the schema components for this schema
// or we will receive them in the future.
}
/**
* Called when a new document is being parsed and checks
* if the document has already been parsed before.
*
* <p>
* Used to avoid recursive inclusion. Note that the same
* document will be parsed multiple times if they are for different
* target namespaces.
*
* <h2>Document Graph Model</h2>
* <p>
* The challenge we are facing here is that you have a graph of
* documents that reference each other. Each document has an unique
* URI to identify themselves, and references are done by using those.
* The graph may contain cycles.
*
* <p>
* Our goal here is to parse all the documents in the graph, without
* parsing the same document twice. This method implements this check.
*
* <p>
* One complication is the chameleon schema; a document can be parsed
* multiple times if they are under different target namespaces.
*
* <p>
* Also, note that when you resolve relative URIs in the @schemaLocation,
* their base URI is *NOT* the URI of the document.
*
* @return true if the document has already been processed and thus
* needs to be skipped.
*/
public boolean hasAlreadyBeenRead() {
if( documentSystemId!=null ) {
if( documentSystemId.startsWith("file:///") )
// change file:///abc to file:/abc
// JDK File.toURL method produces the latter, but according to RFC
// I don't think that's a valid URL. Since two different ways of
// producing URLs could produce those two different forms,
// we need to canonicalize one to the other.
documentSystemId = "file:/"+documentSystemId.substring(8);
} else {
// if the system Id is not provided, we can't test the identity,
// so we have no choice but to read it.
// the newly created SchemaDocumentImpl will be unique one
}
assert document ==null;
document = new SchemaDocumentImpl( currentSchema, documentSystemId );
SchemaDocumentImpl existing = parser.parsedDocuments.get(document);
if(existing==null) {
parser.parsedDocuments.put(document,document);
} else {
document = existing;
}
assert document !=null;
if(referer!=null) {
assert referer.document !=null : "referer "+referer.documentSystemId+" has docIdentity==null";
referer.document.references.add(this.document);
this.document.referers.add(referer.document);
}
return existing!=null;
}
/**
* Parses the specified entity.
*
* @param importLocation
* The source location of the import/include statement.
* Used for reporting errors.
*/
public void parseEntity( InputSource source, boolean includeMode, String expectedNamespace, Locator importLocation )
throws SAXException {
documentSystemId = source.getSystemId();
try {
Schema s = new Schema(this,includeMode,expectedNamespace);
setRootHandler(s);
try {
parser.parser.parse(source,this, getErrorHandler(), parser.getEntityResolver());
} catch( IOException fnfe ) {
SAXParseException se = new SAXParseException(fnfe.toString(), importLocation, fnfe);
parser.errorHandler.warning(se);
}
} catch( SAXException e ) {
parser.setErrorFlag();
throw e;
}
}
/**
* Creates a new instance of annotation parser.
*/
public AnnotationParser createAnnotationParser() {
if(parser.getAnnotationParserFactory()==null)
return DefaultAnnotationParser.theInstance;
else
return parser.getAnnotationParserFactory().create();
}
/**
* Gets the element name that contains the annotation element.
* This method works correctly only when called by the annotation handler.
*/
public String getAnnotationContextElementName() {
return elementNames.get( elementNames.size()-2 );
}
/** Creates a copy of the current locator object. */
public Locator copyLocator() {
return new LocatorImpl(getLocator());
}
public ErrorHandler getErrorHandler() {
return parser.errorHandler;
}
@Override
public void onEnterElementConsumed(String uri, String localName, String qname, Attributes atts)
throws SAXException {
super.onEnterElementConsumed(uri, localName, qname, atts);
elementNames.push(localName);
}
@Override
public void onLeaveElementConsumed(String uri, String localName, String qname) throws SAXException {
super.onLeaveElementConsumed(uri, localName, qname);
elementNames.pop();
}
//
//
// ValidationContext implementation
//
//
// this object lives longer than the parser itself,
// so it's important for this object not to have any reference
// to the parser.
private static class Context implements ValidationContext {
Context( String _prefix, String _uri, Context _context ) {
this.previous = _context;
this.prefix = _prefix;
this.uri = _uri;
}
public String resolveNamespacePrefix(String p) {
if(p.equals(prefix)) return uri;
if(previous==null) return null;
else return previous.resolveNamespacePrefix(p);
}
private final String prefix;
private final String uri;
private final Context previous;
// XSDLib don't use those methods, so we cut a corner here.
public String getBaseUri() { return null; }
public boolean isNotation(String arg0) { return false; }
public boolean isUnparsedEntity(String arg0) { return false; }
}
private Context currentContext=null;
/** Returns an immutable snapshot of the current context. */
public ValidationContext createValidationContext() {
return currentContext;
}
public XmlString createXmlString(String value) {
if(value==null) return null;
else return new XmlString(value,createValidationContext());
}
@Override
public void startPrefixMapping( String prefix, String uri ) throws SAXException {
super.startPrefixMapping(prefix,uri);
currentContext = new Context(prefix,uri,currentContext);
}
@Override
public void endPrefixMapping( String prefix ) throws SAXException {
super.endPrefixMapping(prefix);
currentContext = currentContext.previous;
}
//
//
// Utility functions
//
//
/**
* Parses UName under the given context.
* @param qname Attribute name.
* @return New {@link UName} instance based on attribute name.
*/
public UName parseUName(final String qname ) throws SAXException {
int idx = qname.indexOf(':');
if(idx<0) {
String uri = resolveNamespacePrefix("");
// chamelon behavior. ugly...
if( uri.equals("") && chameleonMode )
uri = currentSchema.getTargetNamespace();
// this is guaranteed to resolve
return new UName(uri,qname,qname);
} else {
String prefix = qname.substring(0,idx);
String uri = currentContext.resolveNamespacePrefix(prefix);
if(uri==null) {
// prefix failed to resolve.
reportError(Messages.format(
Messages.ERR_UNDEFINED_PREFIX,prefix));
uri="undefined"; // replace with a dummy
}
return new UName( uri, qname.substring(idx+1), qname );
}
}
/**
* Utility function for collapsing the namespaces inside qname declarations
* and 'name' attribute values that should contain the qname values
*
* @param text String where whitespaces should be collapsed
* @return String with whitespaces collapsed
*/
public String collapse(String text) {
return collapse((CharSequence) text).toString();
}
/**
* returns true if the specified char is a white space character.
*/
private final boolean isWhiteSpace(char ch) {
// most of the characters are non-control characters.
// so check that first to quickly return false for most of the cases.
if (ch > 0x20) {
return false;
}
// other than we have to do four comparisons.
return ch == 0x9 || ch == 0xA || ch == 0xD || ch == 0x20;
}
/**
* This is usually the biggest processing bottleneck.
*
*/
private CharSequence collapse(CharSequence text) {
int len = text.length();
// most of the texts are already in the collapsed form.
// so look for the first whitespace in the hope that we will
// never see it.
int s = 0;
while (s < len) {
if (isWhiteSpace(text.charAt(s))) {
break;
}
s++;
}
if (s == len) // the input happens to be already collapsed.
{
return text;
}
// we now know that the input contains spaces.
// let's sit down and do the collapsing normally.
StringBuilder result = new StringBuilder(len /*allocate enough size to avoid re-allocation*/);
if (s != 0) {
for (int i = 0; i < s; i++) {
result.append(text.charAt(i));
}
result.append(' ');
}
boolean inStripMode = true;
for (int i = s + 1; i < len; i++) {
char ch = text.charAt(i);
boolean b = isWhiteSpace(ch);
if (inStripMode && b) {
continue; // skip this character
}
inStripMode = b;
if (inStripMode) {
result.append(' ');
} else {
result.append(ch);
}
}
// remove trailing whitespaces
len = result.length();
if (len > 0 && result.charAt(len - 1) == ' ') {
result.setLength(len - 1);
}
// whitespaces are already collapsed,
// so all we have to do is to remove the last one character
// if it's a whitespace.
return result;
}
public boolean parseBoolean(String v) {
if(v==null) return false;
v=v.trim();
return v.equals("true") || v.equals("1");
}
@Override
protected void unexpectedX(String token) throws SAXException {
SAXParseException e = new SAXParseException(MessageFormat.format(
"Unexpected {0} appears at line {1} column {2}",
token,
getLocator().getLineNumber(),
getLocator().getColumnNumber()),
getLocator());
parser.errorHandler.fatalError(e);
throw e; // we will abort anyway
}
public ForeignAttributesImpl parseForeignAttributes( ForeignAttributesImpl next ) {
ForeignAttributesImpl impl = new ForeignAttributesImpl(createValidationContext(),copyLocator(),next);
Attributes atts = getCurrentAttributes();
for( int i=0; i<atts.getLength(); i++ ) {
if(atts.getURI(i).length()>0) {
impl.addAttribute(
atts.getURI(i),
atts.getLocalName(i),
atts.getQName(i),
atts.getType(i),
atts.getValue(i)
);
}
}
return impl;
}
public static final String XMLSchemaNSURI = "http://www.w3.org/2001/XMLSchema";
}