net.sf.saxon.query.XQueryParser Maven / Gradle / Ivy
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2015 Saxonica Limited.
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package net.sf.saxon.query;
import net.sf.saxon.Configuration;
import net.sf.saxon.event.PipelineConfiguration;
import net.sf.saxon.expr.*;
import net.sf.saxon.expr.flwor.*;
import net.sf.saxon.expr.instruct.*;
import net.sf.saxon.expr.parser.*;
import net.sf.saxon.expr.sort.GenericAtomicComparer;
import net.sf.saxon.expr.sort.SortKeyDefinition;
import net.sf.saxon.functions.*;
import net.sf.saxon.lib.*;
import net.sf.saxon.om.*;
import net.sf.saxon.pattern.*;
import net.sf.saxon.serialize.CharacterMap;
import net.sf.saxon.serialize.CharacterMapIndex;
import net.sf.saxon.serialize.SerializationParamsHandler;
import net.sf.saxon.serialize.charcode.UTF16CharacterSet;
import net.sf.saxon.style.AttributeValueTemplate;
import net.sf.saxon.trace.LocationKind;
import net.sf.saxon.trans.DecimalFormatManager;
import net.sf.saxon.trans.DecimalSymbols;
import net.sf.saxon.trans.Err;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.tree.util.FastStringBuffer;
import net.sf.saxon.tree.util.NamespaceResolverWithDefault;
import net.sf.saxon.type.*;
import net.sf.saxon.value.*;
import net.sf.saxon.z.IntHashSet;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamSource;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.regex.Pattern;
/**
* This class defines extensions to the XPath parser to handle the additional
* syntax supported in XQuery
*/
public class XQueryParser extends XPathParser {
public final static String XQUERY10 = "1.0";
public final static String XQUERY30 = "3.0";
public final static String XQUERY31 = "3.1";
private boolean memoFunction = false;
private boolean disableCycleChecks = false;
private boolean streaming = false;
/*@Nullable*/ protected String queryVersion = null;
private int errorCount = 0;
/*@Nullable*/ private XPathException firstError = null;
/*@NotNull*/ protected Executable executable;
private boolean foundCopyNamespaces = false;
private boolean foundBoundarySpaceDeclaration = false;
private boolean foundOrderingDeclaration = false;
private boolean foundEmptyOrderingDeclaration = false;
private boolean foundDefaultCollation = false;
private boolean foundConstructionDeclaration = false;
private boolean foundDefaultFunctionNamespace = false;
private boolean foundDefaultElementNamespace = false;
private boolean foundBaseURIDeclaration = false;
private boolean foundContextItemDeclaration = false;
private boolean foundDefaultDecimalFormat = false;
private boolean preambleProcessed = false;
/*@NotNull*/ public Set importedModules = new HashSet(5);
/*@NotNull*/ List namespacesToBeSealed = new ArrayList(10);
/*@NotNull*/ List schemaImports = new ArrayList(5);
/*@NotNull*/ List moduleImports = new ArrayList(5);
private Set outputPropertiesSeen = new HashSet(4);
/**
* Constructor for internal use: this class should be instantiated via the QueryModule
*/
public XQueryParser() {
queryVersion = XQUERY30;
}
/**
* Create a new parser of the same kind
*
* @return a new parser of the same kind as this one
*/
public XQueryParser newParser() {
XQueryParser qp = new XQueryParser();
qp.setLanguage(language, languageVersion);
return qp;
}
/**
* Create an XQueryExpression
*
* @param query the source text of the query
* @param mainModule the static context of the query
* @param config the Saxon configuration
* @return the compiled XQuery expression
* @throws XPathException if the expression contains static errors
*/
/*@NotNull*/
public XQueryExpression makeXQueryExpression(
/*@NotNull*/ String query,
/*@NotNull*/ QueryModule mainModule,
/*@NotNull*/ Configuration config) throws XPathException {
try {
setLanguage(XQUERY, mainModule.getLanguageVersion());
if (config.getXMLVersion() == Configuration.XML10) {
query = normalizeLineEndings10(query);
} else {
query = normalizeLineEndings11(query);
}
Executable exec = mainModule.getExecutable();
if (exec == null) {
exec = new Executable(config);
exec.setHostLanguage(Configuration.XQUERY, mainModule.getLanguageVersion() >= 30);
exec.setTopLevelPackage(mainModule.getPackageData());
setExecutable(exec);
//mainModule.setExecutable(exec);
}
// setDefaultContainer(new SimpleContainer(mainModule.getPackageData()));
Properties outputProps = new Properties(config.getDefaultSerializationProperties());
if (outputProps.getProperty(OutputKeys.METHOD) == null) {
outputProps.setProperty(OutputKeys.METHOD, "xml");
}
exec.setDefaultOutputProperties(outputProps);
//exec.setLocationMap(new LocationMap());
FunctionLibraryList libList = new FunctionLibraryList();
libList.addFunctionLibrary(new ExecutableFunctionLibrary(config));
exec.setFunctionLibrary(libList);
// this will be changed later
setExecutable(exec);
setCodeInjector(mainModule.getCodeInjector());
Expression exp = parseQuery(query, 0, Token.EOF, mainModule);
if (streaming) {
env.getConfiguration().checkLicensedFeature(Configuration.LicenseFeature.ENTERPRISE_XQUERY, "streaming", -1);
}
exec.fixupQueryModules(mainModule, !disableCycleChecks);
// Check for cyclic dependencies among the modules
// if (!disableCycleChecks) {
// Iterator miter = exec.getQueryLibraryModules();
// while (miter.hasNext()) {
// QueryModule module = (QueryModule)miter.next();
// module.lookForModuleCycles(new Stack(), 1);
// }
// }
// Make the XQueryExpression object
XQueryExpression queryExp = config.makeXQueryExpression(exp, mainModule, streaming);
// Make the function library that's available at run-time (e.g. for saxon:evaluate() and function-lookup()).
// This includes all user-defined functions regardless of which module they are in
FunctionLibrary userlib = exec.getFunctionLibrary();
FunctionLibraryList lib = new FunctionLibraryList();
lib.addFunctionLibrary(
SystemFunctionLibrary.getSystemFunctionLibrary(getPermittedFunctions(), config));
lib.addFunctionLibrary(config.getVendorFunctionLibrary());
lib.addFunctionLibrary(new ConstructorFunctionLibrary(config));
lib.addFunctionLibrary(config.getIntegratedFunctionLibrary());
lib.addFunctionLibrary(mainModule.getGlobalFunctionLibrary());
config.addExtensionBinders(lib);
lib.addFunctionLibrary(userlib);
exec.setFunctionLibrary(lib);
return queryExp;
} catch (XPathException e) {
if (!e.hasBeenReported()) {
reportError(e);
}
throw e;
}
}
/**
* Get the permitted set of standard functions in this environment
*
* @return a code indicating which system library functions are supported in this version of the language
*/
public int getPermittedFunctions() {
int allowed = StandardFunction.CORE;
if (allowXPath30Syntax) {
allowed |= StandardFunction.XPATH30;
}
return allowed;
}
/**
* Check that an expression is streamable
* @param exp the expression to be checked
* @param contextInfo information about the context item
* @throws XPathException if the expression is not streamable
*/
public void checkStreamability(Expression exp, ContextItemStaticInfo contextInfo) throws XPathException {
throw new XPathException("Streaming requires Saxon-EE");
}
/**
* Normalize line endings in the source query, according to the XML 1.1 rules.
*
* @param in the input query
* @return the query with line endings normalized
*/
private static String normalizeLineEndings11(/*@NotNull*/ String in) {
if (in.indexOf((char) 0xd) < 0 && in.indexOf((char) 0x85) < 0 && in.indexOf((char) 0x2028) < 0) {
return in;
}
FastStringBuffer sb = new FastStringBuffer(in.length());
for (int i = 0; i < in.length(); i++) {
char ch = in.charAt(i);
switch (ch) {
case 0x85:
case 0x2028:
sb.append((char) 0xa);
break;
case 0xd:
if (i < in.length() - 1 && (in.charAt(i + 1) == (char) 0xa || in.charAt(i + 1) == (char) 0x85)) {
sb.append((char) 0xa);
i++;
} else {
sb.append((char) 0xa);
}
break;
default:
sb.append(ch);
}
}
return sb.toString();
}
/**
* Normalize line endings in the source query, according to the XML 1.0 rules.
*
* @param in the input query
* @return the query text with line endings normalized
*/
private static String normalizeLineEndings10(/*@NotNull*/ String in) {
if (in.indexOf((char) 0xd) < 0) {
return in;
}
FastStringBuffer sb = new FastStringBuffer(in.length());
for (int i = 0; i < in.length(); i++) {
char ch = in.charAt(i);
switch (ch) {
case 0xd:
if (i < in.length() - 1 && in.charAt(i + 1) == (char) 0xa) {
sb.append((char) 0xa);
i++;
} else {
sb.append((char) 0xa);
}
break;
default:
sb.append(ch);
}
}
return sb.toString();
}
/**
* Get the executable containing this expression.
*
* @return the executable
*/
/*@NotNull*/
public Executable getExecutable() {
return executable;
}
/**
* Set the executable used for this query expression
*
* @param exec the executable
*/
public void setExecutable(/*@NotNull*/ Executable exec) {
executable = exec;
}
/**
* Disable checks for certain kinds of cycle. This is equivalent to
* declare option saxon:allow-cycles "true"
*
* @param disable true if checks for import cycles are to be suppressed, that is,
* if cycles should be allowed
*/
public void setDisableCycleChecks(boolean disable) {
disableCycleChecks = disable;
}
/**
* Callback to tailor the tokenizer
*/
protected void customizeTokenizer(Tokenizer t) {
t.isXQuery = true;
}
/**
* Say whether the query should be compiled and evaluated to use streaming.
* This affects subsequent calls on the parseQuery() method. This option requires
* Saxon-EE.
* @param option if true, the compiler will attempt to compile a query to be
* capable of executing in streaming mode. If the query cannot be streamed,
* a compile-time exception is reported. In streaming mode, the source
* document is supplied as a stream, and no tree is built in memory. The default
* is false.
* @since 9.6
*/
public void setStreaming(boolean option) {
streaming = option;
}
/**
* Ask whether the streaming option has been set, that is, whether
* subsequent calls on parseQuery() will compile queries to be capable
* of executing in streaming mode.
* @return true if the streaming option has been set.
* @since 9.6
*/
public boolean isStreaming() {
return streaming;
}
/**
* Parse a top-level Query.
* Prolog? Expression
*
* @param queryString The text of the query
* @param start Offset of the start of the query
* @param terminator Token expected to follow the query (usually Token.EOF)
* @param env The static context
* @return the Expression object that results from parsing
* @throws net.sf.saxon.trans.XPathException
* if the expression contains a syntax error
*/
/*@NotNull*/
private Expression parseQuery(String queryString,
int start,
int terminator,
/*@NotNull*/ QueryModule env) throws XPathException {
this.env = env;
charChecker = env.getConfiguration().getValidCharacterChecker();
// if (defaultContainer == null) {
// defaultContainer = new TemporaryContainer(env.getConfiguration(), env.getLocationMap(), 1);
// }
language = XQUERY;
t = new Tokenizer();
t.languageLevel = env.getXPathVersion();
t.isXQuery = true;
try {
t.tokenize(queryString, start, -1);
} catch (XPathException err) {
grumble(err.getMessage());
}
parseVersionDeclaration();
allowXPath30Syntax = XQUERY30.equals(queryVersion) || XQUERY31.equals(queryVersion);
allowXPath31Syntax = XQUERY31.equals(queryVersion);
t.languageLevel = allowXPath30Syntax ? 30 : 20;
QNameParser qp = new QNameParser(env.getLiveNamespaceResolver());
qp.setAcceptEQName(allowXPath30Syntax);
setQNameParser(qp);
parseProlog();
processPreamble();
Expression exp = parseExpression();
// Diagnostic code - show the expression before any optimizations
// ExpressionPresenter ep = ExpressionPresenter.make(env.getConfiguration());
// exp.explain(ep);
// ep.close();
// End of diagnostic code
if (t.currentToken != terminator) {
grumble("Unexpected token " + currentTokenDisplay() + " beyond end of query");
}
setLocation(exp);
exp.setRetainedStaticContext(env.makeRetainedStaticContext());
if (errorCount == 0) {
return exp;
} else {
XPathException err = new XPathException("One or more static errors were reported during query analysis");
err.setHasBeenReported(true);
err.setErrorCodeQName(firstError.getErrorCodeQName()); // largely for the XQTS test driver
throw err;
}
}
/**
* Parse a library module.
* Prolog? Expression
*
* @param queryString The text of the library module.
* @param env The static context. The result of parsing
* a library module is that the static context is populated with a set of function
* declarations and variable declarations. Each library module must have its own
* static context objext.
* @throws XPathException if the expression contains a syntax error
*/
public final void parseLibraryModule(String queryString, /*@NotNull*/ QueryModule env)
throws XPathException {
this.env = env;
final Configuration config = env.getConfiguration();
charChecker = config.getValidCharacterChecker();
if (config.getXMLVersion() == Configuration.XML10) {
queryString = normalizeLineEndings10(queryString);
} else {
queryString = normalizeLineEndings11(queryString);
}
Executable exec = env.getExecutable();
if (exec == null) {
throw new IllegalStateException("Query library module has no associated Executable");
}
executable = exec;
// defaultContainer = new SimpleContainer(env.getPackageData());
t = new Tokenizer();
t.languageLevel = env.getXPathVersion();
t.isXQuery = true;
QNameParser qp = new QNameParser(env.getLiveNamespaceResolver());
qp.setAcceptEQName(allowXPath30Syntax);
setQNameParser(qp);
try {
t.tokenize(queryString, 0, -1);
} catch (XPathException err) {
grumble(err.getMessage());
}
parseVersionDeclaration();
//t.setAllowExpandedQNameSyntax("3.0".equals(queryVersion));
parseModuleDeclaration();
parseProlog();
processPreamble();
if (t.currentToken != Token.EOF) {
grumble("Unrecognized content found after the variable and function declarations in a library module");
}
if (errorCount != 0) {
XPathException err = new XPathException("Static errors were reported in the imported library module");
err.setErrorCodeQName(firstError.getErrorCodeQName());
throw err;
}
}
private void reportError(/*@NotNull*/ XPathException exception) throws XPathException {
errorCount++;
if (firstError == null) {
firstError = exception;
}
((QueryModule) env).reportStaticError(exception);
throw exception;
}
/**
* Make a Locator object representing the current parsing location
*
* @return a Locator
*/
/*@NotNull*/
// private ExplicitLocation makeLocator() {
// int line = t.getLineNumber();
// int column = t.getColumnNumber();
//
// return new ExplicitLocation(env.getSystemId(), line, column);
// }
/**
* Make a Locator object representing a specific parsing location
*
* @param offset the (coded) location in the input expression
* @return a Locator
*/
/*@NotNull*/
private ExplicitLocation makeLocator(int offset) {
int line = t.getLineNumber(offset);
int column = t.getColumnNumber(offset);
return new ExplicitLocation(env.getSystemId(), line, column);
}
private static Pattern encNamePattern = Pattern.compile("^[A-Za-z]([A-Za-z0-9._\\x2D])*$");
/**
* Parse the version declaration if present.
*
* @throws XPathException in the event of a syntax error.
*/
private void parseVersionDeclaration() throws XPathException {
if (t.currentToken == Token.XQUERY_VERSION) {
nextToken();
expect(Token.STRING_LITERAL);
queryVersion = unescape(t.currentTokenValue).toString();
if (XQUERY10.equals(queryVersion)) {
// no action
} else if (XQUERY30.equals(queryVersion) || "1.1".equals(queryVersion)) {
queryVersion = XQUERY30;
if (((QueryModule) env).getLanguageVersion() != 30 && ((QueryModule) env).getLanguageVersion() != 31) {
grumble("XQuery 3.0 was not enabled when invoking Saxon", "XQST0031");
queryVersion = XQUERY10;
}
} else if(XQUERY31.equals(queryVersion)) {
queryVersion = XQUERY31;
if (((QueryModule) env).getLanguageVersion() != 31) {
grumble("XQuery 3.1 was not enabled when invoking Saxon", "XQST0031");
queryVersion = XQUERY30;
}
} else {
grumble("Unsupported XQuery version " + queryVersion, "XQST0031");
queryVersion = XQUERY10;
}
nextToken();
if ("encoding".equals(t.currentTokenValue)) {
nextToken();
expect(Token.STRING_LITERAL);
if (!encNamePattern.matcher(unescape(t.currentTokenValue)).matches()) {
grumble("Encoding name contains invalid characters", "XQST0087");
}
// we ignore the encoding now: it was handled earlier, while decoding the byte stream
nextToken();
}
expect(Token.SEMICOLON);
nextToken();
} else {
if (((QueryModule) env).getLanguageVersion()== 30) {
queryVersion = XQUERY30;
} else if (((QueryModule) env).getLanguageVersion() == 31) {
queryVersion = XQUERY31;
} else {
queryVersion = XQUERY10;
}
if (t.currentToken == Token.XQUERY_ENCODING) {
if (!allowXPath30Syntax) {
grumble("XQuery 3.0 has not been enabled, so \"xquery\" must be followed by \"version\"");
}
nextToken();
expect(Token.STRING_LITERAL);
if (!encNamePattern.matcher(t.currentTokenValue).matches()) {
grumble("Encoding name contains invalid characters", "XQST0087");
}
// we ignore the encoding now: it was handled earlier, while decoding the byte stream
nextToken();
expect(Token.SEMICOLON);
nextToken();
}
}
}
/**
* In a library module, parse the module declaration
* Syntax: <"module" "namespace"> prefix "=" uri ";"
*
* @throws XPathException in the event of a syntax error.
*/
private void parseModuleDeclaration() throws XPathException {
expect(Token.MODULE_NAMESPACE);
nextToken();
expect(Token.NAME);
String prefix = t.currentTokenValue;
nextToken();
expect(Token.EQUALS);
nextToken();
expect(Token.STRING_LITERAL);
String uri = URILiteral(t.currentTokenValue);
checkProhibitedPrefixes(prefix, uri);
if (uri.isEmpty()) {
grumble("Module namespace cannot be \"\"", "XQST0088");
uri = "http://saxon.fallback.namespace/"; // for error recovery
}
nextToken();
expect(Token.SEMICOLON);
nextToken();
try {
((QueryModule) env).setModuleNamespace(uri);
((QueryModule) env).declarePrologNamespace(prefix, uri);
executable.addQueryLibraryModule((QueryModule) env);
} catch (XPathException err) {
err.setLocator(makeLocation());
reportError(err);
}
}
/**
* Parse the query prolog. This method, and its subordinate methods which handle
* individual declarations in the prolog, cause the static context to be updated
* with relevant context information. On exit, t.currentToken is the first token
* that is not recognized as being part of the prolog.
*
* @throws XPathException in the event of a syntax error.
*/
private void parseProlog() throws XPathException {
//boolean allowSetters = true;
boolean allowModuleDecl = true;
boolean allowDeclarations = true;
while (true) {
try {
if (t.currentToken == Token.MODULE_NAMESPACE) {
String uri = ((QueryModule) env).getModuleNamespace();
if (uri == null) {
grumble("Module declaration must not be used in a main module");
} else {
grumble("Module declaration appears more than once");
}
if (!allowModuleDecl) {
grumble("Module declaration must precede other declarations in the query prolog");
}
}
allowModuleDecl = false;
switch (t.currentToken) {
case Token.DECLARE_NAMESPACE:
if (!allowDeclarations) {
grumble("Namespace declarations cannot follow variables, functions, or options");
}
//allowSetters = false;
parseNamespaceDeclaration();
break;
case Token.DECLARE_ANNOTATED:
// we have read "declare %"
processPreamble();
if (allowDeclarations) {
sealNamespaces(namespacesToBeSealed, env.getConfiguration());
allowDeclarations = false;
}
nextToken();
expect(Token.PERCENT);
ArrayList annotationList = parseAnnotationsList();
if (isKeyword("function")) {
Map annotations = checkAnnotations(annotationList, true);
parseFunctionDeclaration(annotations);
} else if (isKeyword("variable")) {
Map annotations = checkAnnotations(annotationList, false);
parseVariableDeclaration(annotations);
} else {
grumble("Annotations can appear only in 'declare variable' and 'declare function'");
}
break;
case Token.DECLARE_DEFAULT:
nextToken();
expect(Token.NAME);
if (t.currentTokenValue.equals("element")) {
if (!allowDeclarations) {
grumble("Namespace declarations cannot follow variables, functions, or options");
}
//allowSetters = false;
parseDefaultElementNamespace();
} else if (t.currentTokenValue.equals("function")) {
if (!allowDeclarations) {
grumble("Namespace declarations cannot follow variables, functions, or options");
}
//allowSetters = false;
parseDefaultFunctionNamespace();
} else if (t.currentTokenValue.equals("collation")) {
if (!allowDeclarations) {
grumble("Collation declarations must appear earlier in the prolog");
}
parseDefaultCollation();
} else if (t.currentTokenValue.equals("order")) {
if (!allowDeclarations) {
grumble("Order declarations must appear earlier in the prolog");
}
parseDefaultOrder();
} else if (t.currentTokenValue.equals("decimal-format")) {
nextToken();
parseDefaultDecimalFormat();
} else {
grumble("After 'declare default', expected 'element', 'function', or 'collation'");
}
break;
case Token.DECLARE_BOUNDARY_SPACE:
if (!allowDeclarations) {
grumble("'declare boundary-space' must appear earlier in the query prolog");
}
parseBoundarySpaceDeclaration();
break;
case Token.DECLARE_ORDERING:
if (!allowDeclarations) {
grumble("'declare ordering' must appear earlier in the query prolog");
}
parseOrderingDeclaration();
break;
case Token.DECLARE_COPY_NAMESPACES:
if (!allowDeclarations) {
grumble("'declare copy-namespaces' must appear earlier in the query prolog");
}
parseCopyNamespacesDeclaration();
break;
case Token.DECLARE_BASEURI:
if (!allowDeclarations) {
grumble("'declare base-uri' must appear earlier in the query prolog");
}
parseBaseURIDeclaration();
break;
case Token.DECLARE_DECIMAL_FORMAT:
if (!allowDeclarations) {
grumble("'declare decimal-format' must appear earlier in the query prolog");
}
parseDecimalFormatDeclaration();
break;
case Token.IMPORT_SCHEMA:
//allowSetters = false;
if (!allowDeclarations) {
grumble("Import schema must appear earlier in the prolog");
}
parseSchemaImport();
break;
case Token.IMPORT_MODULE:
//allowSetters = false;
if (!allowDeclarations) {
grumble("Import module must appear earlier in the prolog");
}
parseModuleImport();
break;
case Token.DECLARE_VARIABLE:
//allowSetters = false;
if (allowDeclarations) {
sealNamespaces(namespacesToBeSealed, env.getConfiguration());
allowDeclarations = false;
}
processPreamble();
parseVariableDeclaration(null);
break;
case Token.DECLARE_CONTEXT:
//allowSetters = false;
if (allowDeclarations) {
sealNamespaces(namespacesToBeSealed, env.getConfiguration());
allowDeclarations = false;
}
processPreamble();
parseContextItemDeclaration();
break;
case Token.DECLARE_FUNCTION:
if (allowDeclarations) {
sealNamespaces(namespacesToBeSealed, env.getConfiguration());
allowDeclarations = false;
}
processPreamble();
parseFunctionDeclaration(null);
break;
case Token.DECLARE_UPDATING:
nextToken();
if (!isKeyword("function")) {
grumble("expected 'function' after 'declare updating");
}
if (allowDeclarations) {
sealNamespaces(namespacesToBeSealed, env.getConfiguration());
allowDeclarations = false;
}
processPreamble();
parseUpdatingFunctionDeclaration();
break;
case Token.DECLARE_OPTION:
if (allowDeclarations) {
sealNamespaces(namespacesToBeSealed, env.getConfiguration());
allowDeclarations = false;
}
parseOptionDeclaration();
break;
case Token.DECLARE_CONSTRUCTION:
if (!allowDeclarations) {
grumble("'declare construction' must appear earlier in the query prolog");
}
parseConstructionDeclaration();
break;
case Token.DECLARE_REVALIDATION:
if (!allowDeclarations) {
grumble("'declare revalidation' must appear earlier in the query prolog");
}
parseRevalidationDeclaration();
break;
case Token.EOF:
String uri = ((QueryModule) env).getModuleNamespace();
if (uri == null) {
grumble("The main module must contain a query expression after any declarations in the prolog");
} else {
return;
}
break;
default:
return;
}
expect(Token.SEMICOLON);
nextToken();
} catch (XPathException err) {
if (err.getLocator() == null) {
err.setLocator(makeLocation());
}
if (!err.hasBeenReported()) {
errorCount++;
if (firstError == null) {
firstError = err;
}
((QueryModule) env).reportStaticError(err);
}
// we've reported an error, attempt to recover by skipping to the
// next semicolon
while (t.currentToken != Token.SEMICOLON) {
nextToken();
if (t.currentToken == Token.EOF) {
return;
} else if (t.currentToken == Token.RCURLY) {
t.lookAhead();
} else if (t.currentToken == Token.TAG) {
parsePseudoXML(true);
}
}
nextToken();
}
}
}
/**
* Parse the annotations that can appear in a variable or function declaration
*
* @return the annotations as a map, indexed by annotation name
*/
protected Map parseAnnotations() throws XPathException {
// we have read "declare" and have seen "%" as lookahead
if (!allowXPath30Syntax) {
grumble("Function and variable annotations require XQuery 3.0");
}
Map annotations = new HashMap();
int options = 0;
while (true) {
t.setState(Tokenizer.BARE_NAME_STATE);
nextToken();
expect(Token.NAME);
t.setState(Tokenizer.DEFAULT_STATE);
StructuredQName qName;
String uri;
if (t.currentTokenValue.indexOf(':') < 0) {
uri = NamespaceConstant.XQUERY;
qName = new StructuredQName("", uri, t.currentTokenValue);
} else {
qName = makeStructuredQName(t.currentTokenValue, "");
uri = qName.getURI();
}
Annotation annotation = new Annotation(qName);
if (annotations.get(qName) != null) {
grumble("Annotation " + qName.getDisplayName() + " appears more than once");
}
if (uri.equals(NamespaceConstant.XQUERY)) {
if (qName.equals(Annotation.PRIVATE) || qName.equals(Annotation.PUBLIC) ||
qName.equals(Annotation.UPDATING) || qName.equals(Annotation.SIMPLE)) {
// OK
} else {
grumble("Unrecognized variable or function annotation " + qName.getDisplayName(), "XQST0045");
}
annotation.addAnnotationParameter(new Int64Value(options));
} else if (isReservedInQuery(uri)) {
grumble("The annotation " + t.currentTokenValue + " is in a reserved namespace", "XQST0045");
} else if (uri.equals("")) {
grumble("The annotation " + t.currentTokenValue + " is in no namespace", "XQST0045");
} else {
// no action - ignore namespaced annotations
}
for (Annotation other : annotations.values()) {
if (Annotation.mutuallyExclusive(annotation, other)) {
grumble("The annotations %" + annotation.getAnnotationQName().getDisplayName() +
" and %" + other.getAnnotationQName().getDisplayName() +
" cannot both be present on the same function or variable");
}
}
annotations.put(qName, annotation);
nextToken();
if (t.currentToken == Token.LPAR) {
nextToken();
if (t.currentToken == Token.RPAR) {
grumble("Annotation parameter list cannot be empty");
}
while (true) {
// nextToken();
Literal arg = null;
switch (t.currentToken) {
case Token.STRING_LITERAL:
arg = (Literal)parseStringLiteral(false);
break;
case Token.NUMBER:
arg = (Literal)parseNumericLiteral(false);
break;
default:
grumble("Annotation parameter must be a literal");
return null;
}
GroundedValue val = arg.getValue();
if (val instanceof StringValue || val instanceof NumericValue) {
annotation.addAnnotationParameter((AtomicValue) val);
} else {
grumble("Annotation parameter must be a string or number");
}
if (t.currentToken == Token.RPAR) {
nextToken();
break;
}
expect(Token.COMMA);
nextToken();
}
}
if (t.currentToken != Token.PERCENT) {
return annotations;
}
}
}
/**
* Parse the annotations that can appear in a variable or function declaration
* @return the annotations as a list
* @throws XPathException in the event of a syntax error
*/
protected ArrayList parseAnnotationsList() throws XPathException {
// we have read "declare" and have seen "%" as lookahead
if (!allowXPath30Syntax) {
grumble("Function and variable annotations require XQuery 3.0");
}
ArrayList annotations = new ArrayList();
int options = 0;
while (true) {
t.setState(Tokenizer.BARE_NAME_STATE);
nextToken();
expect(Token.NAME);
t.setState(Tokenizer.DEFAULT_STATE);
StructuredQName qName;
String uri;
if (t.currentTokenValue.indexOf(':') < 0) {
uri = NamespaceConstant.XQUERY;
qName = new StructuredQName("", uri, t.currentTokenValue);
} else {
qName = makeStructuredQName(t.currentTokenValue, "");
uri = qName.getURI();
}
Annotation annotation = new Annotation(qName);
if (uri.equals(NamespaceConstant.XQUERY)) {
if (qName.equals(Annotation.PRIVATE) || qName.equals(Annotation.PUBLIC) ||
qName.equals(Annotation.UPDATING) || qName.equals(Annotation.SIMPLE)) {
// OK
} else {
grumble("Unrecognized variable or function annotation " + qName.getDisplayName(), "XQST0045");
}
annotation.addAnnotationParameter(new Int64Value(options));
} else if (isReservedInQuery(uri)) {
grumble("The annotation " + t.currentTokenValue + " is in a reserved namespace", "XQST0045");
} else if (uri.equals("")) {
grumble("The annotation " + t.currentTokenValue + " is in no namespace", "XQST0045");
} else {
// no action - ignore namespaced annotations
}
nextToken();
if (t.currentToken == Token.LPAR) {
nextToken();
if (t.currentToken == Token.RPAR) {
grumble("Annotation parameter list cannot be empty");
}
while (true) {
// nextToken();
Literal arg = null;
switch (t.currentToken) {
case Token.STRING_LITERAL:
arg = (Literal)parseStringLiteral(false);
break;
case Token.NUMBER:
arg = (Literal)parseNumericLiteral(false);
break;
default:
grumble("Annotation parameter must be a literal");
return null;
}
GroundedValue val = arg.getValue();
if (val instanceof StringValue || val instanceof NumericValue) {
annotation.addAnnotationParameter((AtomicValue) val);
} else {
grumble("Annotation parameter must be a string or number");
}
if (t.currentToken == Token.RPAR) {
nextToken();
break;
}
expect(Token.COMMA);
nextToken();
}
}
annotations.add(annotation);
if (t.currentToken != Token.PERCENT) {
return annotations;
}
}
}
/**
* Check the list of annotations that appear in a variable or function declaration, for duplicates, etc.
* @return the annotations as a map, indexed by annotation name
* @throws XPathException in the event of a syntax error
*/
protected Map checkAnnotations(ArrayList annotationList, boolean isFunction) throws XPathException {
String mutuallyExclusiveErrorCode;
if (isFunction) {
mutuallyExclusiveErrorCode = "XQST0106";
} else {
mutuallyExclusiveErrorCode = "XQST0116";
}
StructuredQName qName;
Map annotationsMap = new HashMap();
for (Annotation ann : annotationList) {
qName = ann.getAnnotationQName();
if (annotationsMap.containsKey(qName)){
grumble("Annotation " + qName.getDisplayName() + " appears more than once", mutuallyExclusiveErrorCode);
} else {
annotationsMap.put(qName, ann);
}
}
if (annotationsMap.containsKey(Annotation.PRIVATE) && annotationsMap.containsKey(Annotation.PUBLIC)) {
grumble("The annotations %private and %public cannot both be present on the same function or variable", mutuallyExclusiveErrorCode);
}
if (annotationsMap.containsKey(Annotation.UPDATING) && annotationsMap.containsKey(Annotation.SIMPLE)) {
grumble("The annotations %updating and %simple cannot both be present on the same function or variable", mutuallyExclusiveErrorCode);
}
return annotationsMap;
}
private void sealNamespaces(/*@NotNull*/ List namespacesToBeSealed, /*@NotNull*/ Configuration config) {
for (Object aNamespacesToBeSealed : namespacesToBeSealed) {
String ns = (String) aNamespacesToBeSealed;
config.sealNamespace(ns);
}
}
/**
* Method called once the setters have been read to do tidying up that can't be done until we've got
* to the end
*
* @throws XPathException if parsing fails
*/
private void processPreamble() throws XPathException {
if (preambleProcessed) {
return;
}
preambleProcessed = true;
if (foundDefaultCollation) {
String collationName = env.getDefaultCollationName();
URI collationURI;
try {
collationURI = new URI(collationName);
if (!collationURI.isAbsolute()) {
URI base = new URI(env.getStaticBaseURI());
collationURI = base.resolve(collationURI);
collationName = collationURI.toString();
}
} catch (URISyntaxException err) {
grumble("Default collation name '" + collationName + "' is not a valid URI", "XQST0046");
collationName = NamespaceConstant.CODEPOINT_COLLATION_URI;
}
if (env.getConfiguration().getCollation(collationName) == null) {
grumble("Default collation name '" + collationName + "' is not a recognized collation", "XQST0038");
collationName = NamespaceConstant.CODEPOINT_COLLATION_URI;
}
((QueryModule) env).setDefaultCollationName(collationName);
}
for (Import imp : schemaImports) {
try {
applySchemaImport(imp);
} catch (XPathException err) {
if (!err.hasBeenReported()) {
throw err;
}
}
}
for (Import imp : moduleImports) {
// Check that this import would not create a cycle involving a change of namespace
// if (!disableCycleChecks) {
// if (!imp.namespaceURI.equals(((QueryModule)env).getModuleNamespace())) {
// QueryModule parent = (QueryModule)env;
// if (!parent.mayImportModule(imp.namespaceURI)) {
// XPathException err = new XPathException(
// "A module cannot import itself directly or indirectly, unless all modules in the cycle are in the same namespace");
// err.setErrorCode("XQST0073");
// err.setIsStaticError(true);
// throw err;
// }
// }
// }
try {
applyModuleImport(imp);
} catch (XPathException err) {
if (!err.hasBeenReported()) {
throw err;
}
}
}
}
private void parseDefaultCollation() throws XPathException {
// <"default" "collation"> StringLiteral
if (foundDefaultCollation) {
grumble("default collation appears more than once", "XQST0038");
}
foundDefaultCollation = true;
nextToken();
expect(Token.STRING_LITERAL);
String uri = URILiteral(t.currentTokenValue);
((QueryModule) env).setDefaultCollationName(uri);
nextToken();
}
/**
* parse "declare default order empty (least|greatest)"
*
* @throws XPathException if parsing fails
*/
private void parseDefaultOrder() throws XPathException {
if (foundEmptyOrderingDeclaration) {
grumble("empty ordering declaration appears more than once", "XQST0069");
}
foundEmptyOrderingDeclaration = true;
nextToken();
if (!isKeyword("empty")) {
grumble("After 'declare default order', expected keyword 'empty'");
}
nextToken();
if (isKeyword("least")) {
((QueryModule) env).setEmptyLeast(true);
} else if (isKeyword("greatest")) {
((QueryModule) env).setEmptyLeast(false);
} else {
grumble("After 'declare default order empty', expected keyword 'least' or 'greatest'");
}
nextToken();
}
/**
* Parse the "declare xmlspace" declaration.
* Syntax: <"declare" "boundary-space"> ("preserve" | "strip")
*
* @throws XPathException if a static error is encountered
*/
private void parseBoundarySpaceDeclaration() throws XPathException {
if (foundBoundarySpaceDeclaration) {
grumble("'declare boundary-space' appears more than once", "XQST0068");
}
foundBoundarySpaceDeclaration = true;
nextToken();
expect(Token.NAME);
if ("preserve".equals(t.currentTokenValue)) {
((QueryModule) env).setPreserveBoundarySpace(true);
} else if ("strip".equals(t.currentTokenValue)) {
((QueryModule) env).setPreserveBoundarySpace(false);
} else {
grumble("boundary-space must be 'preserve' or 'strip'");
}
nextToken();
}
/**
* Parse the "declare ordering" declaration.
* Syntax: <"declare" "ordering"> ("ordered" | "unordered")
*
* @throws XPathException if parsing fails
*/
private void parseOrderingDeclaration() throws XPathException {
if (foundOrderingDeclaration) {
grumble("ordering mode declaration appears more than once", "XQST0065");
}
foundOrderingDeclaration = true;
nextToken();
expect(Token.NAME);
if ("ordered".equals(t.currentTokenValue)) {
// no action
} else if ("unordered".equals(t.currentTokenValue)) {
// no action
} else {
grumble("ordering mode must be 'ordered' or 'unordered'");
}
nextToken();
}
/**
* Parse the "declare copy-namespaces" declaration.
* Syntax: <"declare" "copy-namespaces"> ("preserve" | "no-preserve") "," ("inherit" | "no-inherit")
*
* @throws XPathException if a static error is encountered
*/
private void parseCopyNamespacesDeclaration() throws XPathException {
if (foundCopyNamespaces) {
grumble("declare copy-namespaces appears more than once", "XQST0055");
}
foundCopyNamespaces = true;
nextToken();
expect(Token.NAME);
if ("preserve".equals(t.currentTokenValue)) {
((QueryModule) env).setPreserveNamespaces(true);
} else if ("no-preserve".equals(t.currentTokenValue)) {
((QueryModule) env).setPreserveNamespaces(false);
} else {
grumble("copy-namespaces must be followed by 'preserve' or 'no-preserve'");
}
nextToken();
expect(Token.COMMA);
nextToken();
expect(Token.NAME);
if ("inherit".equals(t.currentTokenValue)) {
((QueryModule) env).setInheritNamespaces(true);
} else if ("no-inherit".equals(t.currentTokenValue)) {
((QueryModule) env).setInheritNamespaces(false);
} else {
grumble("After the comma in the copy-namespaces declaration, expected 'inherit' or 'no-inherit'");
}
nextToken();
}
/**
* Parse the "declare construction" declaration.
* Syntax: <"declare" "construction"> ("preserve" | "strip")
*
* @throws XPathException if parsing fails
*/
private void parseConstructionDeclaration() throws XPathException {
if (foundConstructionDeclaration) {
grumble("declare construction appears more than once", "XQST0067");
}
foundConstructionDeclaration = true;
nextToken();
expect(Token.NAME);
int val;
if ("preserve".equals(t.currentTokenValue)) {
val = Validation.PRESERVE;
// if (!env.getExecutable().isSchemaAware()) {
// grumble("construction mode preserve is allowed only with a schema-aware query");
// }
} else if ("strip".equals(t.currentTokenValue)) {
val = Validation.STRIP;
} else {
grumble("construction mode must be 'preserve' or 'strip'");
val = Validation.STRIP;
}
((QueryModule) env).setConstructionMode(val);
nextToken();
}
/**
* Parse the "declare revalidation" declaration.
* Syntax: not allowed unless XQuery update is in use
*
* @throws XPathException if the syntax is incorrect, or is not allowed in this XQuery processor
*/
protected void parseRevalidationDeclaration() throws XPathException {
grumble("declare revalidation is allowed only in XQuery Update");
}
/**
* Parse (and process) the schema import declaration.
* SchemaImport ::= "import" "schema" SchemaPrefix? URILiteral ("at" URILiteral ("," URILiteral)*)?
* SchemaPrefix ::= ("namespace" NCName "=") | ("default" "element" "namespace")
*
* @throws XPathException if parsing fails
*/
private void parseSchemaImport() throws XPathException {
ensureSchemaAware("import schema");
Import sImport = new Import();
String prefix = null;
sImport.namespaceURI = null;
sImport.locationURIs = new ArrayList(5);
nextToken();
if (isKeyword("namespace")) {
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
expect(Token.NAME);
prefix = t.currentTokenValue;
nextToken();
expect(Token.EQUALS);
nextToken();
} else if (isKeyword("default")) {
nextToken();
if (!isKeyword("element")) {
grumble("In 'import schema', expected 'element namespace'");
}
nextToken();
if (!isKeyword("namespace")) {
grumble("In 'import schema', expected keyword 'namespace'");
}
nextToken();
prefix = "";
}
if (t.currentToken == Token.STRING_LITERAL) {
String uri = URILiteral(t.currentTokenValue);
checkProhibitedPrefixes(prefix, uri);
sImport.namespaceURI = uri;
nextToken();
if (isKeyword("at")) {
nextToken();
expect(Token.STRING_LITERAL);
sImport.locationURIs.add(URILiteral(t.currentTokenValue));
nextToken();
while (t.currentToken == Token.COMMA) {
nextToken();
expect(Token.STRING_LITERAL);
sImport.locationURIs.add(URILiteral(t.currentTokenValue));
nextToken();
}
} else if (t.currentToken != Token.SEMICOLON) {
grumble("After the target namespace URI, expected 'at' or ';'");
}
} else {
grumble("After 'import schema', expected 'namespace', 'default', or a string-literal");
}
if (prefix != null) {
try {
if (prefix.isEmpty()) {
((QueryModule) env).setDefaultElementNamespace(sImport.namespaceURI);
} else {
if (sImport.namespaceURI == null || "".equals(sImport.namespaceURI)) {
grumble("A prefix cannot be bound to the null namespace", "XQST0057");
}
((QueryModule) env).declarePrologNamespace(prefix, sImport.namespaceURI);
}
} catch (XPathException err) {
err.setLocator(makeLocation());
reportError(err);
}
}
for (Object schemaImport : schemaImports) {
Import imp = (Import) schemaImport;
if (imp.namespaceURI.equals(sImport.namespaceURI)) {
grumble("Schema namespace '" + sImport.namespaceURI + "' is imported more than once", "XQST0058");
break;
}
}
schemaImports.add(sImport);
}
protected void ensureSchemaAware(String featureName) throws XPathException {
if (!env.getConfiguration().isLicensedFeature(Configuration.LicenseFeature.ENTERPRISE_XQUERY)) {
throw new XPathException("This Saxon version and license does not allow use of '" + featureName + "'", "XQST0009");
}
env.getConfiguration().checkLicensedFeature(Configuration.LicenseFeature.ENTERPRISE_XQUERY, featureName, -1);
getExecutable().setSchemaAware(true);
getStaticContext().getPackageData().setSchemaAware(true);
}
private void applySchemaImport(/*@NotNull*/ Import sImport) throws XPathException {
// Do the importing
Configuration config = env.getConfiguration();
synchronized (config) {
if (!config.isSchemaAvailable(sImport.namespaceURI)) {
if (!sImport.locationURIs.isEmpty()) {
try {
PipelineConfiguration pipe = config.makePipelineConfiguration();
config.readMultipleSchemas(pipe, env.getStaticBaseURI(), sImport.locationURIs, sImport.namespaceURI);
namespacesToBeSealed.add(sImport.namespaceURI);
} catch (SchemaException err) {
grumble("Error in schema " + sImport.namespaceURI + ": " + err.getMessage(), "XQST0059");
}
} else if (sImport.namespaceURI.equals(NamespaceConstant.XML) ||
sImport.namespaceURI.equals(NamespaceConstant.FN) ||
sImport.namespaceURI.equals(NamespaceConstant.JSON) ||
sImport.namespaceURI.equals(NamespaceConstant.SCHEMA_INSTANCE)) {
config.addSchemaForBuiltInNamespace(sImport.namespaceURI);
} else {
grumble("Unable to locate requested schema " + sImport.namespaceURI, "XQST0059");
}
}
((QueryModule) env).addImportedSchema(sImport.namespaceURI, env.getStaticBaseURI(), sImport.locationURIs);
}
}
/**
* Parse (and expand) the module import declaration.
* Syntax: <"import" "module" ("namespace" NCName "=")? uri ("at" uri ("," uri)*)? ";"
*
* @throws net.sf.saxon.trans.XPathException
* if a static error is encountered
*/
private void parseModuleImport() throws XPathException {
QueryModule thisModule = (QueryModule) env;
Import mImport = new Import();
String prefix = null;
mImport.namespaceURI = null;
mImport.locationURIs = new ArrayList(5);
nextToken();
if (t.currentToken == Token.NAME && t.currentTokenValue.equals("namespace")) {
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
expect(Token.NAME);
prefix = t.currentTokenValue;
nextToken();
expect(Token.EQUALS);
nextToken();
}
if (t.currentToken == Token.STRING_LITERAL) {
String uri = URILiteral(t.currentTokenValue);
checkProhibitedPrefixes(prefix, uri);
mImport.namespaceURI = uri;
if (mImport.namespaceURI.isEmpty()) {
grumble("Imported module namespace cannot be \"\"", "XQST0088");
mImport.namespaceURI = "http://saxon.fallback.namespace/line" + t.getLineNumber(); // for error recovery
}
if (importedModules.contains(mImport.namespaceURI)) {
grumble("Two 'import module' declarations specify the same module namespace", "XQST0047");
}
importedModules.add(mImport.namespaceURI);
((QueryModule) env).addImportedNamespace(mImport.namespaceURI);
nextToken();
if (isKeyword("at")) {
do {
nextToken();
expect(Token.STRING_LITERAL);
mImport.locationURIs.add(URILiteral(t.currentTokenValue));
nextToken();
} while (t.currentToken == Token.COMMA);
}
} else {
grumble("After 'import module', expected 'namespace' or a string-literal");
}
if (prefix != null) {
try {
if (mImport.namespaceURI.equals(thisModule.getModuleNamespace()) &&
mImport.namespaceURI.equals(thisModule.checkURIForPrefix(prefix))) {
// then do nothing: a duplicate declaration in this situation is not an error
} else {
thisModule.declarePrologNamespace(prefix, mImport.namespaceURI);
}
} catch (XPathException err) {
err.setLocator(makeLocation());
reportError(err);
}
}
// // Check that this import would not create a cycle involving a change of namespace
// if (!disableCycleChecks) {
// if (!mImport.namespaceURI.equals(((QueryModule)env).getModuleNamespace())) {
// QueryModule parent = (QueryModule)env;
// if (!parent.mayImport(mImport.namespaceURI)) {
// StaticError err = new StaticError("A module cannot import itself directly or indirectly, unless all modules in the cycle are in the same namespace");
// err.setErrorCode("XQST0073");
// throw err;
// }
// }
// }
moduleImports.add(mImport);
}
public void applyModuleImport(/*@NotNull*/ Import mImport) throws XPathException {
List existingModules;
// resolve the location URIs against the base URI
for (int i = 0; i < mImport.locationURIs.size(); i++) {
try {
String uri = mImport.locationURIs.get(i);
URI abs = ResolveURI.makeAbsolute(uri, env.getStaticBaseURI());
mImport.locationURIs.set(i, abs.toString());
} catch (URISyntaxException e) {
grumble("Invalid URI " + mImport.locationURIs.get(i) + ": " + e.getMessage());
}
}
// See if the URI is that of a separately-compiled query library
QueryLibrary lib = ((QueryModule) env).getUserQueryContext().getCompiledLibrary(mImport.namespaceURI);
if (lib != null) {
executable.addQueryLibraryModule(lib);
existingModules = new ArrayList();
existingModules.add(lib);
lib.link((QueryModule) env);
} else if (!env.getConfiguration().getBooleanProperty(FeatureKeys.XQUERY_MULTIPLE_MODULE_IMPORTS)) {
// Unless this configuration option is set, if we already know a module with the right module URI, then we
// use it irrespective of its location URI.
List list = executable.getQueryLibraryModules(mImport.namespaceURI);
if (list != null && !list.isEmpty()) {
return;
}
} else {
for (int h = mImport.locationURIs.size() - 1; h >= 0; h--) {
if (executable.isQueryLocationHintProcessed(mImport.locationURIs.get(h))) {
mImport.locationURIs.remove(h);
}
}
}
// If there are no location URIs left, and we already know a module with the right module URI.
if (mImport.locationURIs.isEmpty()) {
List list = executable.getQueryLibraryModules(mImport.namespaceURI);
if (list != null && !list.isEmpty()) {
return;
}
}
// Call the module URI resolver to find the remaining modules
ModuleURIResolver resolver = ((QueryModule) env).getUserQueryContext().getModuleURIResolver();
String[] hints = new String[mImport.locationURIs.size()];
for (int h = 0; h < hints.length; h++) {
hints[h] = mImport.locationURIs.get(h);
}
StreamSource[] sources = null;
if (resolver != null) {
try {
sources = resolver.resolve(mImport.namespaceURI, env.getStaticBaseURI(), hints);
} catch (XPathException err) {
grumble("Failed to resolve URI of imported module: " + err.getMessage(), "XQST0059");
}
}
if (sources == null) {
if (hints.length == 0) {
grumble("Cannot locate module for namespace " + mImport.namespaceURI, "XQST0059");
}
resolver = env.getConfiguration().getStandardModuleURIResolver();
sources = resolver.resolve(mImport.namespaceURI, env.getStaticBaseURI(), hints);
}
for (String hint : mImport.locationURIs) {
executable.addQueryLocationHintProcessed(hint);
}
for (int m = 0; m < sources.length; m++) {
StreamSource ss = sources[m];
String baseURI = ss.getSystemId();
if (baseURI == null) {
if (m < hints.length) {
baseURI = hints[m];
ss.setSystemId(hints[m]);
} else {
grumble("No base URI available for imported module", "XQST0059");
}
}
// Although the module hadn't been loaded when we started, it might have been loaded since, as
// a result of a reference from another imported module. Note, we are careful here to use URI.equals()
// rather that String.equals() to compare URIs, as this gives a slightly more intelligent comparison,
// for example the scheme name is case-independent, and file:///x/y/z matches file:/x/y/z.
// TODO: use similar logic when loading schema modules
// TODO: despite the comment above, we are comparing URIs as strings
existingModules = executable.getQueryLibraryModules(mImport.namespaceURI);
boolean loaded = false;
if (existingModules != null && m < hints.length) {
for (QueryModule existingModule : existingModules) {
URI uri = existingModule.getLocationURI();
if (uri != null && uri.toString().equals(mImport.locationURIs.get(m))) {
loaded = true;
break;
}
}
}
if (loaded) {
break;
}
try {
String queryText = QueryReader.readSourceQuery(ss, charChecker);
try {
if (ss.getInputStream() != null) {
ss.getInputStream().close();
} else if (ss.getReader() != null) {
ss.getReader().close();
}
} catch (IOException e) {
throw new XPathException("Failure while closing file for imported query module");
}
QueryModule.makeQueryModule(
baseURI, executable, (QueryModule) env, queryText, mImport.namespaceURI,
disableCycleChecks);
} catch (XPathException err) {
err.maybeSetLocation(makeLocation());
reportError(err);
}
}
}
/**
* Parse the Base URI declaration.
* Syntax: <"declare" "base-uri"> uri-literal
*
* @throws XPathException if a static error is found
*/
private void parseBaseURIDeclaration() throws XPathException {
if (foundBaseURIDeclaration) {
grumble("Base URI Declaration may only appear once", "XQST0032");
}
foundBaseURIDeclaration = true;
nextToken();
expect(Token.STRING_LITERAL);
String uri = URILiteral(t.currentTokenValue);
try {
// if the supplied URI is relative, try to resolve it
URI baseURI = new URI(uri);
if (!baseURI.isAbsolute()) {
String oldBase = env.getStaticBaseURI();
uri = ResolveURI.makeAbsolute(uri, oldBase).toString();
}
((QueryModule) env).setBaseURI(uri);
} catch (URISyntaxException err) {
// The spec says this "is not intrinsically an error", but can cause a failure later
((QueryModule) env).setBaseURI(uri);
}
nextToken();
}
/**
* Parse a named decimal format declaration.
* "declare" "decimal-format" QName (property "=" string-literal)*
*
* @throws XPathException
*/
private void parseDecimalFormatDeclaration() throws XPathException {
nextToken();
expect(Token.NAME);
StructuredQName formatName = makeStructuredQName(t.currentTokenValue, "");
if (env.getDecimalFormatManager().getNamedDecimalFormat(formatName) != null) {
grumble("Duplicate declaration of decimal-format " + formatName.getDisplayName(), "XQST0111");
}
nextToken();
parseDecimalFormatProperties(formatName);
}
/**
* Parse a default decimal format declaration
* "declare" "default" "decimal-format" (property "=" string-literal)*
*
* @throws XPathException
*/
private void parseDefaultDecimalFormat() throws XPathException {
if (foundDefaultDecimalFormat) {
grumble("Duplicate declaration of default decimal-format", "XQST0111");
}
foundDefaultDecimalFormat = true;
parseDecimalFormatProperties(null);
}
private void parseDecimalFormatProperties(/*@Nullable*/ StructuredQName formatName) throws XPathException {
int outerOffset = t.currentTokenStartOffset;
DecimalFormatManager dfm = env.getDecimalFormatManager();
DecimalSymbols dfs = formatName == null ? dfm.getDefaultDecimalFormat() : dfm.obtainNamedDecimalFormat(formatName);
if (allowXPath31Syntax) {
dfs.setHostLanguage(Configuration.XQUERY, 31);
} else if (allowXPath30Syntax) {
dfs.setHostLanguage(Configuration.XQUERY, 30);
} else {
dfs.setHostLanguage(Configuration.XQUERY, 20);
}
//dfs.setHostLanguage(Configuration.XQUERY, allowXPath30Syntax ? 30 : 20);
Set propertyNames = new HashSet(10);
while (t.currentToken != Token.SEMICOLON) {
int offset = t.currentTokenStartOffset;
String propertyName = t.currentTokenValue;
if (propertyNames.contains(propertyName)) {
grumble("Property name " + propertyName + " is defined more than once", "XQST0114", offset);
}
nextToken();
expect(Token.EQUALS);
nextToken();
expect(Token.STRING_LITERAL);
String propertyValue = unescape(t.currentTokenValue).toString();
nextToken();
propertyNames.add(propertyName);
if (propertyName.equals("decimal-separator")) {
dfs.setDecimalSeparator(propertyValue);
} else if (propertyName.equals("grouping-separator")) {
dfs.setGroupingSeparator(propertyValue);
} else if (propertyName.equals("infinity")) {
dfs.setInfinity(propertyValue);
} else if (propertyName.equals("minus-sign")) {
dfs.setMinusSign(propertyValue);
} else if (propertyName.equals("NaN")) {
dfs.setNaN(propertyValue);
} else if (propertyName.equals("percent")) {
dfs.setPercent(propertyValue);
} else if (propertyName.equals("per-mille")) {
dfs.setPerMille(propertyValue);
} else if (propertyName.equals("zero-digit")) {
try {
dfs.setZeroDigit(propertyValue);
} catch (XPathException err) {
err.setErrorCode("XQST0097");
throw err;
}
} else if (propertyName.equals("digit")) {
dfs.setDigit(propertyValue);
} else if (propertyName.equals("pattern-separator")) {
dfs.setPatternSeparator(propertyValue);
} else if (propertyName.equals("exponent-separator")) {
dfs.setExponentSeparator(propertyValue);
} else {
grumble("Unknown decimal-format property: " + propertyName, "XPST0003", offset);
}
}
try {
dfs.checkConsistency(formatName);
} catch (XPathException err) {
grumble(err.getMessage(), "XQST0098", outerOffset);
}
}
/**
* Parse the "default function namespace" declaration.
* Syntax: <"declare" "default" "function" "namespace"> StringLiteral
*
* @throws XPathException to indicate a syntax error
*/
private void parseDefaultFunctionNamespace() throws XPathException {
if (foundDefaultFunctionNamespace) {
grumble("default function namespace appears more than once", "XQST0066");
}
foundDefaultFunctionNamespace = true;
nextToken();
expect(Token.NAME);
if (!"namespace".equals(t.currentTokenValue)) {
grumble("After 'declare default function', expected 'namespace'");
}
nextToken();
expect(Token.STRING_LITERAL);
String uri = URILiteral(t.currentTokenValue);
if (uri.equals(NamespaceConstant.XML) || uri.equals(NamespaceConstant.XMLNS)) {
grumble("Reserved namespace used as default element/type namespace", "XQST0070");
}
((QueryModule) env).setDefaultFunctionNamespace(uri);
nextToken();
}
/**
* Parse the "default element namespace" declaration.
* Syntax: <"declare" "default" "element" "namespace"> StringLiteral
*
* @throws XPathException to indicate a syntax error
*/
private void parseDefaultElementNamespace() throws XPathException {
if (foundDefaultElementNamespace) {
grumble("default element namespace appears more than once", "XQST0066");
}
foundDefaultElementNamespace = true;
nextToken();
expect(Token.NAME);
if (!"namespace".equals(t.currentTokenValue)) {
grumble("After 'declare default element', expected 'namespace'");
}
nextToken();
expect(Token.STRING_LITERAL);
String uri = URILiteral(t.currentTokenValue);
if (uri.equals(NamespaceConstant.XML) || uri.equals(NamespaceConstant.XMLNS)) {
grumble("Reserved namespace used as default element/type namespace", "XQST0070");
}
((QueryModule) env).setDefaultElementNamespace(uri);
nextToken();
}
/**
* Parse a namespace declaration in the Prolog.
* Syntax: <"declare" "namespace"> NCName "=" StringLiteral
*
* @throws XPathException if parsing fails or a static error is found
*/
private void parseNamespaceDeclaration() throws XPathException {
nextToken();
expect(Token.NAME);
String prefix = t.currentTokenValue;
if (!NameChecker.isValidNCName(prefix)) {
grumble("Invalid namespace prefix " + Err.wrap(prefix));
}
nextToken();
expect(Token.EQUALS);
nextToken();
expect(Token.STRING_LITERAL);
String uri = URILiteral(t.currentTokenValue);
checkProhibitedPrefixes(prefix, uri);
if ("xml".equals(prefix)) {
// disallowed here even if bound to the correct namespace - erratum XQ.E19
grumble("Namespace prefix 'xml' cannot be declared", "XQST0070");
}
try {
((QueryModule) env).declarePrologNamespace(prefix, uri);
} catch (XPathException err) {
err.setLocator(makeLocation());
reportError(err);
}
nextToken();
}
/**
* Check that a namespace declaration does not use a prohibited prefix or URI (xml or xmlns)
*
* @param prefix the prefix to be tested
* @param uri the URI being declared
* @throws XPathException if the prefix is prohibited
*/
private void checkProhibitedPrefixes(/*@Nullable*/ String prefix, /*@Nullable*/ String uri) throws XPathException {
if (prefix != null && prefix.length() > 0 && !NameChecker.isValidNCName(prefix)) {
grumble("The namespace prefix " + Err.wrap(prefix) + " is not a valid NCName");
}
if (prefix == null) {
prefix = "";
}
if (uri == null) {
uri = "";
}
if ("xmlns".equals(prefix)) {
grumble("The namespace prefix 'xmlns' cannot be redeclared", "XQST0070");
}
if (uri.equals(NamespaceConstant.XMLNS)) {
grumble("The xmlns namespace URI is reserved", "XQST0070");
}
if (uri.equals(NamespaceConstant.XML) && !prefix.equals("xml")) {
grumble("The XML namespace cannot be bound to any prefix other than 'xml'", "XQST0070");
}
if (prefix.equals("xml") && !uri.equals(NamespaceConstant.XML)) {
grumble("The prefix 'xml' cannot be bound to any namespace other than " + NamespaceConstant.XML, "XQST0070");
}
}
/**
* Parse a global variable definition.
* <"declare" "variable" "$"> VarName TypeDeclaration?
* ((":=" ExprSingle ) | "external")
* XQuery 3.0 allows "external := ExprSingle"
*
* @param annotations derived from any %-annotations present in XQuery 3.0
* @throws XPathException if a static error is found
*/
private void parseVariableDeclaration(Map annotations) throws XPathException {
int offset = t.currentTokenStartOffset;
GlobalVariable var = new GlobalVariable();
var.setPackageData(((QueryModule) env).getPackageData());
var.setLineNumber(t.getLineNumber() + 1);
var.setSystemId(env.getSystemId());
if (annotations != null) {
var.setPrivate(annotations.get(Annotation.PRIVATE) != null);
}
nextToken();
expect(Token.DOLLAR);
t.setState(Tokenizer.BARE_NAME_STATE);
nextToken();
expect(Token.NAME);
String varName = t.currentTokenValue;
StructuredQName varQName = makeStructuredQName(t.currentTokenValue, "");
var.setVariableQName(varQName);
String uri = varQName.getURI();
String moduleURI = ((QueryModule) env).getModuleNamespace();
if (moduleURI != null && !moduleURI.equals(uri)) {
grumble("A variable declared in a library module must be in the module namespace", "XQST0048", offset);
}
nextToken();
SequenceType requiredType = SequenceType.ANY_SEQUENCE;
if (t.currentToken == Token.AS) {
t.setState(Tokenizer.SEQUENCE_TYPE_STATE);
nextToken();
requiredType = parseSequenceType();
}
var.setRequiredType(requiredType);
if (t.currentToken == Token.ASSIGN) {
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
Expression exp = parseExprSingle();
var.setSelectExpression(makeTracer(offset, exp, StandardNames.XSL_VARIABLE, varQName));
} else if (t.currentToken == Token.NAME) {
if ("external".equals(t.currentTokenValue)) {
GlobalParam par = new GlobalParam();
par.setPackageData(((QueryModule) env).getPackageData());
//par.setExecutable(var.getExecutable());
par.setLineNumber(var.getLineNumber());
par.setSystemId(var.getSystemId());
par.setVariableQName(var.getVariableQName());
par.setRequiredType(var.getRequiredType());
var = par;
nextToken();
if (t.currentToken == Token.ASSIGN && allowXPath30Syntax) {
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
Expression exp = parseExprSingle();
var.setSelectExpression(makeTracer(offset, exp, StandardNames.XSL_VARIABLE, varQName));
}
} else {
grumble("Variable must either be initialized or be declared as external");
}
} else {
grumble("Expected ':=' or 'external' in variable declaration");
}
QueryModule qenv = (QueryModule) env;
RetainedStaticContext rsc = env.makeRetainedStaticContext();
var.setRetainedStaticContext(rsc);
if (var.getBody() != null) {
var.getBody().setRetainedStaticContext(rsc);
}
if (qenv.getModuleNamespace() != null &&
!uri.equals(qenv.getModuleNamespace())) {
grumble("Variable " + Err.wrap(varName, Err.VARIABLE) + " is not defined in the module namespace");
}
try {
qenv.declareVariable(var);
} catch (XPathException e) {
grumble(e.getMessage(), e.getErrorCodeQName(), -1);
}
}
/**
* Parse a context item declaration.
* "declare" "context" "item" TypeDeclaration?
* ((":=" ExprSingle ) | ("external" (":=" ExprSingle ))
*
* @throws XPathException
*/
private void parseContextItemDeclaration() throws XPathException {
if (!allowXPath30Syntax) {
grumble("Context item declarations require XQuery 3.0");
}
int offset = t.currentTokenStartOffset;
nextToken();
if (!isKeyword("item")) {
grumble("After 'declare context', expected 'item'");
}
if (foundContextItemDeclaration) {
grumble("More than one context item declaration found", "XQST0099", offset);
}
foundContextItemDeclaration = true;
GlobalContextRequirement req = new GlobalContextRequirement();
req.mayBeSupplied = false;
// GlobalVariable var = new GlobalVariable();
// var.setLineNumber(t.getLineNumber());
// var.setSystemId(env.getSystemId());
// var.setPackageData(((QueryModule) env).getPackageData());
// StructuredQName varQName = StandardNames.SAXON_CONTEXT_ITEM;
// var.setVariableQName(varQName);
t.setState(Tokenizer.BARE_NAME_STATE);
nextToken();
ItemType requiredType = AnyItemType.getInstance();
if (t.currentToken == Token.AS) {
t.setState(Tokenizer.SEQUENCE_TYPE_STATE);
nextToken();
requiredType = parseItemType();
//ItemType requiredItemType = parseItemType();
//requiredType = SequenceType.makeSequenceType(requiredItemType, StaticProperty.EXACTLY_ONE);
}
//var.setRequiredType(requiredType);
req.requiredItemType = requiredType;
if (t.currentToken == Token.ASSIGN) {
if (!((QueryModule) env).isMainModule()) {
grumble("The context item must not be initialized in a library module", "XQST0113");
}
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
Expression exp = parseExprSingle();
exp.setRetainedStaticContext(env.makeRetainedStaticContext());
RoleDiagnostic role = new RoleDiagnostic(RoleDiagnostic.CONTEXT_ITEM, "context item declaration", 0);
exp = CardinalityChecker.makeCardinalityChecker(exp, StaticProperty.EXACTLY_ONE, role);
ExpressionVisitor visitor = ExpressionVisitor.make(env);
exp = exp.simplify();
ContextItemStaticInfo info = new ContextItemStaticInfo(AnyItemType.getInstance(), true);
exp.setRetainedStaticContext(env.makeRetainedStaticContext());
exp = exp.typeCheck(visitor, info);
req.setDefaultValue(exp);
} else if (t.currentToken == Token.NAME && "external".equals(t.currentTokenValue)) {
// GlobalParam par = new GlobalParam();
// par.setPackageData(((QueryModule) env).getPackageData());
// //par.setExecutable(var.getExecutable());
// par.setLineNumber(var.getLineNumber());
// par.setSystemId(var.getSystemId());
// par.setVariableQName(var.getVariableQName());
// par.setRequiredType(var.getRequiredType());
// var = par;
req.mayBeSupplied = true;
nextToken();
if (t.currentToken == Token.ASSIGN) {
if (!((QueryModule) env).isMainModule()) {
grumble("The context item must not be initialized in a library module", "XQST0113");
}
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
Expression exp = parseExprSingle();
//var.setSelectExpression(makeTracer(offset, exp, StandardNames.XSL_VARIABLE, varQName));
RoleDiagnostic role = new RoleDiagnostic(RoleDiagnostic.CONTEXT_ITEM, "context item declaration", 0);
exp = CardinalityChecker.makeCardinalityChecker(exp, StaticProperty.EXACTLY_ONE, role);
exp.setRetainedStaticContext(env.makeRetainedStaticContext());
req.setDefaultValue(exp);
} else {
// if (((QueryModule) env).isMainModule()) {
// var.setSelectExpression(new ErrorExpression(new XPathException("Required external context item not supplied", "XPDY0002")));
// } else {
// // ignore "external" in a library module, since it means nothing unless the main module also specifies "external".
// }
}
} else {
grumble("Expected ':=' or 'external' in context item declaration");
}
Executable exec = getExecutable();
if (exec.getGlobalContextRequirement() != null) {
// the context item is already declared in another module. Compare the required types
GlobalContextRequirement otherVar = exec.getGlobalContextRequirement();
ItemType otherType = otherVar.requiredItemType;
if (otherType != AnyItemType.getInstance()) {
TypeHierarchy th = env.getConfiguration().getTypeHierarchy();
int rel = th.relationship(requiredType, otherType);
if (rel == TypeHierarchy.SAME_TYPE || rel == TypeHierarchy.SUBSUMES) {
// no new constraints; therefore no action except to set the initializer
if (req.getDefaultValue() != null && otherVar.getDefaultValue() == null) {
otherVar.setDefaultValue(req.getDefaultValue());
}
} else if (rel == TypeHierarchy.DISJOINT) {
// the two types are incompatible: fail now
grumble("Different modules specify incompatible requirements for the type of the initial context item", "XPTY0004");
} else if (rel == TypeHierarchy.SUBSUMED_BY) {
// this required type can replace the previous one
if (req.getDefaultValue() == null && otherVar.getDefaultValue() != null) {
req.setDefaultValue(otherVar.getDefaultValue());
}
exec.setGlobalContextRequirement(req);
//exec.registerGlobalVariable(var);
} else {
// we need to replace the required type by the intersection of the two types.
// ASSERT: if the types are non-disjoint and neither subsumes the other, then they must be nodetests
if (!(requiredType instanceof NodeTest && otherType instanceof NodeTest)) {
// TODO: this can fail if both are function/map/array types
throw new AssertionError("Overlapping types found that are not NodeTests");
}
ItemType intersection = new CombinedNodeTest(
(NodeTest) requiredType,
Token.INTERSECT,
(NodeTest) otherType);
otherVar.requiredItemType = intersection;
if (req.getDefaultValue() != null && otherVar.getDefaultValue() == null) {
otherVar.setDefaultValue(req.getDefaultValue());
}
}
}
} else {
// context item not previously declared in another module
// QueryModule qenv = (QueryModule) env;
// try {
// qenv.declareVariable(var);
// } catch (XPathException e) {
// grumble(e.getMessage(), e.getErrorCodeQName(), -1);
// }
exec.setGlobalContextRequirement(req);
// exec.setInitialContextItemVariable(var);
// exec.registerGlobalVariable(var);
}
}
/**
* Parse a function declaration.
* Syntax:
* <"declare" "function"> QName "(" ParamList? ")" ("as" SequenceType)?
* (EnclosedExpr | "external")
*
* On entry, the "declare function" has already been recognized
*
* @param annotations the list of annotations that have been encountered for this function declaration
* @throws XPathException if a syntax error is found
*/
protected void parseFunctionDeclaration(Map annotations) throws XPathException {
// the next token should be the < QNAME "("> pair
int offset = t.currentTokenStartOffset;
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
if (t.currentToken != Token.FUNCTION && !allowXPath30Syntax) {
// XQuery 1.0 allows reserved names as function names. These will appear as some other kind of token
switch (t.currentToken) {
case Token.NODEKIND:
case Token.TYPESWITCH:
case Token.IF:
t.currentToken = Token.FUNCTION;
}
}
expect(Token.FUNCTION);
String uri;
StructuredQName qName;
if (t.currentTokenValue.indexOf(':') < 0) {
uri = env.getDefaultFunctionNamespace();
qName = new StructuredQName("", uri, t.currentTokenValue);
} else {
qName = makeStructuredQName(t.currentTokenValue, "");
uri = qName.getURI();
}
if (uri.isEmpty()) {
grumble("The function must be in a namespace", "XQST0060");
}
String moduleURI = ((QueryModule) env).getModuleNamespace();
if (moduleURI != null && !moduleURI.equals(uri)) {
grumble("A function in a library module must be in the module namespace", "XQST0048");
}
if (isReservedInQuery(uri)) {
grumble("The function name " + t.currentTokenValue + " is in a reserved namespace", "XQST0045");
}
XQueryFunction func = new XQueryFunction();
func.setFunctionName(qName);
func.setResultType(SequenceType.ANY_SEQUENCE);
func.setBody(null);
Location loc = new ExplicitLocation(env.getSystemId(), t.getLineNumber(offset), t.getColumnNumber(offset));
func.setLocation(loc);
func.setStaticContext((QueryModule) env);
func.setMemoFunction(memoFunction);
if (annotations != null) {
func.setUpdating(annotations.get(Annotation.UPDATING) != null);
func.setAnnotations(annotations);
}
nextToken();
HashSet paramNames = new HashSet(8);
if (t.currentToken != Token.RPAR) {
while (true) {
// ParamList ::= Param ("," Param)*
// Param ::= "$" VarName TypeDeclaration?
expect(Token.DOLLAR);
nextToken();
expect(Token.NAME);
StructuredQName argQName = makeStructuredQName(t.currentTokenValue, "");
if (paramNames.contains(argQName)) {
grumble("Duplicate parameter name " + Err.wrap(t.currentTokenValue, Err.VARIABLE), "XQST0039");
}
paramNames.add(argQName);
SequenceType paramType = SequenceType.ANY_SEQUENCE;
nextToken();
if (t.currentToken == Token.AS) {
nextToken();
paramType = parseSequenceType();
}
UserFunctionParameter arg = new UserFunctionParameter();
arg.setRequiredType(paramType);
arg.setVariableQName(argQName);
func.addArgument(arg);
declareRangeVariable(arg);
if (t.currentToken == Token.RPAR) {
break;
} else if (t.currentToken == Token.COMMA) {
nextToken();
} else {
grumble("Expected ',' or ')' after function argument, found '" +
Token.tokens[t.currentToken] + '\'');
}
}
}
t.setState(Tokenizer.BARE_NAME_STATE);
nextToken();
if (t.currentToken == Token.AS) {
if (func.isUpdating()) {
grumble("Cannot specify a return type for an updating function", "XUST0028");
}
t.setState(Tokenizer.SEQUENCE_TYPE_STATE);
nextToken();
func.setResultType(parseSequenceType());
}
if (isKeyword("external")) {
grumble("Saxon does not allow external functions to be declared", "XPST0017");
// TODO: allow this, provided the function is available (and take note of "nondeterministic")
} else {
expect(Token.LCURLY);
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
if (t.currentToken == Token.RCURLY && allowXPath31Syntax) {
Expression body = Literal.makeEmptySequence();
body.setRetainedStaticContext(env.makeRetainedStaticContext());
setLocation(body);
func.setBody(body);
} else {
func.setBody(parseExpression());
}
if (t.currentToken != Token.RCURLY) {
// special case error handling for when the function body is wrongly written as "return XXX"
Expression body = func.getBody();
if (body instanceof AxisExpression && body.toString().equals("child::return")) {
grumble("Incorrect use of 'return' keyword in function body");
}
}
expect(Token.RCURLY);
lookAhead(); // must be done manually after an RCURLY
}
UserFunctionParameter[] params = func.getParameterDefinitions();
//noinspection UnusedDeclaration
for (UserFunctionParameter param : params) {
undeclareRangeVariable();
}
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
QueryModule qenv = (QueryModule) env;
try {
qenv.declareFunction(func);
} catch (XPathException e) {
grumble(e.getMessage(), e.getErrorCodeQName(), -1);
}
memoFunction = false;
}
/**
* Parse an updating function declaration (allowed in XQuery Update only)
*
* @throws XPathException if parsing fails or if updating functions are not allowed
*/
protected void parseUpdatingFunctionDeclaration() throws XPathException {
grumble("Updating functions are allowed only in XQuery Update");
}
/**
* Parse an option declaration.
* Syntax:
* <"declare" "option"> QName "string-literal"
*
* On entry, the "declare option" has already been recognized
*
* @throws XPathException if a syntax error is found
*/
private void parseOptionDeclaration() throws XPathException {
nextToken();
expect(Token.NAME);
String defaultUri = allowXPath30Syntax ? NamespaceConstant.XQUERY : "";
StructuredQName varName = makeStructuredQName(t.currentTokenValue, defaultUri);
String uri = varName.getURI();
if (uri.isEmpty()) {
grumble("The QName identifying an option declaration must be prefixed", "XPST0081");
return;
}
nextToken();
expect(Token.STRING_LITERAL);
//String value = URILiteral(t.currentTokenValue).trim();
String value = unescape(t.currentTokenValue).toString();
if (uri.equals(NamespaceConstant.OUTPUT)) {
parseOutputDeclaration(varName, value);
} else if (uri.equals(NamespaceConstant.SAXON)) {
String localName = varName.getLocalPart();
if (localName.equals("output")) {
setOutputProperty(value);
} else if (localName.equals("memo-function")) {
value = value.trim();
if (value.equals("true")) {
memoFunction = true;
if (env.getConfiguration().getEditionCode().equals("HE")) {
warning("saxon:memo-function option is ignored under Saxon-HE");
}
} else if (value.equals("false")) {
memoFunction = false;
} else {
warning("Value of saxon:memo-function must be 'true' or 'false'");
}
} else if (localName.equals("allow-cycles")) {
value = value.trim();
if (value.equals("true")) {
disableCycleChecks = true;
} else if (value.equals("false")) {
disableCycleChecks = false;
} else {
warning("Value of saxon:allow-cycles must be 'true' or 'false'");
}
} else {
warning("Unknown Saxon option declaration: " + varName.getDisplayName());
}
}
nextToken();
}
protected void parseOutputDeclaration(StructuredQName varName, String value) throws XPathException {
if (!((QueryModule) env).isMainModule()) {
grumble("Output declarations must not appear in a library module", "XQST0108");
}
String localName = varName.getLocalPart();
if (outputPropertiesSeen.contains(varName)) {
grumble("Duplicate output declaration (" + varName + ")", "XQST0110");
}
outputPropertiesSeen.add(varName);
if (localName.equals("parameter-document")) {
Source source;
try {
source = env.getConfiguration().getURIResolver().resolve(value, env.getStaticBaseURI());
} catch (TransformerException e) {
throw XPathException.makeXPathException(e);
}
ParseOptions options = new ParseOptions();
options.setSchemaValidationMode(Validation.LAX);
TreeInfo doc = env.getConfiguration().buildDocumentTree(source);
SerializationParamsHandler ph = new SerializationParamsHandler();
ph.setSerializationParams(doc.getRootNode());
// TODO: handle override semantics
Properties baseProps = ph.getSerializationProperties();
Properties props = getExecutable().getDefaultOutputProperties();
for (String prop : baseProps.stringPropertyNames()) {
props.setProperty(prop, baseProps.getProperty(prop));
}
CharacterMap characterMap = ph.getCharacterMap();
if (characterMap != null) {
CharacterMapIndex index = new CharacterMapIndex();
index.putCharacterMap(characterMap.getName(), characterMap);
getExecutable().setCharacterMapIndex(index);
props.setProperty(SaxonOutputKeys.USE_CHARACTER_MAPS, characterMap.getName().getClarkName());
}
} else if (localName.equals("use-character-maps")) {
grumble("Output declaration use-character-maps cannot appear except in a parameter file", "XQST0109");
} else {
Properties props = getExecutable().getDefaultOutputProperties();
try {
ResultDocument.setSerializationProperty(props,
"", localName,
value,
env.getNamespaceResolver(),
false,
env.getConfiguration());
} catch (XPathException e) {
throw e;
}
}
}
/**
* Handle a saxon:output option declaration. Format:
* declare option saxon:output "indent = yes"
*
* @param property a property name=value pair. The name is the name of a serialization
* property, potentially as a prefixed QName; the value is the value of the property. A warning
* is output for unrecognized properties or values
*/
private void setOutputProperty(/*@NotNull*/ String property) {
int equals = property.indexOf("=");
if (equals < 0) {
badOutputProperty("no equals sign");
} else if (equals == 0) {
badOutputProperty("starts with '=");
}
String keyword = Whitespace.trim(property.substring(0, equals));
String value = equals == property.length() - 1 ? "" : Whitespace.trim(property.substring(equals + 1));
Properties props = getExecutable().getDefaultOutputProperties();
try {
assert keyword != null;
StructuredQName name = makeStructuredQName(keyword, "");
String lname = name.getLocalPart();
String uri = name.getURI();
ResultDocument.setSerializationProperty(props,
uri, lname,
value,
env.getNamespaceResolver(),
false,
env.getConfiguration());
} catch (XPathException e) {
badOutputProperty(e.getMessage());
}
}
private void badOutputProperty(String s) {
try {
warning("Invalid serialization property (" + s + ")");
} catch (XPathException staticError) {
//
}
}
/**
* Parse a FLWOR expression. This replaces the XPath "for" expression.
* Full syntax:
*
* [41] FLWORExpr ::= (ForClause | LetClause)+
* WhereClause? OrderByClause?
* "return" ExprSingle
* [42] ForClause ::= <"for" "$"> VarName TypeDeclaration? PositionalVar? "in" ExprSingle
* ("," "$" VarName TypeDeclaration? PositionalVar? "in" ExprSingle)*
* [43] PositionalVar ::= "at" "$" VarName
* [44] LetClause ::= <"let" "$"> VarName TypeDeclaration? ":=" ExprSingle
* ("," "$" VarName TypeDeclaration? ":=" ExprSingle)*
* [45] WhereClause ::= "where" Expr
* [46] OrderByClause ::= (<"order" "by"> | <"stable" "order" "by">) OrderSpecList
* [47] OrderSpecList ::= OrderSpec ("," OrderSpec)*
* [48] OrderSpec ::= ExprSingle OrderModifier
* [49] OrderModifier ::= ("ascending" | "descending")?
* (<"empty" "greatest"> | <"empty" "least">)?
* ("collation" StringLiteral)?
*
*
* @return the resulting subexpression
* @throws XPathException if any error is encountered
*/
/*@Nullable*/
protected Expression parseFLWORExpression() throws XPathException {
FLWORExpression flwor = new FLWORExpression();
int exprOffset = t.currentTokenStartOffset;
List clauseList = new ArrayList(4);
boolean foundOrderBy = false;
boolean foundWhere = false;
while (true) {
int offset = t.currentTokenStartOffset;
if (t.currentToken == Token.FOR) {
if (foundWhere && languageVersion < 30) {
grumble("In XQuery 1.0 'for' cannot follow 'where'");
}
if (foundOrderBy && languageVersion < 30) {
grumble("In XQuery 1.0 'for' cannot follow 'order by'");
}
parseForClause(flwor, clauseList);
} else if (t.currentToken == Token.LET) {
if (foundWhere && languageVersion < 30) {
grumble("In XQuery 1.0 'let' cannot follow 'where'");
}
if (foundOrderBy && languageVersion < 30) {
grumble("In XQuery 1.0 'let' cannot follow 'order by'");
}
parseLetClause(flwor, clauseList);
} else if (t.currentToken == Token.COUNT) {
parseCountClause(clauseList);
} else if (t.currentToken == Token.GROUP_BY) {
parseGroupByClause(flwor, clauseList);
} else if (t.currentToken == Token.FOR_TUMBLING || t.currentToken == Token.FOR_SLIDING) {
parseWindowClause(flwor, clauseList);
} else if (t.currentToken == Token.WHERE || isKeyword("where")) {
if (foundWhere && languageVersion < 30) {
grumble("In XQuery 1.0 only one 'where' clause is allowed");
}
if (foundOrderBy && languageVersion < 30) {
grumble("In XQuery 1.0 'where' cannot follow 'order by'");
}
nextToken();
Expression condition = parseExprSingle();
WhereClause clause = new WhereClause(flwor, condition);
clause.setRepeated(containsLoopingClause(clauseList));
clauseList.add(clause);
foundWhere = true;
} else if (isKeyword("stable") || isKeyword("order")) {
// we read the "stable" keyword but ignore it; Saxon ordering is always stable
if (isKeyword("stable")) {
nextToken();
if (!isKeyword("order")) {
grumble("'stable' must be followed by 'order by'");
}
}
if (foundOrderBy && languageVersion < 30) {
grumble("In XQuery 1.0 'order by' can only appear once");
}
foundOrderBy = true;
TupleExpression tupleExpression = new TupleExpression();
List vars = new ArrayList();
for (Clause c : clauseList) {
for (LocalVariableBinding b : c.getRangeVariables()) {
vars.add(new LocalVariableReference(b));
}
}
tupleExpression.setVariables(vars);
List sortSpecList;
t.setState(Tokenizer.BARE_NAME_STATE);
nextToken();
if (!isKeyword("by")) {
grumble("'order' must be followed by 'by'");
}
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
sortSpecList = parseSortDefinition();
SortKeyDefinition[] keys = new SortKeyDefinition[sortSpecList.size()];
for (int i = 0; i < keys.length; i++) {
SortSpec spec = (SortSpec) sortSpecList.get(i);
SortKeyDefinition key = new SortKeyDefinition();
key.setSortKey(((SortSpec) sortSpecList.get(i)).sortKey, false);
key.setOrder(new StringLiteral(spec.ascending ? "ascending" : "descending"));
key.setEmptyLeast(spec.emptyLeast);
if (spec.collation != null) {
final StringCollator comparator = env.getConfiguration().getCollation(spec.collation);
if (comparator == null) {
grumble("Unknown collation '" + spec.collation + '\'', "XQST0076");
}
key.setCollation(comparator);
}
keys[i] = key;
}
OrderByClause clause = new OrderByClause(flwor, keys, tupleExpression);
clause.setRepeated(containsLoopingClause(clauseList));
clauseList.add(clause);
} else {
break;
}
setLocation(clauseList.get(clauseList.size() - 1), offset);
}
int returnOffset = t.currentTokenStartOffset;
expect(Token.RETURN);
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
Expression returnExpression = parseExprSingle();
returnExpression = makeTracer(returnOffset, returnExpression, LocationKind.RETURN_EXPRESSION, null);
// undeclare all the range variables
for (int i = clauseList.size() - 1; i >= 0; i--) {
Clause clause = clauseList.get(i);
for (int n = 0; n < clause.getRangeVariables().length; n++) {
undeclareRangeVariable();
}
}
if (codeInjector != null) {
List expandedList = new ArrayList(clauseList.size() * 2);
expandedList.add(clauseList.get(0));
for (int i = 1; i < clauseList.size(); i++) {
Clause extra = codeInjector.injectClause(
clauseList.get(i - 1),
env
);
if (extra != null) {
expandedList.add(extra);
}
expandedList.add(clauseList.get(i));
}
Clause extra = codeInjector.injectClause(
clauseList.get(clauseList.size() - 1), env);
if (extra != null) {
expandedList.add(extra);
}
clauseList = expandedList;
}
flwor.init(clauseList, returnExpression);
setLocation(flwor, exprOffset);
return flwor;
}
/**
* Make a LetExpression. This returns an ordinary LetExpression if tracing is off, and an EagerLetExpression
* if tracing is on. This is so that trace events occur in an order that the user can follow.
*
* @return the constructed "let" expression
*/
/*@NotNull*/
protected LetExpression makeLetExpression() {
if (env.getConfiguration().isCompileWithTracing()) {
return new EagerLetExpression();
} else {
return new LetExpression();
}
}
protected static boolean containsLoopingClause(List clauseList) {
for (Clause c : clauseList) {
if (FLWORExpression.isLoopingClause(c)) {
return true;
}
}
return false;
}
/**
* Parse a ForClause.
*
* [42] ForClause ::= "for" ForBinding ("," ForBinding)*
* [42a] ForBinding ::= "$" VarName TypeDeclaration? ("allowing" "empty")? PositionalVar? "in" ExprSingle
*
*
* @param clauseList - the components of the parsed ForClause are appended to the
* supplied list
* @throws XPathException if parsing fails
*/
private void parseForClause(FLWORExpression flwor, List clauseList) throws XPathException {
boolean first = true;
do {
ForClause clause = new ForClause();
clause.setRepeated(!first || containsLoopingClause(clauseList));
setLocation(clause, t.currentTokenStartOffset);
if (first) {
//clause.offset = t.currentTokenStartOffset;
}
clauseList.add(clause);
nextToken();
if (first) {
first = false;
} else {
//clause.offset = t.currentTokenStartOffset;
}
expect(Token.DOLLAR);
nextToken();
expect(Token.NAME);
StructuredQName varQName = makeStructuredQName(t.currentTokenValue, "");
SequenceType type = SequenceType.SINGLE_ITEM;
nextToken();
boolean explicitType = false;
if (t.currentToken == Token.AS) {
explicitType = true;
nextToken();
type = parseSequenceType();
}
boolean allowingEmpty = false;
if (isKeyword("allowing")) {
if (!allowXPath30Syntax) {
grumble("'allowing empty' requires XQuery 3.0 to be enabled");
}
allowingEmpty = true;
clause.setAllowingEmpty(true);
if (!explicitType) {
type = SequenceType.OPTIONAL_ITEM;
}
nextToken();
if (!isKeyword("empty")) {
grumble("After 'allowing', expected 'empty'");
}
nextToken();
}
if (explicitType && !allowingEmpty && type.getCardinality() != StaticProperty.EXACTLY_ONE) {
warning("Occurrence indicator on singleton range variable has no effect");
type = SequenceType.makeSequenceType(type.getPrimaryType(), StaticProperty.EXACTLY_ONE);
}
LocalVariableBinding binding = new LocalVariableBinding(varQName, type);
clause.setRangeVariable(binding);
if (isKeyword("at")) {
nextToken();
expect(Token.DOLLAR);
nextToken();
expect(Token.NAME);
StructuredQName posQName = makeStructuredQName(t.currentTokenValue, "");
if (!scanOnly && posQName.equals(varQName)) {
grumble("The two variables declared in a single 'for' clause must have different names", "XQST0089");
}
LocalVariableBinding pos = new LocalVariableBinding(posQName, SequenceType.SINGLE_INTEGER);
clause.setPositionVariable(pos);
nextToken();
}
expect(Token.IN);
nextToken();
clause.initSequence(flwor, parseExprSingle());
declareRangeVariable(clause.getRangeVariable());
if (clause.getPositionVariable() != null) {
declareRangeVariable(clause.getPositionVariable());
}
if (allowingEmpty) {
checkForClauseAllowingEmpty(flwor, clause);
}
} while (t.currentToken == Token.COMMA);
}
/**
* Check a ForClause for an "outer for"
*
* @throws net.sf.saxon.trans.XPathException
* if invalid
*/
private void checkForClauseAllowingEmpty(FLWORExpression flwor, ForClause clause) throws XPathException {
if (!allowXPath30Syntax) {
grumble("The 'allowing empty' option requires XQuery 3.0");
}
SequenceType type = clause.getRangeVariable().getRequiredType();
if (!Cardinality.allowsZero(type.getCardinality())) {
warning("When 'allowing empty' is specified, the occurrence indicator on the range variable type should be '?'");
}
}
/**
* Parse a LetClause.
*
* [44] LetClause ::= <"let" "$"> VarName TypeDeclaration? ":=" ExprSingle
* ("," "$" VarName TypeDeclaration? ":=" ExprSingle)*
*
*
* @param clauseList - the components of the parsed LetClause are appended to the
* supplied list
* @throws XPathException if a static error is found
*/
private void parseLetClause(FLWORExpression flwor, List clauseList) throws XPathException {
boolean first = true;
do {
LetClause clause = new LetClause();
setLocation(clause, t.currentTokenStartOffset);
clause.setRepeated(containsLoopingClause(clauseList));
if (first) {
//clause.offset = t.currentTokenStartOffset;
}
clauseList.add(clause);
nextToken();
if (first) {
first = false;
} else {
//clause.offset = t.currentTokenStartOffset;
}
expect(Token.DOLLAR);
nextToken();
expect(Token.NAME);
String var = t.currentTokenValue;
StructuredQName varQName = makeStructuredQName(var, "");
SequenceType type = SequenceType.ANY_SEQUENCE;
nextToken();
if (t.currentToken == Token.AS) {
nextToken();
type = parseSequenceType();
}
LocalVariableBinding v = new LocalVariableBinding(varQName, type);
expect(Token.ASSIGN);
nextToken();
clause.initSequence(flwor, parseExprSingle());
clause.setRangeVariable(v);
declareRangeVariable(v);
} while (t.currentToken == Token.COMMA);
}
/**
* Parse a CountClause.
*
* [44] CountClause ::= <"count" "$"> VarName
*
*
* @param clauseList - the components of the parsed CountClause are appended to the
* supplied list
* @throws XPathException in the event of a syntax error
*/
private void parseCountClause(List clauseList) throws XPathException {
if (!allowXPath30Syntax) {
grumble("The count clause requires XQuery 3.0");
}
do {
CountClause clause = new CountClause();
setLocation(clause, t.currentTokenStartOffset);
clause.setRepeated(containsLoopingClause(clauseList));
clauseList.add(clause);
nextToken();
expect(Token.DOLLAR);
nextToken();
expect(Token.NAME);
String var = t.currentTokenValue;
StructuredQName varQName = makeStructuredQName(var, "");
SequenceType type = SequenceType.ANY_SEQUENCE;
nextToken();
LocalVariableBinding v = new LocalVariableBinding(varQName, type);
clause.setRangeVariable(v);
declareRangeVariable(v);
} while (t.currentToken == Token.COMMA);
}
/**
* Parse a Group By clause.
* Handles the full XQuery 3.0 syntax:
* "group by" ($varname ["collation" URILiteral]) [,...]
* @param clauseList the list of clauses of the FLWOR expression, to which this clause is added
* @throws XPathException if there is a syntax error
*
*/
private void parseGroupByClause(FLWORExpression flwor, List clauseList) throws XPathException {
if (!allowXPath30Syntax) {
grumble("Grouping requires XQuery 3.0");
}
GroupByClause clause = new GroupByClause(env.getConfiguration());
setLocation(clause, t.currentTokenStartOffset);
clause.setRepeated(containsLoopingClause(clauseList));
List variableNames = new ArrayList();
List collations = new ArrayList();
nextToken();
while (true) {
StructuredQName varQName = readVariableName();
if (t.currentToken == Token.ASSIGN) {
LetClause letClause = new LetClause();
clauseList.add(letClause);
nextToken();
SequenceType type = SequenceType.ANY_SEQUENCE;
if (t.currentToken == Token.AS) {
nextToken();
type = parseSequenceType();
}
LocalVariableBinding v = new LocalVariableBinding(varQName, type);
letClause.initSequence(flwor, parseExprSingle());
letClause.setRangeVariable(v);
declareRangeVariable(v);
}
variableNames.add(varQName);
if (isKeyword("collation")) {
nextToken();
expect(Token.STRING_LITERAL);
collations.add(t.currentTokenValue);
nextToken();
} else {
collations.add(env.getDefaultCollationName());
}
if (t.currentToken == Token.COMMA) {
nextToken();
} else {
break;
}
}
// Each of the variable names acts both as a variable reference (for a variable in the pre-grouping stream)
// and a variable declaration (for a variable in the post-grouping stream).
TupleExpression groupingTupleExpr = new TupleExpression();
TupleExpression retainedTupleExpr = new TupleExpression();
List groupingRefs = new ArrayList();
List retainedRefs = new ArrayList();
List groupedBindings = new ArrayList();
for (StructuredQName q : variableNames) {
boolean found = false;
search:
for (int i = clauseList.size() - 1; i >= 0; i--) {
for (LocalVariableBinding b : clauseList.get(i).getRangeVariables()) {
if (q.equals(b.getVariableQName())) {
groupedBindings.add(b);
groupingRefs.add(new LocalVariableReference(b));
found = true;
break search;
}
}
}
if (!found) {
grumble("The grouping variable " + q.getDisplayName() + " must be the name of a variable bound earlier in the FLWOR expression",
"XQST0094");
}
}
groupingTupleExpr.setVariables(groupingRefs);
clause.initGroupingTupleExpression(flwor, groupingTupleExpr);
List ungroupedBindings = new ArrayList();
for (int i = clauseList.size() - 1; i >= 0; i--) {
for (LocalVariableBinding b : clauseList.get(i).getRangeVariables()) {
if (!groupedBindings.contains(b)) {
ungroupedBindings.add(b);
retainedRefs.add(new LocalVariableReference(b));
}
}
}
retainedTupleExpr.setVariables(retainedRefs);
clause.initRetainedTupleExpression(flwor, retainedTupleExpr);
LocalVariableBinding[] bindings = new LocalVariableBinding[groupedBindings.size() + ungroupedBindings.size()];
int k = 0;
for (LocalVariableBinding b : groupedBindings) {
bindings[k] = new LocalVariableBinding(b.getVariableQName(), b.getRequiredType());
//declareRangeVariable(bindings[k]);
k++;
}
for (LocalVariableBinding b : ungroupedBindings) {
ItemType itemType = b.getRequiredType().getPrimaryType();
bindings[k] = new LocalVariableBinding(b.getVariableQName(),
SequenceType.makeSequenceType(itemType, StaticProperty.ALLOWS_ZERO_OR_MORE));
//declareRangeVariable(bindings[k]);
k++;
}
for (int z = groupedBindings.size(); z < bindings.length; z++) {
declareRangeVariable(bindings[z]);
}
for (int z = 0; z < groupedBindings.size(); z++) {
declareRangeVariable(bindings[z]);
}
clause.setVariableBindings(bindings);
GenericAtomicComparer[] comparers = new GenericAtomicComparer[collations.size()];
XPathContext context = env.makeEarlyEvaluationContext();
for (int i = 0; i < comparers.length; i++) {
StringCollator coll = env.getConfiguration().getCollation(collations.get(i));
comparers[i] = (GenericAtomicComparer) GenericAtomicComparer.makeAtomicComparer(
BuiltInAtomicType.ANY_ATOMIC, BuiltInAtomicType.ANY_ATOMIC, coll, context);
}
clause.setComparers(comparers);
clauseList.add(clause);
}
private StructuredQName readVariableName() throws XPathException {
expect(Token.DOLLAR);
nextToken();
expect(Token.NAME);
String name = t.currentTokenValue;
nextToken();
return makeStructuredQName(name, "");
}
/**
* Parse a tumbling or sliding window clause.
* @param clauseList the list of clauses of the FLWOR expression, to which this clause is aded
* @throws XPathException if there is a syntax error
*
*/
private void parseWindowClause(FLWORExpression flwor, List clauseList) throws XPathException {
if (!allowXPath30Syntax) {
grumble("The window clause requires XQuery 3.0");
}
WindowClause clause = new WindowClause();
setLocation(clause, t.currentTokenStartOffset);
clause.setRepeated(containsLoopingClause(clauseList));
clause.setIsSlidingWindow(t.currentToken == Token.FOR_SLIDING);
nextToken();
if (!isKeyword("window")) {
grumble("after 'sliding' or 'tumbling', expected 'window', but found " + currentTokenDisplay());
}
nextToken();
StructuredQName windowVarName = readVariableName();
SequenceType windowType = SequenceType.ANY_SEQUENCE;
if (t.currentToken == Token.AS) {
nextToken();
windowType = parseSequenceType();
}
LocalVariableBinding windowVar = new LocalVariableBinding(windowVarName, windowType);
clause.setVariableBinding(WindowClause.WINDOW_VAR, windowVar);
// We can't assume that all the items in the input sequence belong to the item type of the windows: test case SlidingWindowExpr507
SequenceType windowItemTypeMandatory = SequenceType.SINGLE_ITEM;
SequenceType windowItemTypeOptional = SequenceType.OPTIONAL_ITEM;
expect(Token.IN);
nextToken();
clause.initSequence(flwor, parseExprSingle());
if (!isKeyword("start")) {
grumble("in window clause, expected 'start', but found " + currentTokenDisplay());
}
t.setState(Tokenizer.BARE_NAME_STATE);
nextToken();
if (t.currentToken == Token.DOLLAR) {
LocalVariableBinding startItemVar = new LocalVariableBinding(readVariableName(), windowItemTypeMandatory);
clause.setVariableBinding(WindowClause.START_ITEM, startItemVar);
declareRangeVariable(startItemVar);
}
if (isKeyword("at")) {
nextToken();
LocalVariableBinding startPositionVar = new LocalVariableBinding(readVariableName(), SequenceType.SINGLE_INTEGER);
clause.setVariableBinding(WindowClause.START_ITEM_POSITION, startPositionVar);
declareRangeVariable(startPositionVar);
}
if (isKeyword("previous")) {
nextToken();
LocalVariableBinding startPreviousItemVar = new LocalVariableBinding(readVariableName(), windowItemTypeOptional);
clause.setVariableBinding(WindowClause.START_PREVIOUS_ITEM, startPreviousItemVar);
declareRangeVariable(startPreviousItemVar);
}
if (isKeyword("next")) {
nextToken();
LocalVariableBinding startNextItemVar = new LocalVariableBinding(readVariableName(), windowItemTypeOptional);
clause.setVariableBinding(WindowClause.START_NEXT_ITEM, startNextItemVar);
declareRangeVariable(startNextItemVar);
}
if (!isKeyword("when")) {
grumble("Expected 'when' condition for window start, but found " + currentTokenDisplay());
}
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
clause.initStartCondition(flwor, parseExprSingle());
if (isKeyword("only")) {
clause.setIncludeUnclosedWindows(false);
nextToken();
}
if (isKeyword("end")) {
t.setState(Tokenizer.BARE_NAME_STATE);
nextToken();
if (t.currentToken == Token.DOLLAR) {
LocalVariableBinding endItemVar = new LocalVariableBinding(readVariableName(), windowItemTypeMandatory);
clause.setVariableBinding(WindowClause.END_ITEM, endItemVar);
declareRangeVariable(endItemVar);
}
if (isKeyword("at")) {
nextToken();
LocalVariableBinding endPositionVar = new LocalVariableBinding(readVariableName(), SequenceType.SINGLE_INTEGER);
clause.setVariableBinding(WindowClause.END_ITEM_POSITION, endPositionVar);
declareRangeVariable(endPositionVar);
}
if (isKeyword("previous")) {
nextToken();
LocalVariableBinding endPreviousItemVar = new LocalVariableBinding(readVariableName(), windowItemTypeOptional);
clause.setVariableBinding(WindowClause.END_PREVIOUS_ITEM, endPreviousItemVar);
declareRangeVariable(endPreviousItemVar);
}
if (isKeyword("next")) {
nextToken();
LocalVariableBinding endNextItemVar = new LocalVariableBinding(readVariableName(), windowItemTypeOptional);
clause.setVariableBinding(WindowClause.END_NEXT_ITEM, endNextItemVar);
declareRangeVariable(endNextItemVar);
}
if (!isKeyword("when")) {
grumble("Expected 'when' condition for window end, but found " + currentTokenDisplay());
}
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
clause.initEndCondition(flwor, parseExprSingle());
} else {
// no "end" condition found
if (clause.isSlidingWindow()) {
grumble("A sliding window requires an end condition");
}
}
declareRangeVariable(windowVar);
clauseList.add(clause);
}
/**
* Make a string-join expression that concatenates the string-values of items in
* a sequence with intervening spaces. This may be simplified later as a result
* of type-checking.
*
* @param exp the base expression, evaluating to a sequence
* @param env the static context
* @return a call on string-join to create a string containing the
* representations of the items in the sequence separated by spaces.
*/
/*@Nullable*/
public static Expression makeStringJoin(Expression exp, /*@NotNull*/ StaticContext env) throws XPathException {
exp = Atomizer.makeAtomizer(exp);
ItemType t = exp.getItemType();
if (!t.equals(BuiltInAtomicType.STRING) && !t.equals(BuiltInAtomicType.UNTYPED_ATOMIC)) {
exp = new AtomicSequenceConverter(exp, BuiltInAtomicType.STRING);
((AtomicSequenceConverter) exp).allocateConverter(env.getConfiguration(), false);
}
if (exp.getCardinality() == StaticProperty.EXACTLY_ONE) {
return exp;
} else {
RetainedStaticContext rsc = new RetainedStaticContext(env);
Expression fn = SystemFunction.makeCall("string-join", rsc, exp, new StringLiteral(StringValue.SINGLE_SPACE));
ExpressionTool.copyLocationInfo(exp, fn);
return fn;
}
}
/**
* Parse the "order by" clause.
* [46] OrderByClause ::= (<"order" "by"> | <"stable" "order" "by">) OrderSpecList
* [47] OrderSpecList ::= OrderSpec ("," OrderSpec)*
* [48] OrderSpec ::= ExprSingle OrderModifier
* [49] OrderModifier ::= ("ascending" | "descending")?
* (<"empty" "greatest"> | <"empty" "least">)?
* ("collation" StringLiteral)?
*
* @return a list of sort specifications (SortSpec), one per sort key
* @throws XPathException if parsing fails
*/
/*@NotNull*/
private List parseSortDefinition() throws XPathException {
List sortSpecList = new ArrayList(5);
while (true) {
SortSpec sortSpec = new SortSpec();
sortSpec.sortKey = parseExprSingle();
sortSpec.ascending = true;
sortSpec.emptyLeast = ((QueryModule) env).isEmptyLeast();
sortSpec.collation = env.getDefaultCollationName();
//t.setState(t.BARE_NAME_STATE);
if (isKeyword("ascending")) {
nextToken();
} else if (isKeyword("descending")) {
sortSpec.ascending = false;
nextToken();
}
if (isKeyword("empty")) {
nextToken();
if (isKeyword("greatest")) {
sortSpec.emptyLeast = false;
nextToken();
} else if (isKeyword("least")) {
sortSpec.emptyLeast = true;
nextToken();
} else {
grumble("'empty' must be followed by 'greatest' or 'least'");
}
}
if (isKeyword("collation")) {
sortSpec.collation = readCollationName();
}
sortSpecList.add(sortSpec);
if (t.currentToken == Token.COMMA) {
nextToken();
} else {
break;
}
}
return sortSpecList;
}
protected String readCollationName() throws XPathException {
nextToken();
expect(Token.STRING_LITERAL);
String collationName = URILiteral(t.currentTokenValue);
URI collationURI;
try {
collationURI = new URI(collationName);
if (!collationURI.isAbsolute()) {
URI base = new URI(env.getStaticBaseURI());
collationURI = base.resolve(collationURI);
collationName = collationURI.toString();
}
} catch (URISyntaxException err) {
grumble("Collation name '" + collationName + "' is not a valid URI", "XQST0046");
collationName = NamespaceConstant.CODEPOINT_COLLATION_URI;
}
nextToken();
return collationName;
}
private static class SortSpec {
/*@Nullable*/ public Expression sortKey;
public boolean ascending;
public boolean emptyLeast;
public String collation;
}
/**
* Parse a Typeswitch Expression.
* This construct is XQuery-only.
* TypeswitchExpr ::=
* "typeswitch" "(" Expr ")"
* CaseClause+
* "default" ("$" VarName)? "return" ExprSingle
* CaseClause ::=
* "case" ("$" VarName "as")? SequenceType "return" ExprSingle
*
* @throws XPathException if parsing fails
*/
/*@NotNull*/
protected Expression parseTypeswitchExpression() throws XPathException {
// On entry, the "(" has already been read
int offset = t.currentTokenStartOffset;
nextToken();
Expression operand = parseExpression();
List> types = new ArrayList>(10);
List actions = new ArrayList(10);
expect(Token.RPAR);
nextToken();
// The code generated takes the form:
// let $zzz := operand return
// if ($zzz instance of t1) then action1
// else if ($zzz instance of t2) then action2
// else default-action
//
// If a variable is declared in a case clause or default clause,
// then "action-n" takes the form
// let $v as type := $zzz return action-n
// we were generating "let $v as type := $zzz return action-n" but this gives a compile time error if
// there's a case clause that specifies an impossible type.
LetExpression outerLet = makeLetExpression();
outerLet.setRequiredType(SequenceType.ANY_SEQUENCE);
outerLet.setVariableQName(new StructuredQName("zz", NamespaceConstant.SAXON, "zz_typeswitchVar"));
outerLet.setSequence(operand);
while (t.currentToken == Token.CASE) {
int caseOffset = t.currentTokenStartOffset;
List typeList;
Expression action;
nextToken();
if (t.currentToken == Token.DOLLAR) {
nextToken();
expect(Token.NAME);
final String var = t.currentTokenValue;
final StructuredQName varQName = makeStructuredQName(var, "");
nextToken();
expect(Token.AS);
nextToken();
typeList = parseSequenceTypeList();
action = makeTracer(caseOffset,
parseTypeswitchReturnClause(varQName, outerLet),
LocationKind.CASE_EXPRESSION,
varQName);
if (action instanceof TraceExpression) {
((TraceExpression) action).setProperty("type", typeList.get(0).toString());
}
} else {
typeList = parseSequenceTypeList();
action = makeTracer(caseOffset, parseExprSingle(), LocationKind.CASE_EXPRESSION, null);
if (action instanceof TraceExpression) {
((TraceExpression) action).setProperty("type", typeList.get(0).toString());
}
}
if (typeList.size() > 1 && !allowXPath30Syntax) {
grumble("Typeswitch with a list of types separated by '|' requires XQuery 3.0 to be enabled");
}
types.add(typeList);
actions.add(action);
}
if (types.isEmpty()) {
grumble("At least one case clause is required in a typeswitch");
}
expect(Token.DEFAULT);
final int defaultOffset = t.currentTokenStartOffset;
nextToken();
Expression defaultAction;
if (t.currentToken == Token.DOLLAR) {
nextToken();
expect(Token.NAME);
final String var = t.currentTokenValue;
final StructuredQName varQName = makeStructuredQName(var, "");
nextToken();
expect(Token.RETURN);
nextToken();
defaultAction = makeTracer(defaultOffset,
parseTypeswitchReturnClause(varQName, outerLet),
LocationKind.DEFAULT_EXPRESSION,
varQName);
} else {
t.treatCurrentAsOperator();
expect(Token.RETURN);
nextToken();
defaultAction = makeTracer(defaultOffset, parseExprSingle(), LocationKind.DEFAULT_EXPRESSION, null);
}
Expression lastAction = defaultAction;
// Note, the ragged "choose" later gets flattened into a single-level choose, saving stack space
for (int i = types.size() - 1; i >= 0; i--) {
final LocalVariableReference var = new LocalVariableReference(outerLet);
setLocation(var);
Expression ioe = new InstanceOfExpression(var, types.get(i).get(0));
for (int j = 1; j < types.get(i).size(); j++) {
ioe = new OrExpression(ioe, new InstanceOfExpression(var.copy(new RebindingMap()), types.get(i).get(j)));
}
setLocation(ioe);
final Expression ife = Choose.makeConditional(ioe, actions.get(i), lastAction);
setLocation(ife);
lastAction = ife;
}
outerLet.setAction(lastAction);
return makeTracer(offset, outerLet, LocationKind.TYPESWITCH_EXPRESSION, null);
}
/*@NotNull*/
private List parseSequenceTypeList() throws XPathException {
List typeList = new ArrayList();
while (true) {
SequenceType type = parseSequenceType();
typeList.add(type);
t.treatCurrentAsOperator();
if (t.currentToken == Token.UNION) {
nextToken();
} else {
break;
}
}
expect(Token.RETURN);
nextToken();
return typeList;
}
/*@NotNull*/
private Expression parseTypeswitchReturnClause(StructuredQName varQName, LetExpression outerLet)
throws XPathException {
Expression action;
// t.treatCurrentAsOperator();
// expect(Token.RETURN);
// nextToken();
LetExpression innerLet = makeLetExpression();
innerLet.setRequiredType(SequenceType.ANY_SEQUENCE);
innerLet.setVariableQName(varQName);
innerLet.setSequence(new LocalVariableReference(outerLet));
declareRangeVariable(innerLet);
action = parseExprSingle();
undeclareRangeVariable();
innerLet.setAction(action);
return innerLet;
// if (Literal.isEmptySequence(action)) {
// // The purpose of simplifying this now is that () is allowed in a branch even in XQuery Update when
// // other branches of the typeswitch are updating.
// return action;
// } else {
// return innerLet;
// }
}
/**
* Parse a Switch Expression.
* This construct is XQuery-3.0-only.
* SwitchExpr ::= "switch" "(" Expr ")" SwitchCaseClause+ "default" "return" ExprSingle
* SwitchCaseClause ::= ("case" ExprSingle)+ "return" ExprSingle
*/
/*@NotNull*/
protected Expression parseSwitchExpression() throws XPathException {
if (!allowXPath30Syntax) {
grumble("switch requires XQuery 3.0");
}
// On entry, the "(" has already been read
int offset = t.currentTokenStartOffset;
nextToken();
Expression operand = parseExpression();
expect(Token.RPAR);
nextToken();
List conditions = new ArrayList(10);
List actions = new ArrayList(10);
// The code generated takes the form:
// let $zzz := zero-or-one(atomize(operand)) return
// choose
// when ($zzz eq t1) then action1
// when ($zzz eq t2) then action2
// when (true) default-action
//
// We rely on the optimizer to convert this to a SwitchExpression in the case where all the case clauses
// are literal constants.
LetExpression outerLet = makeLetExpression();
outerLet.setRequiredType(SequenceType.OPTIONAL_ATOMIC);
outerLet.setVariableQName(new StructuredQName("zz", NamespaceConstant.SAXON, "zz_switchVar"));
outerLet.setSequence(Atomizer.makeAtomizer(operand));
while (true) {
//int caseOffset = t.currentTokenStartOffset;
List caseExpressions = new ArrayList(4);
expect(Token.CASE);
do {
nextToken();
Expression c = parseExprSingle();
caseExpressions.add(c);
} while (t.currentToken == Token.CASE);
expect(Token.RETURN);
nextToken();
Expression action = parseExprSingle();
for (int i = 0; i < caseExpressions.size(); i++) {
EquivalenceComparison vc = new EquivalenceComparison(
new LocalVariableReference(outerLet),
Token.FEQ,
caseExpressions.get(i));
if (i == 0) {
conditions.add(vc);
actions.add(action);
} else {
OrExpression orExpr = new OrExpression(conditions.remove(conditions.size() - 1), vc);
conditions.add(orExpr);
}
//actions.add((i==0 ? action : action.copy()));
}
if (t.currentToken != Token.CASE) {
break;
}
}
expect(Token.DEFAULT);
nextToken();
expect(Token.RETURN);
nextToken();
Expression defaultExpr = parseExprSingle();
conditions.add(Literal.makeLiteral(BooleanValue.TRUE));
actions.add(defaultExpr);
Choose choice = new Choose(
conditions.toArray(new Expression[conditions.size()]),
actions.toArray(new Expression[conditions.size()]));
outerLet.setAction(choice);
return makeTracer(offset, outerLet, LocationKind.SWITCH_EXPRESSION, null);
}
/**
* Parse a Validate Expression.
* This construct is XQuery-only. The syntax allows:
* validate mode? { Expr }
* mode ::= "strict" | "lax"
*
* @throws XPathException if parsing fails
*/
/*@NotNull*/
protected Expression parseValidateExpression() throws XPathException {
int offset = t.currentTokenStartOffset;
int mode = Validation.STRICT;
boolean foundCurly = false;
SchemaType requiredType = null;
ensureSchemaAware("validate expression");
switch (t.currentToken) {
case Token.VALIDATE_STRICT:
mode = Validation.STRICT;
nextToken();
break;
case Token.VALIDATE_LAX:
mode = Validation.LAX;
nextToken();
break;
case Token.VALIDATE_TYPE:
if (XQUERY10.equals(queryVersion)) {
grumble("validate-as-type requires XQuery 3.0");
}
mode = Validation.BY_TYPE;
nextToken();
expect(Token.KEYWORD_CURLY);
if (!NameChecker.isQName(t.currentTokenValue)) {
grumble("Schema type name expected after 'validate type");
}
requiredType = env.getConfiguration().getSchemaType(
makeStructuredQName(t.currentTokenValue, env.getDefaultElementNamespace()));
if (requiredType == null) {
grumble("Unknown schema type " + t.currentTokenValue, "XQST0104");
}
foundCurly = true;
break;
case Token.KEYWORD_CURLY:
if (t.currentTokenValue.equals("validate")) {
mode = Validation.STRICT;
} else {
throw new AssertionError("shouldn't be parsing a validate expression");
}
foundCurly = true;
}
if (!foundCurly) {
expect(Token.LCURLY);
}
nextToken();
Expression exp = parseExpression();
if (exp instanceof ParentNodeConstructor) {
((ParentNodeConstructor) exp).setValidationAction(mode, (mode == Validation.BY_TYPE ? requiredType : null));
} else {
// the expression must return a single element or document node. The type-
// checking machinery can't handle a union type, so we just check that it's
// a node for now. Because we are reusing XSLT copy-of code, we need
// an ad-hoc check that the node is of the right kind.
// below code moved to XQuery-specific path in CopyOf
// try {
// RoleLocator role = new RoleLocator(RoleLocator.TYPE_OP, "validate", 0);
// role.setErrorCode("XQTY0030");
// setLocation(exp);
// exp = TypeChecker.staticTypeCheck(exp,
// SequenceType.SINGLE_NODE,
// false,
// role, ExpressionVisitor.make(env, getExecutable()));
// } catch (XPathException err) {
// grumble(err.getMessage(), err.getErrorCodeQName(), -1);
// }
exp = new CopyOf(exp, true, mode, requiredType, true);
setLocation(exp);
((CopyOf) exp).setRequireDocumentOrElement(true);
}
expect(Token.RCURLY);
t.lookAhead(); // always done manually after an RCURLY
nextToken();
return makeTracer(offset, exp, LocationKind.VALIDATE_EXPRESSION, null);
}
/**
* Parse an Extension Expression.
* Syntax: "(#" QName arbitrary-text "#)")+ "{" expr? "}"
*
* @throws XPathException if parsing fails
*/
/*@NotNull*/
protected Expression parseExtensionExpression() throws XPathException {
SchemaType requiredType = null;
CharSequence trimmed = Whitespace.removeLeadingWhitespace(t.currentTokenValue);
int c = 0;
int len = trimmed.length();
while (c < len && " \t\r\n".indexOf(trimmed.charAt(c)) < 0) {
c++;
}
String qname = trimmed.subSequence(0, c).toString();
String pragmaContents = "";
while (c < len && " \t\r\n".indexOf(trimmed.charAt(c)) >= 0) {
c++;
}
if (c < len) {
pragmaContents = trimmed.subSequence(c, len).toString();
}
boolean validateType = false;
boolean streaming = false;
String uri;
String localName;
if (qname.startsWith("Q{")) {
StructuredQName sq = parseExtendedQName(qname);
uri = sq.getURI();
localName = sq.getLocalPart();
} else if (!NameChecker.isQName(qname)) {
grumble("First token in pragma must be a valid QName, terminated by whitespace");
return new ErrorExpression();
} else {
StructuredQName name = makeStructuredQName(qname, "");
uri = name.getURI();
localName = name.getLocalPart();
}
if (uri.equals(NamespaceConstant.SAXON)) {
if (localName.equals("validate-type")) {
if (!env.getConfiguration().isLicensedFeature(Configuration.LicenseFeature.ENTERPRISE_XQUERY)) {
warning("Ignoring saxon:validate-type. To use this feature " +
"you need the Saxon-EE processor from http://www.saxonica.com/");
} else {
String typeName = Whitespace.trim(pragmaContents);
if (!NameChecker.isQName(typeName)) {
grumble("Schema type name expected in saxon:validate-type pragma: found " + Err.wrap(typeName));
}
assert typeName != null;
requiredType = env.getConfiguration().getSchemaType(
makeStructuredQName(typeName, env.getDefaultElementNamespace()));
if (requiredType == null) {
grumble("Unknown schema type " + typeName);
}
validateType = true;
}
} else if (localName.equals("stream")) {
if (!env.getConfiguration().isLicensedFeature(Configuration.LicenseFeature.ENTERPRISE_XQUERY)) {
warning("Ignoring saxon:stream. To use this feature " +
"you need the Saxon-EE processor from http://www.saxonica.com/");
} else {
streaming = true;
}
} else {
warning("Ignored pragma " + qname + " (unrecognized Saxon pragma)");
}
} else if (uri.isEmpty()) {
grumble("The QName identifying an option declaration must be prefixed", "XPST0081");
}
nextToken();
Expression expr;
if (t.currentToken == Token.PRAGMA) {
expr = parseExtensionExpression();
} else {
expect(Token.LCURLY);
nextToken();
if (t.currentToken == Token.RCURLY) {
t.lookAhead(); // always done manually after an RCURLY
nextToken();
grumble("Unrecognized pragma, with no fallback expression", "XQST0079");
}
expr = parseExpression();
expect(Token.RCURLY);
t.lookAhead(); // always done manually after an RCURLY
nextToken();
}
if (validateType) {
if (expr instanceof ParentNodeConstructor) {
((ParentNodeConstructor) expr).setValidationAction(Validation.BY_TYPE, requiredType);
return expr;
} else if (expr instanceof AttributeCreator) {
if (!(requiredType instanceof SimpleType)) {
grumble("The type used for validating an attribute must be a simple type");
}
//noinspection ConstantConditions
((AttributeCreator) expr).setSchemaType((SimpleType) requiredType);
((AttributeCreator) expr).setValidationAction(Validation.BY_TYPE);
return expr;
} else {
CopyOf copy = new CopyOf(expr, true, Validation.BY_TYPE, requiredType, true);
copy.setLocation(makeLocation());
return copy;
}
} else if (streaming) {
CopyOf copy = new CopyOf(expr, true, Validation.PRESERVE, null, true);
copy.setLocation(makeLocation());
copy.setReadOnce(true);
return copy;
} else {
return expr;
}
}
/**
* Convert an EQName in format Q{uri}local to a StructuredQName. We have already established
* that the string starts with "Q{"
*
*
* @param in the string to be checked/parsed
* @return the expanded QName
* @throws XPathException if the input syntax is incorrect
*/
/*@NotNull*/
private static StructuredQName parseExtendedQName(/*@NotNull*/ String in) throws XPathException {
if (in.length() < 4) {
invalidExtendedQName(in, "too short");
}
int end = in.indexOf('}', 1);
if (end < 0) {
invalidExtendedQName(in, "no closing '}'");
}
if (end + 1 >= in.length()) {
invalidExtendedQName(in, "no local name after URI");
}
String uri = in.substring(2, end);
String localName = in.substring(end + 2);
if (!NameChecker.isValidNCName(localName)) {
invalidExtendedQName(in, "invalid local name after colon");
}
return new StructuredQName("", uri, localName);
}
private static void invalidExtendedQName(String in, String msg) throws XPathException {
throw new XPathException("Invalid EQName " + in + " - " + msg, "XPST0003");
}
/**
* Parse a node constructor. This is allowed only in XQuery. This method handles
* both the XML-like "direct" constructors, and the XQuery-based "computed"
* constructors.
*
* @return an Expression for evaluating the parsed constructor
* @throws XPathException in the event of a syntax error.
*/
/*@NotNull*/
protected Expression parseConstructor() throws XPathException {
int offset = t.currentTokenStartOffset;
switch (t.currentToken) {
case Token.TAG:
Expression tag = parsePseudoXML(false);
lookAhead();
t.setState(Tokenizer.OPERATOR_STATE);
nextToken();
return tag;
case Token.KEYWORD_CURLY:
String nodeKind = t.currentTokenValue;
if (nodeKind.equals("validate")) {
grumble("A validate expression is not allowed within a path expression");
//if (nodeKind.equals("validate")) {
// this allows a validate{} expression to appear as an operand of '/', which the grammar does not allow
// return parseValidateExpression();
} else if (nodeKind.equals("ordered") || nodeKind.equals("unordered")) {
// these are currently no-ops in Saxon
nextToken();
Expression content;
if (t.currentToken == Token.RCURLY && allowXPath31Syntax) {
content = Literal.makeEmptySequence();
} else {
content = parseExpression();
}
expect(Token.RCURLY);
lookAhead(); // must be done manually after an RCURLY
nextToken();
return content;
} else if (nodeKind.equals("document")) {
return parseDocumentConstructor(offset);
} else if ("element".equals(nodeKind)) {
return parseComputedElementConstructor(offset);
} else if ("attribute".equals(nodeKind)) {
return parseComputedAttributeConstructor(offset);
} else if ("text".equals(nodeKind)) {
return parseTextNodeConstructor(offset);
} else if ("comment".equals(nodeKind)) {
return parseCommentConstructor(offset);
} else if ("processing-instruction".equals(nodeKind)) {
return parseProcessingInstructionConstructor(offset);
} else if ("namespace".equals(nodeKind)) {
return parseNamespaceConstructor(offset);
} else {
grumble("Unrecognized node constructor " + t.currentTokenValue + "{}");
}
case Token.ELEMENT_QNAME:
return parseNamedElementConstructor(offset);
case Token.ATTRIBUTE_QNAME:
return parseNamedAttributeConstructor(offset);
case Token.NAMESPACE_QNAME:
return parseNamedNamespaceConstructor(offset);
case Token.PI_QNAME:
return parseNamedProcessingInstructionConstructor(offset);
}
return new ErrorExpression();
}
/**
* Parse document constructor: document {...}
*
* @param offset the location in the source query
* @return the document constructor instruction
* @throws XPathException if parsing fails
*/
/*@NotNull*/
private Expression parseDocumentConstructor(int offset) throws XPathException {
nextToken();
Expression content = parseExpression();
expect(Token.RCURLY);
lookAhead(); // must be done manually after an RCURLY
nextToken();
DocumentInstr doc = new DocumentInstr(false, null);
if (!((QueryModule) env).isPreserveNamespaces()) {
content = new CopyOf(content, false, Validation.PRESERVE, null, true);
}
doc.setValidationAction(((QueryModule) env).getConstructionMode(), null);
doc.setContentExpression(content);
setLocation(doc, offset);
return doc;
}
/**
* Parse an element constructor of the form
* element {expr} {expr}
*
* @param offset location of the expression in the source query
* @return the compiled instruction
* @throws XPathException if a static error is found
*/
/*@NotNull*/
private Expression parseComputedElementConstructor(int offset) throws XPathException {
nextToken();
// get the expression that yields the element name
Expression name = parseExpression();
expect(Token.RCURLY);
lookAhead(); // must be done manually after an RCURLY
nextToken();
expect(Token.LCURLY);
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
Expression content = null;
if (t.currentToken != Token.RCURLY) {
// get the expression that yields the element content
content = parseExpression();
// if the child expression creates another element,
// suppress validation, as the parent already takes care of it
if (content instanceof ElementCreator && ((ElementCreator) content).getSchemaType() == null) {
// TODO: does this cover all cases? E.g. the child element might match a wildcard in the parent declaration.
((ElementCreator) content).setValidationAction(Validation.PRESERVE, null);
}
expect(Token.RCURLY);
}
lookAhead(); // done manually after an RCURLY
nextToken();
Instruction inst;
if (name instanceof Literal) {
GroundedValue vName = ((Literal) name).getValue();
// if element name is supplied as a literal, treat it like a direct element constructor
NodeName elemName;
if (vName instanceof StringValue && !(vName instanceof AnyURIValue)) {
String lex = ((StringValue) vName).getStringValue();
try {
elemName = makeNodeName(lex, true);
elemName.allocateNameCode(env.getConfiguration().getNamePool());
} catch (XPathException staticError) {
String code = staticError.getErrorCodeLocalPart();
if ("XPST0008".equals(code) || "XPST0081".equals(code)) {
staticError.setErrorCode("XQDY0074");
} else if ("XPST0003".equals(code)) {
//staticError.setErrorCode("XQDY0074");
grumble("Invalid QName in element constructor: " + lex, "XQDY0074", offset);
return new ErrorExpression();
}
staticError.setLocator(makeLocation());
staticError.setIsStaticError(false);
return new ErrorExpression(staticError);
} catch (QNameException qerr) {
grumble("Invalid QName in element constructor: " + lex, "XQDY0074", offset);
return new ErrorExpression();
}
} else if (vName instanceof QualifiedNameValue) {
String uri = ((QualifiedNameValue) vName).getNamespaceURI();
elemName = new FingerprintedQName("", uri, ((QualifiedNameValue) vName).getLocalName());
elemName.allocateNameCode(env.getConfiguration().getNamePool());
} else {
grumble("Element name must be either a string or a QName", "XPTY0004", offset);
return new ErrorExpression();
}
inst = new FixedElement(elemName,
((QueryModule) env).getActiveNamespaceCodes(),
((QueryModule) env).isInheritNamespaces(),
true, null,
((QueryModule) env).getConstructionMode());
if (content == null) {
content = Literal.makeEmptySequence();
}
if (!((QueryModule) env).isPreserveNamespaces()) {
content = new CopyOf(content, false, Validation.PRESERVE, null, true);
}
((FixedElement) inst).setContentExpression(content);
setLocation(inst, offset);
//makeContentConstructor(content, (InstructionWithChildren) inst, offset);
return makeTracer(offset, inst, LocationKind.LITERAL_RESULT_ELEMENT, elemName.getStructuredQName());
} else {
// it really is a computed element constructor: save the namespace context
NamespaceResolver ns = new NamespaceResolverWithDefault(
env.getNamespaceResolver(),
env.getDefaultElementNamespace());
inst = new ComputedElement(name, null, null,
((QueryModule) env).getConstructionMode(),
((QueryModule) env).isInheritNamespaces(),
true);
setLocation(inst);
if (content == null) {
content = Literal.makeEmptySequence();
}
if (!((QueryModule) env).isPreserveNamespaces()) {
content = new CopyOf(content, false, Validation.PRESERVE, null, true);
}
((ComputedElement) inst).setContentExpression(content);
setLocation(inst, offset);
//makeContentConstructor(content, (InstructionWithChildren) inst, offset);
return makeTracer(offset, inst, StandardNames.XSL_ELEMENT, null);
}
}
/**
* Parse an element constructor of the form
* element qname { expr }
*
* @param offset the position in the source query
* @return the compiled instruction
* @throws XPathException if parsing fails
*/
private Expression parseNamedElementConstructor(int offset) throws XPathException {
int nameCode = makeNameCode(t.currentTokenValue, true);
Expression content = null;
nextToken();
if (t.currentToken != Token.RCURLY) {
content = parseExpression();
expect(Token.RCURLY);
}
lookAhead(); // after an RCURLY
nextToken();
FixedElement el2 = new FixedElement(new CodedName(nameCode, env.getConfiguration().getNamePool()),
((QueryModule) env).getActiveNamespaceCodes(),
((QueryModule) env).isInheritNamespaces(),
true, null,
((QueryModule) env).getConstructionMode());
setLocation(el2, offset);
if (content == null) {
content = Literal.makeEmptySequence();
}
if (!((QueryModule) env).isPreserveNamespaces()) {
content = new CopyOf(content, false, Validation.PRESERVE, null, true);
}
el2.setContentExpression(content);
//makeContentConstructor(content, el2, offset);
return makeTracer(offset, el2, LocationKind.LITERAL_RESULT_ELEMENT,
env.getConfiguration().getNamePool().getStructuredQName(nameCode));
}
/**
* Parse an attribute constructor of the form
* attribute {expr} {expr}
*
* @param offset position of the expression in the input
* @return the compiled instruction
* @throws XPathException if a static error is encountered
*/
/*@NotNull*/
private Expression parseComputedAttributeConstructor(int offset) throws XPathException {
nextToken();
Expression name = parseExpression();
expect(Token.RCURLY);
lookAhead(); // must be done manually after an RCURLY
nextToken();
expect(Token.LCURLY);
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
Expression content = null;
if (t.currentToken != Token.RCURLY) {
content = parseExpression();
expect(Token.RCURLY);
}
lookAhead(); // after an RCURLY
nextToken();
if (name instanceof Literal) {
GroundedValue vName = ((Literal) name).getValue();
if (vName instanceof StringValue && !(vName instanceof AnyURIValue)) {
String lex = ((StringValue) vName).getStringValue();
if (lex.equals("xmlns") || lex.startsWith("xmlns:")) {
grumble("Cannot create a namespace using an attribute constructor", "XQDY0044", offset);
}
NodeName attributeName;
try {
attributeName = makeNodeName(lex, false);
} catch (XPathException staticError) {
String code = staticError.getErrorCodeLocalPart();
staticError.setLocator(makeLocation());
if ("XPST0008".equals(code) || "XPST0081".equals(code)) {
staticError.setErrorCode("XQDY0074");
} else if ("XPST0003".equals(code)) {
grumble("Invalid QName in attribute constructor: " + lex, "XQDY0074", offset);
return new ErrorExpression();
}
throw staticError;
} catch (QNameException err) {
grumble("Invalid QName in attribute constructor: " + lex, "XQDY0074", offset);
return new ErrorExpression();
}
FixedAttribute fatt = new FixedAttribute(attributeName,
Validation.STRIP,
null);
fatt.setRejectDuplicates();
makeSimpleContent(content, fatt, offset);
return makeTracer(offset, fatt, StandardNames.XSL_ATTRIBUTE, null);
} else if (vName instanceof QNameValue) {
QNameValue qnv = (QNameValue) vName;
NodeName attributeName = new FingerprintedQName(
qnv.getPrefix(), qnv.getNamespaceURI(), qnv.getLocalName());
attributeName.allocateNameCode(env.getConfiguration().getNamePool());
FixedAttribute fatt = new FixedAttribute(attributeName,
Validation.STRIP,
null);
fatt.setRejectDuplicates();
makeSimpleContent(content, fatt, offset);
return makeTracer(offset, fatt, StandardNames.XSL_ATTRIBUTE, null);
}
}
ComputedAttribute att = new ComputedAttribute(name,
null,
env.getNamespaceResolver(),
Validation.STRIP,
null,
true);
att.setRejectDuplicates();
makeSimpleContent(content, att, offset);
return makeTracer(offset, att, StandardNames.XSL_ATTRIBUTE, null);
}
/**
* Parse an attribute constructor of the form
* attribute name {expr}
*
* @param offset position of the expression in the source
* @return the parsed expression
* @throws XPathException if a static error is found
*/
private Expression parseNamedAttributeConstructor(int offset) throws XPathException {
String warning = null;
if (t.currentTokenValue.equals("xmlns") || t.currentTokenValue.startsWith("xmlns:")) {
warning = "Cannot create a namespace declaration using an attribute constructor";
}
NodeName attributeName;
try {
attributeName = makeNodeName(t.currentTokenValue, false);
} catch (QNameException e) {
throw new XPathException(e);
}
if (!attributeName.getURI().equals("") && attributeName.getPrefix().equals("")) {
// This must be because the name was given as Q{uri}local. Invent a prefix.
attributeName = new FingerprintedQName("_", attributeName.getURI(), attributeName.getLocalPart());
}
Expression attContent = null;
nextToken();
if (t.currentToken != Token.RCURLY) {
attContent = parseExpression();
expect(Token.RCURLY);
}
lookAhead(); // after an RCURLY
nextToken();
if (warning == null) {
FixedAttribute att2 = new FixedAttribute(attributeName,
Validation.STRIP,
null);
att2.setRejectDuplicates();
makeSimpleContent(attContent, att2, offset);
return makeTracer(offset, att2, LocationKind.LITERAL_RESULT_ATTRIBUTE, attributeName.getStructuredQName());
} else {
warning(warning);
return new ErrorExpression(warning, "XQDY0044", false);
}
}
private Expression parseTextNodeConstructor(int offset) throws XPathException {
nextToken();
Expression value;
if (t.currentToken == Token.RCURLY && allowXPath31Syntax) {
value = Literal.makeEmptySequence();
} else {
value = parseExpression();
}
expect(Token.RCURLY);
lookAhead(); // after an RCURLY
nextToken();
Expression select = stringify(value, true, env.getConfiguration());
ValueOf vof = new ValueOf(select, false, true);
setLocation(vof, offset);
return makeTracer(offset, vof, StandardNames.XSL_TEXT, null);
}
private Expression parseCommentConstructor(int offset) throws XPathException {
nextToken();
Expression value;
if (t.currentToken == Token.RCURLY && allowXPath31Syntax) {
value = Literal.makeEmptySequence();
} else {
value = parseExpression();
}
expect(Token.RCURLY);
lookAhead(); // after an RCURLY
nextToken();
Comment com = new Comment();
makeSimpleContent(value, com, offset);
return makeTracer(offset, com, StandardNames.XSL_COMMENT, null);
}
/**
* Parse a processing instruction constructor of the form
* processing-instruction {expr} {expr}
*
* @param offset the position of the expression in the source query
* @return the compiled instruction
* @throws XPathException if parsing fails
*/
private Expression parseProcessingInstructionConstructor(int offset) throws XPathException {
nextToken();
Expression name = parseExpression();
expect(Token.RCURLY);
lookAhead(); // must be done manually after an RCURLY
nextToken();
expect(Token.LCURLY);
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
Expression content = null;
if (t.currentToken != Token.RCURLY) {
content = parseExpression();
expect(Token.RCURLY);
}
lookAhead(); // after an RCURLY
nextToken();
ProcessingInstruction pi = new ProcessingInstruction(name);
makeSimpleContent(content, pi, offset);
return makeTracer(offset, pi, StandardNames.XSL_PROCESSING_INSTRUCTION, null);
}
/**
* Parse a processing instruction constructor of the form
* processing-instruction name { expr }
*
* @param offset position of the expression in the source
* @return the parsed expression
* @throws XPathException if a static error is found
*/
private Expression parseNamedProcessingInstructionConstructor(int offset) throws XPathException {
String target = t.currentTokenValue;
String warning = null;
if (target.equalsIgnoreCase("xml")) {
warning = "A processing instruction must not be named 'xml' in any combination of upper and lower case";
}
if (!NameChecker.isValidNCName(target)) {
grumble("Invalid processing instruction name " + Err.wrap(target));
}
Expression piName = new StringLiteral(target);
Expression piContent = null;
nextToken();
if (t.currentToken != Token.RCURLY) {
piContent = parseExpression();
expect(Token.RCURLY);
}
lookAhead(); // after an RCURLY
nextToken();
if (warning == null) {
ProcessingInstruction pi2 = new ProcessingInstruction(piName);
makeSimpleContent(piContent, pi2, offset);
return makeTracer(offset, pi2, StandardNames.XSL_PROCESSING_INSTRUCTION, null);
} else {
warning(warning);
return new ErrorExpression(warning, "XQDY0064", false);
}
}
/**
* Parse a Try/Catch Expression.
* This construct is XQuery-3.0 only. The syntax allows:
* try { Expr } catch NameTest ('|' NameTest)* { Expr }
* We don't currently implement the CatchVars
*/
protected Expression parseTryCatchExpression() throws XPathException {
if (!allowXPath30Syntax) {
grumble("try/catch requires XQuery 3.0");
}
int offset = t.currentTokenStartOffset;
nextToken();
Expression tryExpr = parseExpression();
TryCatch tryCatch = new TryCatch(tryExpr);
setLocation(tryCatch, offset);
expect(Token.RCURLY);
lookAhead(); // must be done manually after an RCURLY
nextToken();
boolean foundOneCatch = false;
List tests = new ArrayList();
while (isKeyword("catch")) {
tests.clear();
foundOneCatch = true;
boolean seenCurly = false;
do {
nextToken();
String tokv = t.currentTokenValue;
switch (t.currentToken) {
case Token.NAME:
nextToken();
tests.add(makeNameTest(Type.ELEMENT, tokv, false));
break;
case Token.KEYWORD_CURLY:
nextToken();
tests.add(makeNameTest(Type.ELEMENT, tokv, false));
seenCurly = true;
break;
case Token.PREFIX:
nextToken();
tests.add(makeNamespaceTest(Type.ELEMENT, tokv));
break;
case Token.SUFFIX:
nextToken();
tokv = t.currentTokenValue;
if (t.currentToken == Token.NAME) {
// OK
} else if (t.currentToken == Token.KEYWORD_CURLY) {
// OK
seenCurly = true;
} else {
grumble("Expected name after '*:'");
}
nextToken();
tests.add(makeLocalNameTest(Type.ELEMENT, tokv));
break;
case Token.STAR:
case Token.MULT:
nextToken();
tests.add(AnyNodeTest.getInstance());
break;
default:
grumble("Unrecognized name test");
return null;
}
}
while (t.currentToken == Token.UNION && !t.currentTokenValue.equals("union")); // must be "|" not "union"!
if (!seenCurly) {
expect(Token.LCURLY);
nextToken();
}
QNameTest test;
if (tests.size() == 1) {
test = tests.get(0);
} else {
test = new UnionQNameTest(tests);
}
catchDepth++;
Expression catchExpr = parseExpression();
tryCatch.addCatchExpression(test, catchExpr);
expect(Token.RCURLY);
lookAhead(); // must be done manually after an RCURLY
nextToken();
catchDepth--;
}
if (!foundOneCatch) {
grumble("After try{}, expected 'catch'");
}
return tryCatch;
}
/**
* Parse a computed namespace constructor of the form
* namespace {expr}{expr}
*
* @param offset the location of the expression in the query source
* @return the compiled Namespace instruction
* @throws XPathException in the event of a syntax error
*/
/*@NotNull*/
private Expression parseNamespaceConstructor(int offset) throws XPathException {
if (!allowXPath30Syntax) {
grumble("Namespace node constructors require XQuery 3.0");
}
nextToken();
Expression nameExpr = parseExpression();
expect(Token.RCURLY);
lookAhead(); // must be done manually after an RCURLY
nextToken();
expect(Token.LCURLY);
t.setState(Tokenizer.DEFAULT_STATE);
nextToken();
Expression content = null;
if (t.currentToken != Token.RCURLY) {
content = parseExpression();
expect(Token.RCURLY);
}
lookAhead(); // after an RCURLY
nextToken();
NamespaceConstructor instr = new NamespaceConstructor(nameExpr);
setLocation(instr);
makeSimpleContent(content, instr, offset);
return makeTracer(offset, instr, StandardNames.XSL_NAMESPACE, null);
}
/**
* Parse a namespace node constructor of the form
* namespace name { expr }
*
* @param offset the location of the expression in the query source
* @return the compiled instruction
* @throws XPathException in the event of a syntax error
*/
/*@NotNull*/
private Expression parseNamedNamespaceConstructor(int offset) throws XPathException {
if (!allowXPath30Syntax) {
grumble("Namespace node constructors require XQuery 3.0");
}
String target = t.currentTokenValue;
if (!NameChecker.isValidNCName(target)) {
grumble("Invalid namespace prefix " + Err.wrap(target));
}
Expression nsName = new StringLiteral(target);
Expression nsContent = null;
nextToken();
if (t.currentToken != Token.RCURLY) {
nsContent = parseExpression();
expect(Token.RCURLY);
}
lookAhead(); // after an RCURLY
nextToken();
NamespaceConstructor instr = new NamespaceConstructor(nsName);
makeSimpleContent(nsContent, instr, offset);
return makeTracer(offset, instr, StandardNames.XSL_NAMESPACE, null);
}
/**
* Make the instructions for the children of a node with simple content (attribute, text, PI, etc)
*
* @param content the expression making up the simple content
* @param inst the skeletal instruction for creating the node
* @param offset the character position of this construct within the source query
* @throws net.sf.saxon.trans.XPathException
* if a static error is encountered
*/
protected void makeSimpleContent(/*@Nullable*/ Expression content, /*@NotNull*/ SimpleNodeConstructor inst, int offset) throws XPathException {
try {
if (content == null) {
inst.setSelect(new StringLiteral(StringValue.EMPTY_STRING));
} else {
inst.setSelect(stringify(content, false, env.getConfiguration()));
}
setLocation(inst, offset);
} catch (XPathException e) {
grumble(e.getMessage());
}
}
/**
* Make a sequence of instructions as the children of an element-construction instruction.
* The idea here is to convert an XPath expression that "pulls" the content into a sequence
* of XSLT-style instructions that push nodes directly onto the subtree, thus reducing the
* need for copying of intermediate nodes.
* @param content The content of the element as an expression
* @param inst The element-construction instruction (Element or FixedElement)
* @param offset the character position in the query
*/
// private void makeContentConstructor(Expression content, InstructionWithChildren inst, int offset) {
// if (content == null) {
// inst.setChildren(null);
// } else if (content instanceof AppendExpression) {
// List instructions = new ArrayList(10);
// convertAppendExpression((AppendExpression) content, instructions);
// inst.setChildren((Expression[]) instructions.toArray(new Expression[instructions.size()]));
// } else {
// Expression children[] = {content};
// inst.setChildren(children);
// }
// setLocation(inst, offset);
// }
/**
* Parse pseudo-XML syntax in direct element constructors, comments, CDATA, etc.
* This is handled by reading single characters from the Tokenizer until the
* end of the tag (or an enclosed expression) is enountered.
* This method is also used to read an end tag. Because an end tag is not an
* expression, the method in this case returns a StringValue containing the
* contents of the end tag.
*
* @param allowEndTag true if the context allows an End Tag to appear here
* @return an Expression representing the result of parsing the constructor.
* If an end tag was read, its contents will be returned as a StringValue.
* @throws XPathException if parsing fails
*/
/*@NotNull*/
private Expression parsePseudoXML(boolean allowEndTag) throws XPathException {
try {
Expression exp;
int offset = t.inputOffset;
// we're reading raw characters, so we don't want the currentTokenStartOffset
char c = t.nextChar();
switch (c) {
case '!':
c = t.nextChar();
if (c == '-') {
exp = parseCommentConstructor();
} else if (c == '[') {
grumble("A CDATA section is allowed only in element content");
return null;
// if CDATA were allowed here, we would have already read it
} else {
grumble("Expected '--' or '[CDATA[' after '') {
break;
}
sb.append(c);
}
return new StringLiteral(sb.toString());
}
grumble("Unmatched XML end tag");
return new ErrorExpression();
default:
t.unreadChar();
exp = parseDirectElementConstructor(allowEndTag);
}
setLocation(exp, offset);
return exp;
} catch (StringIndexOutOfBoundsException e) {
grumble("End of input encountered while parsing direct constructor");
return new ErrorExpression();
}
}
/**
* Parse a direct element constructor
*
* @param isNested true if this constructor is nested directly as part of the content of another
* element constructor. This has the effect that the child element is not copied, which means
* that namespace inheritance (which only happens during copying) has no effect
* @return the expression representing the constructor
* @throws XPathException if a syntax error is found
* @throws StringIndexOutOfBoundsException
* if the end of input is encountered prematurely
*/
private Expression parseDirectElementConstructor(boolean isNested) throws XPathException, StringIndexOutOfBoundsException {
NamePool pool = env.getConfiguration().getNamePool();
boolean changesContext = false;
int offset = t.inputOffset - 1;
// we're reading raw characters, so we don't want the currentTokenStartOffset
char c;
FastStringBuffer buff = new FastStringBuffer(FastStringBuffer.C64);
int namespaceCount = 0;
while (true) {
c = t.nextChar();
if (c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == '/' || c == '>') {
break;
}
buff.append(c);
}
String elname = buff.toString();
if (elname.isEmpty()) {
grumble("Expected element name after '<'");
}
//Used LinkedHashMap because it is friendly to retain the order of attributes.
LinkedHashMap attributes = new LinkedHashMap(10);
while (true) {
// loop through the attributes
// We must process namespace declaration attributes first;
// their scope applies to all preceding attribute names and values.
// But finding the delimiting quote of an attribute value requires the
// XPath expressions to be parsed, because they may contain nested quotes.
// So we parse in "scanOnly" mode, which ignores any undeclared namespace
// prefixes, use the result of this parse to determine the length of the
// attribute value, save the value, and reparse it when all the namespace
// declarations have been dealt with.
c = skipSpaces(c);
if (c == '/' || c == '>') {
break;
}
int attOffset = t.inputOffset - 1;
buff.setLength(0);
// read the attribute name
while (true) {
buff.append(c);
c = t.nextChar();
if (c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == '=') {
break;
}
}
String attName = buff.toString();
if (!NameChecker.isQName(attName)) {
grumble("Invalid attribute name " + Err.wrap(attName, Err.ATTRIBUTE));
}
c = skipSpaces(c);
expectChar(c, '=');
c = t.nextChar();
c = skipSpaces(c);
char delim = c;
boolean isNamespace = "xmlns".equals(attName) || attName.startsWith("xmlns:");
int end;
if (isNamespace) {
end = makeNamespaceContent(t.input, t.inputOffset, delim);
changesContext = true;
} else {
Expression avt;
try {
avt = makeAttributeContent(t.input, t.inputOffset, delim, true);
} catch (XPathException err) {
if (!err.hasBeenReported()) {
grumble(err.getMessage());
}
throw err;
}
// by convention, this returns the end position when called with scanOnly set
end = (int) ((Int64Value) ((Literal) avt).getValue()).longValue();
}
// save the value with its surrounding quotes
String val = t.input.substring(t.inputOffset - 1, end + 1);
// and without
String rval = t.input.substring(t.inputOffset, end);
// account for any newlines found in the value
// (note, subexpressions between curlies will have been parsed using a different tokenizer)
String tail = val;
int pos;
while ((pos = tail.indexOf('\n')) >= 0) {
t.incrementLineNumber(t.inputOffset - 1 + pos);
tail = tail.substring(pos + 1);
}
t.inputOffset = end + 1;
if (isNamespace) {
// Processing follows the resolution of bug 5083: doubled curly braces represent single
// curly braces, single curly braces are not allowed.
FastStringBuffer sb = new FastStringBuffer(rval.length());
boolean prevDelim = false;
boolean prevOpenCurly = false;
boolean prevCloseCurly = false;
for (int i = 0; i < rval.length(); i++) {
char n = rval.charAt(i);
if (n == delim) {
prevDelim = !prevDelim;
if (prevDelim) {
continue;
}
}
if (n == '{') {
prevOpenCurly = !prevOpenCurly;
if (prevOpenCurly) {
continue;
}
} else if (prevOpenCurly) {
grumble("Namespace must not contain an unescaped opening brace", "XQST0022");
}
if (n == '}') {
prevCloseCurly = !prevCloseCurly;
if (prevCloseCurly) {
continue;
}
} else if (prevCloseCurly) {
grumble("Namespace must not contain an unescaped closing brace", "XPST0003");
}
sb.append(n);
}
if (prevOpenCurly) {
grumble("Namespace must not contain an unescaped opening brace", "XQST0022");
}
if (prevCloseCurly) {
grumble("Namespace must not contain an unescaped closing brace", "XPST0003");
}
rval = sb.toString();
String uri = URILiteral(rval);
if (!StandardURIChecker.getInstance().isValidURI(uri)) {
grumble("Namespace must be a valid URI value", "XQST0046");
}
String prefix;
if ("xmlns".equals(attName)) {
prefix = "";
if (uri.equals(NamespaceConstant.XML)) {
grumble("Cannot have the XML namespace as the default namespace", "XQST0070");
}
} else {
prefix = attName.substring(6);
if (prefix.equals("xml") && !uri.equals(NamespaceConstant.XML)) {
grumble("Cannot bind the prefix 'xml' to a namespace other than the XML namespace", "XQST0070");
} else if (uri.equals(NamespaceConstant.XML) && !prefix.equals("xml")) {
grumble("Cannot bind a prefix other than 'xml' to the XML namespace", "XQST0070");
} else if (prefix.equals("xmlns")) {
grumble("Cannot use xmlns as a namespace prefix", "XQST0070");
}
if (uri.isEmpty()) {
if (env.getConfiguration().getXMLVersion() == Configuration.XML10) {
grumble("Namespace URI must not be empty", "XQST0085");
}
}
}
namespaceCount++;
((QueryModule) env).declareActiveNamespace(prefix, uri);
}
if (attributes.get(attName) != null) {
if (isNamespace) {
grumble("Duplicate namespace declaration " + attName, "XQST0071", attOffset);
} else {
grumble("Duplicate attribute name " + attName, "XQST0040", attOffset);
}
}
// if (attName.equals("xml:id") && !NameChecker.isValidNCName(rval)) {
// grumble("Value of xml:id must be a valid NCName", "XQST0082");
// }
AttributeDetails a = new AttributeDetails();
a.value = val;
a.startOffset = attOffset;
attributes.put(attName, a);
// on return, the current character is the closing quote
c = t.nextChar();
if (!(c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == '/' || c == '>')) {
grumble("There must be whitespace after every attribute except the last");
}
}
StructuredQName qName = null;
int elNameCode = 0;
if (scanOnly) {
elNameCode = StandardNames.XSL_ELEMENT; // any name will do
qName = StandardNames.getStructuredQName(StandardNames.XSL_ELEMENT);
} else {
try {
String[] parts = NameChecker.getQNameParts(elname);
String namespace = ((QueryModule) env).checkURIForPrefix(parts[0]);
if (namespace == null) {
grumble("Undeclared prefix in element name " + Err.wrap(elname, Err.ELEMENT), "XPST0081", offset);
}
qName = new StructuredQName(parts[0], namespace, parts[1]);
elNameCode = pool.allocate(parts[0], namespace, parts[1]);
} catch (QNameException e) {
grumble("Invalid element name " + Err.wrap(elname, Err.ELEMENT), "XPST0003", offset);
}
}
int validationMode = ((QueryModule) env).getConstructionMode();
FingerprintedQName fqn = new FingerprintedQName(
qName.getPrefix(), qName.getURI(), qName.getLocalPart(), elNameCode);
FixedElement elInst = new FixedElement(fqn,
((QueryModule) env).getActiveNamespaceCodes(),
((QueryModule) env).isInheritNamespaces(),
!isNested,
null,
validationMode);
setLocation(elInst, offset);
List contents = new ArrayList(10);
IntHashSet attFingerprints = new IntHashSet(attributes.size());
// we've checked for duplicate lexical QNames, but not for duplicate expanded-QNames
for (Map.Entry entry : attributes.entrySet()) {
String attName = entry.getKey();
AttributeDetails a = entry.getValue();
String attValue = a.value;
int attOffset = a.startOffset;
if ("xmlns".equals(attName) || attName.startsWith("xmlns:")) {
// do nothing
} else if (scanOnly) {
// This means we are prescanning an attribute constructor, and we found a nested attribute
// constructor, which we have prescanned; we now don't want to re-process the nested attribute
// constructor because it might depend on things like variables declared in the containing
// attribute constructor, and in any case we're going to come back to it again later.
// See test qxmp180
} else {
NodeName attributeName = null;
String attNamespace;
try {
String[] parts = NameChecker.getQNameParts(attName);
if (parts[0].isEmpty()) {
// attributes don't use the default namespace
attNamespace = "";
} else {
attNamespace = ((QueryModule) env).checkURIForPrefix(parts[0]);
}
if (attNamespace == null) {
grumble("Undeclared prefix in attribute name " +
Err.wrap(attName, Err.ATTRIBUTE), "XPST0081", attOffset);
}
attributeName = new FingerprintedQName(parts[0], attNamespace, parts[1]);
attributeName.allocateNameCode(pool);
int key = attributeName.getFingerprint();
if (attFingerprints.contains(key)) {
grumble("Duplicate expanded attribute name " + attName, "XQST0040", attOffset);
}
attFingerprints.add(key);
} catch (QNameException e) {
grumble("Invalid attribute name " + Err.wrap(attName, Err.ATTRIBUTE), "XPST0003", attOffset);
}
FixedAttribute attInst =
new FixedAttribute(attributeName, Validation.STRIP, null);
setLocation(attInst);
Expression select;
try {
select = makeAttributeContent(attValue, 1, attValue.charAt(0), false);
} catch (XPathException err) {
err.setIsStaticError(true);
throw err;
}
attInst.setSelect(select);
attInst.setRejectDuplicates();
setLocation(attInst);
contents.add(makeTracer(attOffset, attInst, LocationKind.LITERAL_RESULT_ATTRIBUTE,
attributeName.getStructuredQName()));
}
}
if (c == '/') {
// empty element tag
expectChar(t.nextChar(), '>');
} else {
readElementContent(elname, contents);
}
Expression[] elk = new Expression[contents.size()];
for (int i = 0; i < contents.size(); i++) {
// if the child expression creates another element,
// suppress validation, as the parent already takes care of it
if (validationMode != Validation.STRIP) {
contents.get(i).suppressValidation(validationMode);
}
elk[i] = contents.get(i);
}
Block block = new Block(elk);
if (changesContext) {
block.setRetainedStaticContext(env.makeRetainedStaticContext());
}
elInst.setContentExpression(block);
// reset the in-scope namespaces to what they were before
for (int n = 0; n < namespaceCount; n++) {
((QueryModule) env).undeclareNamespace();
}
return makeTracer(offset, elInst, LocationKind.LITERAL_RESULT_ELEMENT,
pool.getStructuredQName(elNameCode));
}
/**
* Parse the content of an attribute in a direct element constructor. This may contain nested expressions
* within curly braces. A particular problem is that the namespaces used in the expression may not yet be
* known. This means we need the ability to parse in "scanOnly" mode, where undeclared namespace prefixes
* are ignored.
*
* The code is based on the XSLT code in {@link AttributeValueTemplate#make}: the main difference is that
* character entities and built-in entity references need to be recognized and expanded. Also, whitespace
* needs to be normalized, mimicking the action of an XML parser
*
* @param avt the content of the attribute as written, including variable portions enclosed in curly braces
* @param start the character position in the attribute value where parsing should start
* @param terminator a character that is to be taken as marking the end of the expression
* @param scanOnly if the purpose of this parse is simply to locate the end of the attribute value, and not
* to report any semantic errors.
* @return the expression that will evaluate the content of the attribute
* @throws XPathException if parsing fails
*/
/*@Nullable*/
private Expression makeAttributeContent(/*@NotNull*/ String avt,
int start,
char terminator,
boolean scanOnly) throws XPathException {
Location loc = makeLocation();
List components = new ArrayList(10);
int i0, i1, i2, i8, i9, len, last;
last = start;
len = avt.length();
while (last < len) {
i2 = avt.indexOf(terminator, last);
if (i2 < 0) {
XPathException e = new XPathException("Attribute constructor is not properly terminated");
e.setIsStaticError(true);
throw e;
}
i0 = avt.indexOf("{", last);
i1 = avt.indexOf("{{", last);
i8 = avt.indexOf("}", last);
i9 = avt.indexOf("}}", last);
if ((i0 < 0 || i2 < i0) && (i8 < 0 || i2 < i8)) { // found end of string
addStringComponent(components, avt, last, i2);
// look for doubled quotes, and skip them (for now)
if (i2 + 1 < avt.length() && avt.charAt(i2 + 1) == terminator) {
components.add(new StringLiteral(terminator + ""));
last = i2 + 2;
//continue;
} else {
last = i2;
break;
}
} else if (i8 >= 0 && (i0 < 0 || i8 < i0)) { // found a "}"
if (i8 != i9) { // a "}" that isn't a "}}"
XPathException e = new XPathException(
"Closing curly brace in attribute value template \"" + avt + "\" must be doubled");
e.setIsStaticError(true);
throw e;
}
addStringComponent(components, avt, last, i8 + 1);
last = i8 + 2;
} else if (i1 >= 0 && i1 == i0) { // found a doubled "{{"
addStringComponent(components, avt, last, i1 + 1);
last = i1 + 2;
} else if (i0 >= 0) { // found a single "{"
if (i0 > last) {
addStringComponent(components, avt, last, i0);
}
Expression exp;
XPathParser parser = newParser();
((XQueryParser) parser).executable = executable;
parser.setAllowAbsentExpression(allowXPath31Syntax);
parser.setScanOnly(scanOnly);
parser.setRangeVariableStack(rangeVariables);
parser.setCatchDepth(catchDepth);
exp = parser.parse(avt, i0 + 1, Token.RCURLY, env);
if (!scanOnly) {
exp = exp.simplify();
}
last = parser.getTokenizer().currentTokenStartOffset + 1;
components.add(makeStringJoin(exp, env));
} else {
throw new IllegalStateException("Internal error parsing direct attribute constructor");
}
}
// if this is simply a prescan, return the position of the end of the
// AVT, so we can parse it properly later
if (scanOnly) {
return Literal.makeLiteral(Int64Value.makeIntegerValue(last));
}
// is it empty?
if (components.isEmpty()) {
return new StringLiteral(StringValue.EMPTY_STRING);
}
// is it a single component?
if (components.size() == 1) {
return components.get(0);
}
// otherwise, return an expression that concatenates the components
Expression[] args = new Expression[components.size()];
components.toArray(args);
RetainedStaticContext rsc = new RetainedStaticContext(env);
Expression fn = SystemFunction.makeCall("concat", rsc, args);
fn.setLocation(loc);
return fn;
//return visitor.simplify(fn);
}
private void addStringComponent(/*@NotNull*/ List components, /*@NotNull*/ String avt, int start, int end)
throws XPathException {
// analyze fixed text within the value of a direct attribute constructor.
if (start < end) {
FastStringBuffer sb = new FastStringBuffer(end - start);
for (int i = start; i < end; i++) {
char c = avt.charAt(i);
switch (c) {
case '&': {
int semic = avt.indexOf(';', i);
if (semic < 0) {
grumble("No closing ';' found for entity or character reference");
} else {
String entity = avt.substring(i + 1, semic);
sb.append(analyzeEntityReference(entity));
i = semic;
}
break;
}
case '<':
grumble("The < character must not appear in attribute content");
break;
case '\n':
case '\t':
sb.append(' ');
break;
case '\r':
sb.append(' ');
if (i + 1 < end && avt.charAt(i + 1) == '\n') {
i++;
}
break;
default:
sb.append(c);
}
}
components.add(new StringLiteral(sb.toString()));
}
}
/**
* Parse the content of an namespace declaration attribute in a direct element constructor. This is simpler
* than an ordinary attribute because it does not contain nested expressions in curly braces. (But see bug 5083).
*
* @param avt the content of the attribute as written, including variable portions enclosed in curly braces
* @param start the character position in the attribute value where parsing should start
* @param terminator a character that is to be taken as marking the end of the expression
* @return the position of the end of the URI value
* @throws XPathException if parsing fails
*/
private int makeNamespaceContent(/*@NotNull*/ String avt, int start, char terminator) throws XPathException {
int i2, len, last;
last = start;
len = avt.length();
while (last < len) {
i2 = avt.indexOf(terminator, last);
if (i2 < 0) {
XPathException e = new XPathException("Namespace declaration is not properly terminated");
e.setIsStaticError(true);
throw e;
}
// look for doubled quotes, and skip them (for now)
if (i2 + 1 < avt.length() && avt.charAt(i2 + 1) == terminator) {
last = i2 + 2;
//continue;
} else {
last = i2;
break;
}
}
// return the position of the end of the literal
return last;
}
/**
* Read the content of a direct element constructor
*
* @param startTag the element start tag
* @param components an empty list, to which the expressions comprising the element contents are added
* @throws XPathException if any static errors are detected
*/
private void readElementContent(String startTag, /*@NotNull*/ List components) throws XPathException {
try {
boolean afterEnclosedExpr = false;
while (true) {
// read all the components of the element value
FastStringBuffer text = new FastStringBuffer(FastStringBuffer.C64);
char c;
boolean containsEntities = false;
while (true) {
c = t.nextChar();
if (c == '<') {
// See if we've got a CDATA section
if (t.nextChar() == '!') {
if (t.nextChar() == '[') {
readCDATASection(text);
containsEntities = true;
continue;
} else {
t.unreadChar();
t.unreadChar();
}
} else {
t.unreadChar();
}
break;
} else if (c == '&') {
text.append(readEntityReference());
containsEntities = true;
} else if (c == '}') {
c = t.nextChar();
if (c != '}') {
grumble("'}' must be written as '}}' within element content");
}
text.append(c);
} else if (c == '{') {
c = t.nextChar();
if (c != '{') {
c = '{';
break;
}
text.append(c);
} else {
if (!charChecker.matches(c) && !UTF16CharacterSet.isSurrogate(c)) {
grumble("Character code " + c + " is not a valid XML character");
}
text.append(c);
}
}
if (text.length() > 0 &&
(containsEntities |
((QueryModule) env).isPreserveBoundarySpace() ||
!Whitespace.isWhite(text))) {
ValueOf inst = new ValueOf(new StringLiteral(new StringValue(text.condense())), false, false);
setLocation(inst);
components.add(inst);
afterEnclosedExpr = false;
}
if (c == '<') {
Expression exp = parsePseudoXML(true);
// An end tag can appear here, and is returned as a string value
if (exp instanceof StringLiteral) {
String endTag = ((StringLiteral) exp).getStringValue();
if (Whitespace.isWhitespace(endTag.charAt(0))) {
grumble("End tag contains whitespace before the name");
}
endTag = Whitespace.trim(endTag);
if (endTag.equals(startTag)) {
return;
} else {
grumble("End tag " + endTag +
"> does not match start tag <" + startTag + '>', "XQST0118");
// error code allocated by spec bug 11609
}
} else {
components.add(exp);
}
} else {
// we read an '{' indicating an enclosed expression
if (afterEnclosedExpr) {
Expression previousComponent = components.get(components.size() - 1);
boolean previousComponentIsNodeTest = true;
try {
ItemType previousItemType = previousComponent.getItemType();
previousComponentIsNodeTest = previousItemType instanceof NodeTest;
} catch (Exception err) {
// no action
// TODO: this is bad form. We shouldn't call getItemType() during parsing; it can
// fail because there's not yet enough information on the expression tree, e.g.
// parent pointers may be missing. See test K2-DirectConElemWhitespace-15
}
if (!previousComponentIsNodeTest) {
// Add a zero-length text node, to prevent {"a"}{"b"} generating an intervening space
// See tests (qxmp132, qxmp261)
ValueOf inst = new ValueOf(new StringLiteral(StringValue.EMPTY_STRING), false, false);
setLocation(inst);
components.add(inst);
}
}
t.unreadChar();
t.setState(Tokenizer.DEFAULT_STATE);
lookAhead();
nextToken();
if (t.currentToken == Token.RCURLY && allowXPath31Syntax) {
components.add(Literal.makeEmptySequence());
} else {
Expression exp = parseExpression();
if (!((QueryModule) env).isPreserveNamespaces()) {
exp = new CopyOf(exp, false, Validation.PRESERVE, null, true);
}
components.add(exp);
expect(Token.RCURLY);
}
afterEnclosedExpr = true;
}
}
} catch (StringIndexOutOfBoundsException err) {
grumble("No closing end tag found for direct element constructor");
}
}
/*@Nullable*/
private Expression parsePIConstructor() throws XPathException {
try {
FastStringBuffer pi = new FastStringBuffer(FastStringBuffer.C64);
int firstSpace = -1;
while (!pi.toString().endsWith("?>")) {
char c = t.nextChar();
if (firstSpace < 0 && " \t\r\n".indexOf(c) >= 0) {
firstSpace = pi.length();
}
pi.append(c);
}
pi.setLength(pi.length() - 2);
String target;
String data = "";
if (firstSpace < 0) {
// there is no data part
target = pi.toString();
} else {
// trim leading space from the data part, but not trailing space
target = pi.toString().substring(0, firstSpace);
firstSpace++;
while (firstSpace < pi.length() && " \t\r\n".indexOf(pi.charAt(firstSpace)) >= 0) {
firstSpace++;
}
data = pi.toString().substring(firstSpace);
}
if (!NameChecker.isValidNCName(target)) {
grumble("Invalid processing instruction name " + Err.wrap(target));
}
if (target.equalsIgnoreCase("xml")) {
grumble("A processing instruction must not be named 'xml' in any combination of upper and lower case");
}
ProcessingInstruction instruction =
new ProcessingInstruction(new StringLiteral(target));
instruction.setSelect(new StringLiteral(data));
setLocation(instruction);
return instruction;
} catch (StringIndexOutOfBoundsException err) {
grumble("No closing '?>' found for processing instruction");
return null;
}
}
private void readCDATASection(/*@NotNull*/ FastStringBuffer cdata) throws XPathException {
try {
char c;
// CDATA section
c = t.nextChar();
expectChar(c, 'C');
c = t.nextChar();
expectChar(c, 'D');
c = t.nextChar();
expectChar(c, 'A');
c = t.nextChar();
expectChar(c, 'T');
c = t.nextChar();
expectChar(c, 'A');
c = t.nextChar();
expectChar(c, '[');
while (!cdata.toString().endsWith("]]>")) {
cdata.append(t.nextChar());
}
cdata.setLength(cdata.length() - 3);
} catch (StringIndexOutOfBoundsException err) {
grumble("No closing ']]>' found for CDATA section");
}
}
/*@Nullable*/
private Expression parseCommentConstructor() throws XPathException {
try {
char c = t.nextChar();
// XML-like comment
expectChar(c, '-');
FastStringBuffer comment = new FastStringBuffer(FastStringBuffer.C256);
while (!comment.toString().endsWith("--")) {
comment.append(t.nextChar());
}
if (t.nextChar() != '>') {
grumble("'--' is not permitted in an XML comment");
}
CharSequence commentText = comment.subSequence(0, comment.length() - 2);
Comment instruction = new Comment();
instruction.setSelect(new StringLiteral(new StringValue(commentText)));
setLocation(instruction);
return instruction;
} catch (StringIndexOutOfBoundsException err) {
grumble("No closing '-->' found for comment constructor");
return null;
}
}
/**
* Convert an expression so it generates a space-separated sequence of strings
*
* @param exp the expression that calculates the content
* @param noNodeIfEmpty if true, no node is produced when the value of the content
* expression is an empty sequence. If false, the effect of supplying an empty sequence
* is that a node is created whose string-value is a zero-length string. Set to true for
* text node constructors, false for other kinds of node.
* @param config the Saxon configuration
* @return an expression that computes the content and converts the result to a character string
* @throws XPathException if parsing fails
*/
public static Expression stringify(Expression exp, boolean noNodeIfEmpty, Configuration config) throws XPathException {
// Compare with XSLLeafNodeConstructor.makeSimpleContentConstructor
// Atomize the result
exp = Atomizer.makeAtomizer(exp);
// Convert each atomic value to a string
exp = new AtomicSequenceConverter(exp, BuiltInAtomicType.STRING);
//((AtomicSequenceConverter) exp).allocateConverter(config, false);
// Join the resulting strings with a separator
exp = SystemFunction.makeCall("string-join", exp.getRetainedStaticContext(), exp, new StringLiteral(StringValue.SINGLE_SPACE));
assert exp != null;
if (noNodeIfEmpty) {
((StringJoin) ((SystemFunctionCall) exp).getTargetFunction()).setReturnEmptyIfEmpty(true);
}
// All that's left for the instruction to do is to construct the right kind of node
return exp;
}
/**
* Method to make a string literal from a token identified as a string
* literal. This is trivial in XPath, but in XQuery the method is overridden
* to identify pseudo-XML character and entity references
*
* @param token the string as written (or as returned by the tokenizer)
* @return The string value of the string literal, after dereferencing entity and
* character references
* @throws XPathException if parsing fails
*/
@Override
/*@NotNull*/
protected Literal makeStringLiteral(/*@NotNull*/ String token) throws XPathException {
StringLiteral lit;
if (token.indexOf('&') == -1) {
lit = new StringLiteral(token);
} else {
CharSequence sb = unescape(token);
lit = new StringLiteral(StringValue.makeStringValue(sb));
}
setLocation(lit);
return lit;
}
/**
* Unescape character references and built-in entity references in a string
*
* @param token the input string, which may include XML-style character references or built-in
* entity references
* @return the string with character references and built-in entity references replaced by their expansion
* @throws XPathException if a malformed character or entity reference is found
*/
/*@NotNull*/
protected CharSequence unescape(/*@NotNull*/ String token) throws XPathException {
FastStringBuffer sb = new FastStringBuffer(token.length());
for (int i = 0; i < token.length(); i++) {
char c = token.charAt(i);
if (c == '&') {
int semic = token.indexOf(';', i);
if (semic < 0) {
grumble("No closing ';' found for entity or character reference");
} else {
String entity = token.substring(i + 1, semic);
sb.append(analyzeEntityReference(entity));
i = semic;
}
} else {
sb.append(c);
}
}
return sb;
}
/**
* Read a pseudo-XML character reference or entity reference.
*
* @return The character represented by the character or entity reference. Note
* that this is a string rather than a char because a char only accommodates characters
* up to 65535.
* @throws XPathException if the character or entity reference is not well-formed
*/
/*@Nullable*/
private String readEntityReference() throws XPathException {
try {
FastStringBuffer sb = new FastStringBuffer(FastStringBuffer.C64);
while (true) {
char c = t.nextChar();
if (c == ';') {
break;
}
sb.append(c);
}
String entity = sb.toString();
return analyzeEntityReference(entity);
} catch (StringIndexOutOfBoundsException err) {
grumble("No closing ';' found for entity or character reference");
}
return null; // to keep the Java compiler happy
}
/*@Nullable*/
private String analyzeEntityReference(/*@NotNull*/ String entity) throws XPathException {
if ("lt".equals(entity)) {
return "<";
} else if ("gt".equals(entity)) {
return ">";
} else if ("amp".equals(entity)) {
return "&";
} else if ("quot".equals(entity)) {
return "\"";
} else if ("apos".equals(entity)) {
return "'";
} else if (entity.length() < 2 || entity.charAt(0) != '#') {
grumble("invalid character reference &" + entity + ';');
return null;
} else {
//entity = entity.toLowerCase();
return parseCharacterReference(entity);
}
}
/*@Nullable*/
private String parseCharacterReference(/*@NotNull*/ String entity) throws XPathException {
int value = 0;
if (entity.charAt(1) == 'x') {
if (entity.length() < 3) {
grumble("No hex digits in hexadecimal character reference");
}
entity = entity.toLowerCase();
for (int i = 2; i < entity.length(); i++) {
int digit = "0123456789abcdef".indexOf(entity.charAt(i));
if (digit < 0) {
grumble("Invalid hex digit '" + entity.charAt(i) + "' in character reference");
}
value = (value * 16) + digit;
if (value > UTF16CharacterSet.NONBMP_MAX) {
grumble("Character reference exceeds Unicode codepoint limit", "XQST0090");
}
}
} else {
for (int i = 1; i < entity.length(); i++) {
int digit = "0123456789".indexOf(entity.charAt(i));
if (digit < 0) {
grumble("Invalid digit '" + entity.charAt(i) + "' in decimal character reference");
}
value = (value * 10) + digit;
if (value > UTF16CharacterSet.NONBMP_MAX) {
grumble("Character reference exceeds Unicode codepoint limit", "XQST0090");
}
}
}
net.sf.saxon.z.IntPredicate nc = env.getConfiguration().getValidCharacterChecker();
if (!nc.matches(value)) {
grumble("Invalid XML character reference x"
+ Integer.toHexString(value), "XQST0090");
}
// following code borrowed from AElfred
// Check for surrogates: 00000000 0000xxxx yyyyyyyy zzzzzzzz
// (1101|10xx|xxyy|yyyy + 1101|11yy|zzzz|zzzz:
if (value <= 0x0000ffff) {
// no surrogates needed
return "" + (char) value;
} else if (value <= 0x0010ffff) {
value -= 0x10000;
// > 16 bits, surrogate needed
return "" + ((char) (0xd800 | (value >> 10)))
+ ((char) (0xdc00 | (value & 0x0003ff)));
} else {
// too big for surrogate
grumble("Character reference x" + Integer.toHexString(value) + " is too large", "XQST0090");
}
return null;
}
/**
* Parse a string template: introduced in XQuery 3.1
*/
@Override
public Expression parseStringTemplate(boolean complete) throws XPathException {
int offset = t.currentTokenStartOffset;
if (!allowXPath31Syntax) {
throw new XPathException("String constructor expressions require XQuery 3.1");
}
if (complete) {
Expression result = new StringLiteral(t.currentTokenValue);
setLocation(result, offset);
t.next();
return result;
} else {
List components = new ArrayList();
components.add(new StringLiteral(t.currentTokenValue));
t.next();
while (true) {
if (t.currentToken != Token.STRING_TEMPLATE_FINAL && t.currentToken != Token.STRING_TEMPLATE_INTERMEDIATE) {
Expression enclosed = parseExpression();
Expression stringJoin = SystemFunction.makeCall(
"string-join", env.makeRetainedStaticContext(), enclosed, new StringLiteral(" "));
components.add(stringJoin);
}
if (t.currentToken == Token.STRING_TEMPLATE_FINAL) {
components.add(new StringLiteral(t.currentTokenValue));
t.next();
Expression[] args = components.toArray(new Expression[components.size()]);
Expression result = SystemFunction.makeCall("concat", env.makeRetainedStaticContext(), args);
setLocation(result, offset);
return result;
} else if (t.currentToken == Token.STRING_TEMPLATE_INTERMEDIATE) {
components.add(new StringLiteral(t.currentTokenValue));
t.next();
} else {
grumble("Expected '}`' after enclosed expression in string template");
}
}
}
}
/**
* Handle a URI literal. This is whitespace-normalized as well as being unescaped
*
* @param in the string as written
* @return the URI after unescaping of entity and character references
* followed by whitespace normalization
* @throws net.sf.saxon.trans.XPathException
* if an error is found while unescaping the URI
*/
public String URILiteral(/*@NotNull*/ String in) throws XPathException {
return Whitespace.applyWhitespaceNormalization(Whitespace.COLLAPSE, unescape(in)).toString();
}
/**
* Convert a QName in expanded-name format Q{uri}local into Clark format
*
* @param s the QName in expanded-name format
* @return the corresponding expanded name in Clark format
* @throws XPathException
*/
/*@NotNull*/
protected String normalizeEQName(/*@NotNull*/ String s) throws XPathException {
// overridden for XQuery
if (!Whitespace.containsWhitespace(s) && s.indexOf('&') < 0) {
return s;
}
StructuredQName sq;
try {
sq = StructuredQName.fromEQName(s);
} catch (IllegalArgumentException e) {
throw new XPathException(e);
}
final String in = sq.getURI();
final String uri = Whitespace.applyWhitespaceNormalization(Whitespace.COLLAPSE, unescape(in)).toString();
return "Q{" + uri + "}" + sq.getLocalPart();
}
/**
* Lookahead one token, catching any exception thrown by the tokenizer. This
* method is only called from the query parser when switching from character-at-a-time
* mode to tokenizing mode
*
* @throws XPathException if parsing fails
*/
protected void lookAhead() throws XPathException {
try {
t.lookAhead();
} catch (XPathException err) {
grumble(err.getMessage());
}
}
@Override
protected boolean atStartOfRelativePath() {
return t.currentToken == Token.TAG || super.atStartOfRelativePath();
// "<" after "/" is recognized in XQuery but not in XPath.
}
@Override
protected void testPermittedAxis(byte axis, String errorCode) throws XPathException {
if (axis == AxisInfo.NAMESPACE && language == XQUERY) {
grumble("The namespace axis is not available in XQuery", errorCode);
}
}
/**
* Skip whitespace.
*
* @param c the current character
* @return the first character after any whitespace
* @throws StringIndexOutOfBoundsException
* if the end of input is encountered
*/
private char skipSpaces(char c) throws StringIndexOutOfBoundsException {
while (c == ' ' || c == '\n' || c == '\r' || c == '\t') {
c = t.nextChar();
}
return c;
}
/**
* Test whether the current character is the expected character.
*
* @param actual The character that was read
* @param expected The character that was expected
* @throws XPathException if they are different
*/
private void expectChar(char actual, char expected) throws XPathException {
if (actual != expected) {
grumble("Expected '" + expected + "', found '" + actual + '\'');
}
}
/**
* Get the current language (XPath or XQuery)
*/
/*@NotNull*/
protected String getLanguage() {
return "XQuery";
}
private static class AttributeDetails {
String value;
int startOffset;
}
private static class Import {
/*@Nullable*/ String namespaceURI;
List locationURIs;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy