/*
* Copyright (c) 1997, 2017, 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.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.net.URL;
import java.text.MessageFormat;
import java.util.Stack;
import java.util.regex.Pattern;
NGCCRuntime extended with various utility methods for
parsing XML Schema.
Author: Kohsuke Kawaguchi (kohsuke.kawaguchi@sun.com)
/**
* 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. /** coordinator. */
public final ParserContext parser;
The schema currently being parsed. /** The schema currently being parsed. */
public SchemaImpl currentSchema;
The @finalDefault value of the current schema. /** The @finalDefault value of the current schema. */
public int finalDefault = 0;
The @blockDefault value of the current schema. /** 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.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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
/**
* 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.
/**
* Points to the schema document (the parser of it) that included/imported
* this schema.
*/
private final NGCCRuntimeEx referer;
Points to the SchemaDocumentImpl
that represents the schema document being parsed. /**
* 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. */
@Override
public void addPatcher( Patch patcher ) {
parser.patcherManager.addPatcher(patcher);
}
@Override
public void addErrorChecker( Patch patcher ) {
parser.patcherManager.addErrorChecker(patcher);
}
@Override
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.
Params: - namespaceURI –
passed to the entity resolver.
- relativeUri –
value of the schemaLocation attribute. Can be null.
Returns: non-null if EntityResolver
returned an InputSource
, or if the relativeUri parameter seems to be pointing to something. Otherwise it returns null, in which case import/include should be abandoned.
/**
* 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) {
if (isAbsolute(relativeUri)) {
systemId = relativeUri;
}
if (baseUri == null || !isAbsolute(baseUri)) {
throw new IOException("Unable to resolve relative URI " + relativeUri + " because base URI is not absolute: " + baseUri);
}
systemId = new URL(new URL(baseUri), relativeUri).toString();
}
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;
}
}
private static final Pattern P = Pattern.compile(".*[/#?].*");
private static boolean isAbsolute(String uri) {
int i = uri.indexOf(':');
if (i < 0) {
return false;
}
return !P.matcher(uri.substring(0, i)).matches();
}
Includes the specified schema.
Params: - schemaLocation –
Throws:
/**
* Includes the specified schema.
*
* @param schemaLocation
* @throws org.xml.sax.SAXException */
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.
Params: - ns –
- schemaLocation –
Throws:
/**
* Imports the specified schema.
*
* @param ns
* @param schemaLocation
* @throws org.xml.sax.SAXException */
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.
Used to avoid recursive inclusion. Note that the same
document will be parsed multiple times if they are for different
target namespaces.
Document Graph Model
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.
Our goal here is to parse all the documents in the graph, without
parsing the same document twice. This method implements this check.
One complication is the chameleon schema; a document can be parsed
multiple times if they are under different target namespaces.
Also, note that when you resolve relative URIs in the @schemaLocation,
their base URI is *NOT* the URI of the document.
Returns: true if the document has already been processed and thus
needs to be skipped.
/**
* 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.
Params: - source –
- importLocation –
The source location of the import/include statement.
Used for reporting errors.
- includeMode –
- expectedNamespace –
Throws:
/**
* Parses the specified entity.
*
* @param source
* @param importLocation
* The source location of the import/include statement.
* Used for reporting errors.
* @param includeMode
* @param expectedNamespace
* @throws org.xml.sax.SAXException
*/
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.
Returns: Annotation parser
/**
* Creates a new instance of annotation parser.
*
* @return 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.
Returns: Element name
/**
* Gets the element name that contains the annotation element.This method works correctly only when called by the annotation handler.
*
* @return Element name
*/
public String getAnnotationContextElementName() {
return elementNames.get( elementNames.size()-2 );
}
Creates a copy of the current locator object.
Returns: Locator copy
/**
* Creates a copy of the current locator object.
*
* @return Locator copy
*/
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;
}
@Override
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.
@Override
public String getBaseUri() { return null; }
@Override
public boolean isNotation(String arg0) { return false; }
@Override
public boolean isUnparsedEntity(String arg0) { return false; }
}
private Context currentContext=null;
Returns an immutable snapshot of the current context.
Returns: Snapshot of current context
/** Returns an immutable snapshot of the current context.
*
* @return Snapshot of 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.
Params: - qname – Attribute name.
Throws: Returns: New UName
instance based on attribute name.
/**
* Parses UName under the given context.
* @param qname Attribute name.
* @return New {@link UName} instance based on attribute name.
* @throws org.xml.sax.SAXException
*/
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
Params: - text – String where whitespaces should be collapsed
Returns: String with whitespaces collapsed
/**
* 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.
/**
* 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.
/**
* 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";
}