/* * eXist Open Source Native XML Database * Copyright (C) 2001-2015 The eXist Project * http://exist-db.org * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * */ package org.exist.xquery; import java.io.IOException; import java.io.Reader; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.text.Collator; import java.util.ArrayList; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Properties; import java.util.SimpleTimeZone; import java.util.Stack; import java.util.TimeZone; import java.util.TreeMap; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import javax.xml.datatype.Duration; import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.stream.XMLStreamException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.Database; import org.exist.EXistException; import org.exist.Namespaces; import org.exist.collections.Collection; import org.exist.debuggee.Debuggee; import org.exist.debuggee.DebuggeeJoint; import org.exist.dom.persistent.BinaryDocument; import org.exist.dom.persistent.DefaultDocumentSet; import org.exist.dom.persistent.DocumentImpl; import org.exist.dom.persistent.DocumentSet; import org.exist.dom.persistent.MutableDocumentSet; import org.exist.dom.persistent.NodeHandle; import org.exist.dom.persistent.NodeProxy; import org.exist.dom.QName; import org.exist.http.servlets.RequestWrapper; import org.exist.interpreter.Context; import org.exist.dom.memtree.InMemoryXMLStreamReader; import org.exist.dom.memtree.MemTreeBuilder; import org.exist.dom.memtree.NodeImpl; import org.exist.numbering.NodeId; import org.exist.repo.ExistRepository; import org.exist.security.AuthenticationException; import org.exist.security.Permission; import org.exist.security.PermissionDeniedException; import org.exist.security.Subject; import org.exist.source.*; import org.exist.stax.ExtendedXMLStreamReader; import org.exist.storage.DBBroker; import org.exist.storage.UpdateListener; import org.exist.storage.lock.Lock; import org.exist.storage.lock.Lock.LockMode; import org.exist.storage.lock.LockedDocumentMap; import org.exist.util.Collations; import org.exist.util.Configuration; import org.exist.util.LockException; import org.exist.util.hashtable.NamePool; import org.exist.xmldb.XmldbURI; import org.exist.xquery.functions.request.RequestModule; import org.exist.xquery.parser.*; import org.exist.xquery.pragmas.*; import org.exist.xquery.update.Modification; import org.exist.xquery.value.*; import antlr.RecognitionException; import antlr.TokenStreamException; import antlr.collections.AST; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; /** * The current XQuery execution context. Contains the static as well as the dynamic * XQuery context components. * * @author Wolfgang Meier (wolfgang@exist-db.org) */ public class XQueryContext implements BinaryValueManager, Context { public static final String ENABLE_QUERY_REWRITING_ATTRIBUTE = "enable-query-rewriting"; public static final String XQUERY_BACKWARD_COMPATIBLE_ATTRIBUTE = "backwardCompatible"; public static final String XQUERY_RAISE_ERROR_ON_FAILED_RETRIEVAL_ATTRIBUTE = "raise-error-on-failed-retrieval"; public static final String ENFORCE_INDEX_USE_ATTRIBUTE = "enforce-index-use"; //TODO : move elsewhere ? public static final String BUILT_IN_MODULE_URI_ATTRIBUTE = "uri"; public static final String BUILT_IN_MODULE_CLASS_ATTRIBUTE = "class"; public static final String BUILT_IN_MODULE_SOURCE_ATTRIBUTE = "src"; public static final String PROPERTY_XQUERY_BACKWARD_COMPATIBLE = "xquery.backwardCompatible"; public static final String PROPERTY_ENABLE_QUERY_REWRITING = "xquery.enable-query-rewriting"; public static final String PROPERTY_XQUERY_RAISE_ERROR_ON_FAILED_RETRIEVAL = "xquery.raise-error-on-failed-retrieval"; public static final boolean XQUERY_RAISE_ERROR_ON_FAILED_RETRIEVAL_DEFAULT = false; public static final String PROPERTY_ENFORCE_INDEX_USE = "xquery.enforce-index-use"; //TODO : move elsewhere ? public static final String PROPERTY_BUILT_IN_MODULES = "xquery.modules"; public static final String PROPERTY_STATIC_MODULE_MAP = "xquery.modules.static"; public static final String PROPERTY_MODULE_PARAMETERS = "xquery.modules.parameters"; public static final String JAVA_URI_START = "java:"; //private static final String XMLDB_URI_START = "xmldb:exist://"; protected final static Logger LOG = LogManager.getLogger( XQueryContext.class ); private static final String TEMP_STORE_ERROR = "Error occurred while storing temporary data"; public static final String XQUERY_CONTEXTVAR_XQUERY_UPDATE_ERROR = "_eXist_xquery_update_error"; public static final String HTTP_SESSIONVAR_XMLDB_USER = "_eXist_xmldb_user"; public static final String HTTP_REQ_ATTR_USER = "xquery.user"; public static final String HTTP_REQ_ATTR_PASS = "xquery.password"; // Static namespace/prefix mappings protected HashMap<String, String> staticNamespaces = new HashMap<String, String>(); // Static prefix/namespace mappings protected HashMap<String, String> staticPrefixes = new HashMap<String, String>(); // Local in-scope namespace/prefix mappings in the current context protected HashMap<String, String> inScopeNamespaces = new HashMap<String, String>(); // Local prefix/namespace mappings in the current context protected HashMap<String, String> inScopePrefixes = new HashMap<String, String>(); // Inherited in-scope namespace/prefix mappings in the current context protected HashMap<String, String> inheritedInScopeNamespaces = new HashMap<String, String>(); // Inherited prefix/namespace mappings in the current context protected HashMap<String, String> inheritedInScopePrefixes = new HashMap<String, String>(); protected HashMap<String, XmldbURI> mappedModules = new HashMap<String, XmldbURI>(); private boolean preserveNamespaces = true; private boolean inheritNamespaces = true; // Local namespace stack protected Stack<HashMap<String, String>> namespaceStack = new Stack<HashMap<String, String>>(); // Known user defined functions in the local module protected TreeMap<FunctionId, UserDefinedFunction> declaredFunctions = new TreeMap<FunctionId, UserDefinedFunction>(); // Globally declared variables protected Map<QName, Variable> globalVariables = new TreeMap<QName, Variable>(); // The last element in the linked list of local in-scope variables protected LocalVariable lastVar = null; protected Stack<LocalVariable> contextStack = new Stack<LocalVariable>(); protected Stack<FunctionSignature> callStack = new Stack<FunctionSignature>(); // The current size of the variable stack protected int variableStackSize = 0; // Unresolved references to user defined functions protected Stack<FunctionCall> forwardReferences = new Stack<FunctionCall>(); // List of options declared for this query at compile time - i.e. declare option protected List<Option> staticOptions = null; // List of options declared for this query at run time - i.e. util:declare-option() protected List<Option> dynamicOptions = null; //The Calendar for this context : may be changed by some options XMLGregorianCalendar calendar = null; TimeZone implicitTimeZone = null; /** the watchdog object assigned to this query. */ protected XQueryWatchDog watchdog; /** Loaded modules. */ protected HashMap<String, Module> modules = new HashMap<String, Module>(); /** Loaded modules, including ones bubbled up from imported modules. */ protected HashMap<String, Module> allModules = new HashMap<String, Module>(); /** Used to save current state when modules are imported dynamically */ protected SavedState savedState = new SavedState(); /** * Whether some modules were rebound to new instances since the last time this context's query was analyzed. (This assumes that each context is * attached to at most one query.) */ @SuppressWarnings( "unused" ) private boolean modulesChanged = true; /** The set of statically known documents specified as an array of paths to documents and collections. */ protected XmldbURI[] staticDocumentPaths = null; /** The actual set of statically known documents. This will be generated on demand from staticDocumentPaths. */ protected DocumentSet staticDocuments = null; /** The set of statically known documents specified as an array of paths to documents and collections. */ protected XmldbURI[] staticCollections = null; /** * A set of documents which were modified during the query, usually through an XQuery update extension. The documents will be checked after the * query completed to see if a defragmentation run is needed. */ protected MutableDocumentSet modifiedDocuments = null; /** A general-purpose map to set attributes in the current query context. */ protected Map<String, Object> attributes = new HashMap<String, Object>(); protected AnyURIValue baseURI = AnyURIValue.EMPTY_URI; protected boolean baseURISetInProlog = false; protected String moduleLoadPath = "."; protected String defaultFunctionNamespace = Function.BUILTIN_FUNCTION_NS; protected AnyURIValue defaultElementNamespace = AnyURIValue.EMPTY_URI; protected AnyURIValue defaultElementNamespaceSchema = AnyURIValue.EMPTY_URI; /** The default collation URI. */ private String defaultCollation = Collations.CODEPOINT; /** Default Collator. Will be null for the default unicode codepoint collation. */ private Collator defaultCollator = null; /** Set to true to enable XPath 1.0 backwards compatibility. */ private boolean backwardsCompatible = false; /** Should whitespace inside node constructors be stripped? */ private boolean stripWhitespace = true; /** Should empty order greatest or least? */ private boolean orderEmptyGreatest = true; /** The context item set in the query prolog or externally */ private Sequence contextItem = Sequence.EMPTY_SEQUENCE; /** * The position of the currently processed item in the context sequence. This field has to be set on demand, for example, before calling the * fn:position() function. */ private int contextPosition = 0; private Sequence contextSequence = null; /** Shared name pool used by all in-memory documents constructed in this query context. */ private NamePool sharedNamePool = null; /** Stack for temporary document fragments. */ private Stack<MemTreeBuilder> fragmentStack = new Stack<MemTreeBuilder>(); /** The root of the expression tree. */ private Expression rootExpression; /** An incremental counter to count the expressions in the current XQuery. Used during compilation to assign a unique ID to every expression. */ private int expressionCounter = 0; /** * Should all documents loaded by the query be locked? If set to true, it is the responsibility of the calling client code to unlock documents * after the query has completed. */ // private boolean lockDocumentsOnLoad = false; /** Documents locked during the query. */ // private LockedDocumentMap lockedDocuments = null; private LockedDocumentMap protectedDocuments = null; /** The profiler instance used by this context. */ protected Profiler profiler; //For holding XQuery Context variables for general storage in the XQuery Context HashMap<String, Object> XQueryContextVars = new HashMap<String, Object>(); //For holding the environment variables Map<String,String> envs; private ContextUpdateListener updateListener = null; private boolean enableOptimizer = true; private boolean raiseErrorOnFailedRetrieval = XQUERY_RAISE_ERROR_ON_FAILED_RETRIEVAL_DEFAULT; private boolean isShared = false; private Source source = null; private DebuggeeJoint debuggeeJoint = null; private int xqueryVersion = 31; protected Database db; private boolean analyzed = false; /** * The Subject of the User that requested the execution of the XQuery * attached by this Context. This is not the same as the Effective User * as we may be executed setUid or setGid. The Effective User can be retrieved * through broker.getCurrentSubject() */ private Subject realUser; /** * Indicates whether a user from a http session * was pushed onto the current broker from {@link XQueryContext#prepareForExecution()}, * if so then we must pop the user in {@link XQueryContext#reset(boolean)} */ private boolean pushedUserFromHttpSession = false; public synchronized Optional<ExistRepository> getRepository() throws XPathException { return getBroker().getBrokerPool().getExpathRepo(); } private Module resolveInEXPathRepository(String namespace, String prefix) throws XPathException { // the repo and its eXist handler final Optional<ExistRepository> repo = getRepository(); // try an internal module if (repo.isPresent()) { final Module jMod = repo.get().resolveJavaModule(namespace, this); if (jMod != null) { return jMod; } } // try an eXist-specific module Path resolved = null; if (repo.isPresent()) { resolved = repo.get().resolveXQueryModule(namespace); // use the resolved file or return null if ( resolved == null ) { return null; } } // build a module object from the file final Source src = new FileSource(resolved, false); return compileOrBorrowModule(prefix, namespace, "", src); } // TODO: end of expath repo manager, may change protected XQueryContext( ) { profiler = new Profiler( null ); } public XQueryContext( Database db ) { this( ); this.db = db; loadDefaults( db.getConfiguration() ); this.profiler = new Profiler( db ); } public XQueryContext( XQueryContext copyFrom ) { this( ); this.db = copyFrom.db; loadDefaultNS(); final Iterator<String> prefixes = copyFrom.staticNamespaces.keySet().iterator(); while( prefixes.hasNext() ) { final String prefix = prefixes.next(); if( "xml".equals(prefix) || "xmlns".equals(prefix) ) { continue; } try { declareNamespace( prefix, copyFrom.staticNamespaces.get( prefix ) ); } catch( final XPathException ex ) { ex.printStackTrace(); } } this.profiler = copyFrom.profiler; } /** * Returns true if this context has a parent context (means it is a module context). * * @return False. */ public boolean hasParent() { return( false ); } public XQueryContext getRootContext() { return( this ); } public XQueryContext copyContext() { final XQueryContext ctx = new XQueryContext( this ); copyFields( ctx ); return( ctx ); } /** * Update the current dynamic context using the properties of another context. This is needed by {@link org.exist.xquery.functions.util.Eval}. * * @param from */ public void updateContext( XQueryContext from ) { this.watchdog = from.watchdog; this.lastVar = from.lastVar; this.variableStackSize = from.getCurrentStackSize(); this.contextStack = from.contextStack; this.inScopeNamespaces = from.inScopeNamespaces; this.inScopePrefixes = from.inScopePrefixes; this.inheritedInScopeNamespaces = from.inheritedInScopeNamespaces; this.inheritedInScopePrefixes = from.inheritedInScopePrefixes; this.variableStackSize = from.variableStackSize; this.attributes = from.attributes; this.updateListener = from.updateListener; this.modules = from.modules; this.allModules = from.allModules; this.mappedModules = from.mappedModules; this.dynamicOptions = from.dynamicOptions; this.staticOptions = from.staticOptions; this.db = from.db; } protected void copyFields( XQueryContext ctx ) { ctx.calendar = this.calendar; ctx.implicitTimeZone = this.implicitTimeZone; ctx.baseURI = this.baseURI; ctx.baseURISetInProlog = this.baseURISetInProlog; ctx.staticDocumentPaths = this.staticDocumentPaths; ctx.staticDocuments = this.staticDocuments; ctx.moduleLoadPath = this.moduleLoadPath; ctx.defaultFunctionNamespace = this.defaultFunctionNamespace; ctx.defaultElementNamespace = this.defaultElementNamespace; ctx.defaultCollation = this.defaultCollation; ctx.defaultCollator = this.defaultCollator; ctx.backwardsCompatible = this.backwardsCompatible; ctx.enableOptimizer = this.enableOptimizer; ctx.stripWhitespace = this.stripWhitespace; ctx.preserveNamespaces = this.preserveNamespaces; ctx.inheritNamespaces = this.inheritNamespaces; ctx.orderEmptyGreatest = this.orderEmptyGreatest; ctx.declaredFunctions = new TreeMap<FunctionId, UserDefinedFunction>( this.declaredFunctions ); ctx.globalVariables = new TreeMap<QName, Variable>( this.globalVariables ); ctx.attributes = new HashMap<String, Object>( this.attributes ); // make imported modules available in the new context ctx.modules = new HashMap<String, Module>(); for( final Module module : this.modules.values() ) { try { ctx.modules.put( module.getNamespaceURI(), module ); final String prefix = this.staticPrefixes.get( module.getNamespaceURI() ); ctx.declareNamespace( prefix, module.getNamespaceURI() ); } catch( final XPathException e ) { // ignore } } ctx.allModules = new HashMap<String, Module>(); for( final Module module : this.allModules.values() ) { if( module != null ) { //UNDERSTAND: why is it possible? -shabanovd ctx.allModules.put( module.getNamespaceURI(), module ); } } ctx.watchdog = this.watchdog; ctx.profiler = getProfiler(); ctx.lastVar = this.lastVar; ctx.variableStackSize = getCurrentStackSize(); ctx.contextStack = this.contextStack; ctx.mappedModules = new HashMap<String, XmldbURI>( this.mappedModules ); ctx.staticNamespaces = new HashMap<String, String>( this.staticNamespaces ); ctx.staticPrefixes = new HashMap<String, String>( this.staticPrefixes ); if (this.dynamicOptions != null){ ctx.dynamicOptions = new ArrayList<Option>( this.dynamicOptions ); } if (this.staticOptions != null){ ctx.staticOptions = new ArrayList<Option>( this.staticOptions ); } ctx.source = this.source; } /** * Prepares the current context before xquery execution. */ @Override public void prepareForExecution() { //if there is an existing user in the current http session //then set the DBBroker user final Subject user = getUserFromHttpSession(); if(user != null) { getBroker().pushSubject(user); //this will be popped in {@link XQueryContext#reset(boolean)} this.pushedUserFromHttpSession = true; } setRealUser(getBroker().getCurrentSubject()); //this will be unset in {@link XQueryContext#reset(boolean)} //Reset current context position setContextSequencePosition( 0, null ); //Note that, for some reasons, an XQueryContext might be used without calling this method } public void setContextItem(Sequence contextItem) { this.contextItem = contextItem; } public Sequence getContextItem() { return contextItem; } /** * Is profiling enabled? * * @return true if profiling is enabled for this context. */ public boolean isProfilingEnabled() { return( profiler.isEnabled() ); } public boolean isProfilingEnabled( int verbosity ) { return( profiler.isEnabled() && ( profiler.verbosity() >= verbosity ) ); } /** * Returns the {@link Profiler} instance of this context if profiling is enabled. * * @return the profiler instance. */ public Profiler getProfiler() { return( profiler ); } /** * Called from the XQuery compiler to set the root expression for this context. * * @param expr */ public void setRootExpression( Expression expr ) { this.rootExpression = expr; } /** * Returns the root expression of the XQuery associated with this context. * * @return root expression */ public Expression getRootExpression() { return( rootExpression ); } /** * Returns the next unique expression id. Every expression in the XQuery is identified by a unique id. During compilation, expressions are * assigned their id by calling this method. * * @return The next unique expression id. */ protected int nextExpressionId() { return( expressionCounter++ ); } /** * Returns the number of expression objects in the internal representation of the query. Used to estimate the size of the query. * * @return number of expression objects */ public int getExpressionCount() { return( expressionCounter ); } /** * Declare a user-defined static prefix/namespace mapping. * * <p>eXist internally keeps a table containing all prefix/namespace mappings it found in documents, which have been previously stored into the * database. These default mappings need not to be declared explicitely.</p> * * @param prefix * @param uri * * @throws XPathException */ public void declareNamespace( String prefix, String uri ) throws XPathException { if( prefix == null ) { prefix = ""; } if( uri == null ) { uri = ""; } if( "xml".equals(prefix) || "xmlns".equals(prefix) ) { throw( new XPathException( ErrorCodes.XQST0070, "Namespace predefined prefix '" + prefix + "' can not be bound" ) ); } if( uri.equals( Namespaces.XML_NS ) ) { throw( new XPathException( ErrorCodes.XQST0070, "Namespace URI '" + uri + "' must be bound to the 'xml' prefix" ) ); } final String prevURI = staticNamespaces.get( prefix ); //This prefix was not bound if( prevURI == null ) { if( uri.length() > 0 ) { //Bind it staticNamespaces.put( prefix, uri ); staticPrefixes.put( uri, prefix ); return; } else { //Nothing to bind //TODO : check the specs : unbinding an NS which is not already bound may be disallowed. LOG.warn( "Unbinding unbound prefix '" + prefix + "'" ); } } else { //This prefix was bound //Unbind it if( uri.length() == 0 ) { // if an empty namespace is specified, // remove any existing mapping for this namespace //TODO : improve, since XML_NS can't be unbound staticPrefixes.remove( uri ); staticNamespaces.remove( prefix ); return; } //those prefixes can be rebound to different URIs if( ( "xs".equals(prefix) && Namespaces.SCHEMA_NS.equals( prevURI ) ) || ( "xsi".equals(prefix) && Namespaces.SCHEMA_INSTANCE_NS.equals( prevURI ) ) || ( "xdt".equals(prefix) && Namespaces.XPATH_DATATYPES_NS.equals( prevURI ) ) || ( "fn".equals(prefix) && Namespaces.XPATH_FUNCTIONS_NS.equals( prevURI ) ) || ( "math".equals(prefix)) && Namespaces.XPATH_FUNCTIONS_MATH_NS.equals( prevURI ) || ( "local".equals(prefix) && Namespaces.XQUERY_LOCAL_NS.equals( prevURI ) ) ) { staticPrefixes.remove( prevURI ); staticNamespaces.remove( prefix ); if( uri.length() > 0 ) { staticNamespaces.put( prefix, uri ); staticPrefixes.put( uri, prefix ); return; } else { //Nothing to bind (not sure if it should raise an error though) //TODO : check the specs : unbinding an NS which is not already bound may be disallowed. LOG.warn( "Unbinding unbound prefix '" + prefix + "'" ); } } else { //Forbids rebinding the *same* prefix in a *different* namespace in this *same* context if( !uri.equals( prevURI ) ) { throw new XPathException(ErrorCodes.XQST0033, "Cannot bind prefix '" + prefix + "' to '" + uri + "' it is already bound to '" + prevURI + "'"); } } } } public void declareNamespaces( Map<String, String> namespaceMap ) { String prefix; String uri; for( final Map.Entry<String, String> entry : namespaceMap.entrySet() ) { prefix = entry.getKey(); uri = entry.getValue(); if( prefix == null ) { prefix = ""; } if( uri == null ) { uri = ""; } staticNamespaces.put( prefix, uri ); staticPrefixes.put( uri, prefix ); } } /** * Removes the namespace URI from the prefix/namespace mappings table. * * @param uri */ public void removeNamespace( String uri ) { staticPrefixes.remove( uri ); for( final Iterator<String> i = staticNamespaces.values().iterator(); i.hasNext(); ) { if( i.next().equals( uri ) ) { i.remove(); return; } } inScopePrefixes.remove( uri ); if( inScopeNamespaces != null ) { for( final Iterator<String> i = inScopeNamespaces.values().iterator(); i.hasNext(); ) { if( i.next().equals( uri ) ) { i.remove(); return; } } } //TODO : is this relevant ? inheritedInScopePrefixes.remove( uri ); if( inheritedInScopeNamespaces != null ) { for( final Iterator<String> i = inheritedInScopeNamespaces.values().iterator(); i.hasNext(); ) { if( i.next().equals( uri ) ) { i.remove(); return; } } } } /** * Declare an in-scope namespace. This is called during query execution. * * @param prefix * @param uri */ public void declareInScopeNamespace( String prefix, String uri ) { if( ( prefix == null ) || ( uri == null ) ) { throw( new IllegalArgumentException( "null argument passed to declareNamespace" ) ); } //Activate the namespace by removing it from the inherited namespaces if( inheritedInScopePrefixes.get( getURIForPrefix( prefix ) ) != null ) { inheritedInScopePrefixes.remove( uri ); } if( inheritedInScopeNamespaces.get( prefix ) != null ) { inheritedInScopeNamespaces.remove( prefix ); } inScopePrefixes.put( uri, prefix ); inScopeNamespaces.put( prefix, uri ); } public String getInScopeNamespace( String prefix ) { return( ( inScopeNamespaces == null ) ? null : inScopeNamespaces.get( prefix ) ); } public String getInScopePrefix( String uri ) { return( ( inScopePrefixes == null ) ? null : inScopePrefixes.get( uri ) ); } public Map<String, String> getInScopePrefixes( ) { return( ( inScopePrefixes == null ) ? null : inScopePrefixes ); } public String getInheritedNamespace( String prefix ) { return( ( inheritedInScopeNamespaces == null ) ? null : inheritedInScopeNamespaces.get( prefix ) ); } public String getInheritedPrefix( String uri ) { return( ( inheritedInScopePrefixes == null ) ? null : inheritedInScopePrefixes.get( uri ) ); } /** * Return the namespace URI mapped to the registered prefix or null if the prefix is not registered. * * @param prefix * * @return namespace */ public String getURIForPrefix( String prefix ) { // try in-scope namespace declarations String uri = ( inScopeNamespaces == null ) ? null : inScopeNamespaces.get( prefix ); if( uri != null ) { return( uri ); } if( inheritNamespaces ) { uri = ( inheritedInScopeNamespaces == null ) ? null : inheritedInScopeNamespaces.get( prefix ); if( uri != null ) { return( uri ); } } return( staticNamespaces.get( prefix ) ); /* old code checked namespaces first String ns = (String) namespaces.get(prefix); if (ns == null) // try in-scope namespace declarations return inScopeNamespaces == null ? null : (String) inScopeNamespaces.get(prefix); else return ns; */ } /** * Get URI Prefix * * @param uri * * @return the prefix mapped to the registered URI or null if the URI is not registered. */ public String getPrefixForURI( String uri ) { String prefix = ( inScopePrefixes == null ) ? null : inScopePrefixes.get( uri ); if( prefix != null ) { return( prefix ); } if( inheritNamespaces ) { prefix = ( inheritedInScopePrefixes == null ) ? null : inheritedInScopePrefixes.get( uri ); if( prefix != null ) { return( prefix ); } } return( staticPrefixes.get( uri ) ); } /** * Clear all user-defined prefix/namespace mappings. * * @return */ // TODO: remove since never used? // public void clearNamespaces() { // staticNamespaces.clear(); // staticPrefixes.clear(); // if (inScopeNamespaces != null) { // inScopeNamespaces.clear(); // inScopePrefixes.clear(); // } // //TODO : it this relevant ? // if (inheritedInScopeNamespaces != null) { // inheritedInScopeNamespaces.clear(); // inheritedInScopePrefixes.clear(); // } // loadDefaults(broker.getConfiguration()); // } /** * Returns the current default function namespace. * * @return current default function namespace */ public String getDefaultFunctionNamespace() { return( defaultFunctionNamespace ); } /** * Set the default function namespace. By default, this points to the namespace for XPath built-in functions. * * @param uri * * @throws XPathException */ public void setDefaultFunctionNamespace( String uri ) throws XPathException { //Not sure for the 2nd clause : eXist forces the function NS as default. if( ( defaultFunctionNamespace != null ) && !defaultFunctionNamespace.equals( Function.BUILTIN_FUNCTION_NS ) && !defaultFunctionNamespace.equals( uri ) ) { throw( new XPathException( "err:XQST0066: default function namespace is already set to: '" + defaultFunctionNamespace + "'" ) ); } defaultFunctionNamespace = uri; } /** * Returns the current default element namespace. * * @return current default element namespace schema * * @throws XPathException */ public String getDefaultElementNamespaceSchema() throws XPathException { return( defaultElementNamespaceSchema.getStringValue() ); } /** * Set the default element namespace. By default, this points to the empty uri. * * @param uri * * @throws XPathException */ public void setDefaultElementNamespaceSchema( String uri ) throws XPathException { // eXist forces the empty element NS as default. if( !defaultElementNamespaceSchema.equals( AnyURIValue.EMPTY_URI ) ) { throw( new XPathException( "err:XQST0066: default function namespace schema is already set to: '" + defaultElementNamespaceSchema.getStringValue() + "'" ) ); } defaultElementNamespaceSchema = new AnyURIValue( uri ); } /** * Returns the current default element namespace. * * @return current default element namespace * * @throws XPathException */ public String getDefaultElementNamespace() throws XPathException { return( defaultElementNamespace.getStringValue() ); } /** * Set the default element namespace. By default, this points to the empty uri. * * @param uri a <code>String</code> value * @param schema a <code>String</code> value * * @exception XPathException if an error occurs */ public void setDefaultElementNamespace( String uri, String schema ) throws XPathException { // eXist forces the empty element NS as default. if( !defaultElementNamespace.equals( AnyURIValue.EMPTY_URI ) ) { throw( new XPathException( "err:XQST0066: default element namespace is already set to: '" + defaultElementNamespace.getStringValue() + "'" ) ); } defaultElementNamespace = new AnyURIValue( uri ); if( schema != null ) { defaultElementNamespaceSchema = new AnyURIValue( schema ); } } /** * Set the default collation to be used by all operators and functions on strings. Throws an exception if the collation is unknown or cannot be * instantiated. * * @param uri * * @throws XPathException */ public void setDefaultCollation( String uri ) throws XPathException { if( uri.equals( Collations.CODEPOINT ) || uri.equals( Collations.CODEPOINT_SHORT ) ) { defaultCollation = Collations.CODEPOINT; defaultCollator = null; } URI uriTest; try { uriTest = new URI( uri ); } catch( final URISyntaxException e ) { throw( new XPathException( "err:XQST0038: Unknown collation : '" + uri + "'" ) ); } if( uri.startsWith( Collations.EXIST_COLLATION_URI ) || uri.startsWith( "?" ) || uriTest.isAbsolute() ) { defaultCollator = Collations.getCollationFromURI( this, uri ); defaultCollation = uri; } else { String absUri = getBaseURI().getStringValue() + uri; defaultCollator = Collations.getCollationFromURI( this, absUri ); defaultCollation = absUri; } } public String getDefaultCollation() { return( defaultCollation ); } public Collator getCollator( String uri ) throws XPathException { if( uri == null ) { return( defaultCollator ); } return( Collations.getCollationFromURI( this, uri ) ); } public Collator getDefaultCollator() { return( defaultCollator ); } /** * Set the set of statically known documents for the current execution context. These documents will be processed if no explicit document set has * been set for the current expression with fn:doc() or fn:collection(). * * @param docs */ public void setStaticallyKnownDocuments( XmldbURI[] docs ) { staticDocumentPaths = docs; } public void setStaticallyKnownDocuments( DocumentSet set ) { staticDocuments = set; } //TODO : not sure how these 2 options might/have to be related public void setCalendar( XMLGregorianCalendar newCalendar ) { this.calendar = (XMLGregorianCalendar)newCalendar.clone(); } public void setTimeZone( TimeZone newTimeZone ) { this.implicitTimeZone = newTimeZone; } public XMLGregorianCalendar getCalendar() { //TODO : we might prefer to return null if( calendar == null ) { try { //Initialize to current dateTime calendar = DatatypeFactory.newInstance().newXMLGregorianCalendar( new GregorianCalendar() ); } catch( final DatatypeConfigurationException e ) { LOG.error( e.getMessage(), e ); } } //That's how we ensure stability of that static context function return( calendar ); } public TimeZone getImplicitTimeZone() { if( implicitTimeZone == null ) { implicitTimeZone = TimeZone.getDefault(); if( implicitTimeZone.inDaylightTime( new Date() ) ) { implicitTimeZone.setRawOffset( implicitTimeZone.getRawOffset() + implicitTimeZone.getDSTSavings() ); } } //That's how we ensure stability of that static context function return( this.implicitTimeZone ); } /** * Get statically known documents * * @return set of statically known documents. * * @throws XPathException */ public DocumentSet getStaticallyKnownDocuments() throws XPathException { if( staticDocuments != null ) { // the document set has already been built, return it return( staticDocuments ); } if( protectedDocuments != null ) { staticDocuments = protectedDocuments.toDocumentSet(); return( staticDocuments ); } MutableDocumentSet ndocs = new DefaultDocumentSet( 1031 ); if( staticDocumentPaths == null ) { // no path defined: return all documents in the db try { getBroker().getAllXMLResources( ndocs ); } catch(final PermissionDeniedException pde) { LOG.warn("Permission denied to read resource all resources" + pde.getMessage(), pde); throw new XPathException("Permission denied to read resource all resources" + pde.getMessage(), pde); } } else { DocumentImpl doc; Collection collection; for( int i = 0; i < staticDocumentPaths.length; i++ ) { try { collection = getBroker().getCollection( staticDocumentPaths[i] ); if( collection != null ) { collection.allDocs( getBroker(), ndocs, true); } else { doc = getBroker().getXMLResource( staticDocumentPaths[i], LockMode.READ_LOCK ); if( doc != null ) { if( doc.getPermissions().validate( getBroker().getCurrentSubject(), Permission.READ ) ) { ndocs.add( doc ); } doc.getUpdateLock().release( LockMode.READ_LOCK ); } } } catch( final PermissionDeniedException e ) { LOG.warn( "Permission denied to read resource " + staticDocumentPaths[i] + ". Skipping it." ); } } } staticDocuments = ndocs; return( staticDocuments ); } public DocumentSet getStaticDocs() { return staticDocuments; } public ExtendedXMLStreamReader getXMLStreamReader( NodeValue nv ) throws XMLStreamException, IOException { ExtendedXMLStreamReader reader; if( nv.getImplementationType() == NodeValue.IN_MEMORY_NODE ) { final NodeImpl node = (NodeImpl)nv; reader = new InMemoryXMLStreamReader( node.getOwnerDocument(), node.getOwnerDocument() ); } else { final NodeProxy proxy = (NodeProxy)nv; reader = getBroker().newXMLStreamReader( new NodeProxy( proxy.getOwnerDocument(), NodeId.DOCUMENT_NODE, proxy.getOwnerDocument().getFirstChildAddress() ), false ); } return( reader ); } public void setProtectedDocs( LockedDocumentMap map ) { this.protectedDocuments = map; } public LockedDocumentMap getProtectedDocs() { return( this.protectedDocuments ); } public boolean inProtectedMode() { return( protectedDocuments != null ); } /** * Should loaded documents be locked? * * <p>see #setLockDocumentsOnLoad(boolean)</p> */ public boolean lockDocumentsOnLoad() { return( false ); } // /** // * If lock is true, all documents loaded during query execution // * will be locked. This way, we avoid that query results become // * invalid before the entire result has been processed by the client // * code. All attempts to modify nodes which are part of the result // * set will be blocked. // * // * However, it is the client's responsibility to proper unlock // * all documents once processing is completed. // * // * @param lock // */ // public void setLockDocumentsOnLoad(boolean lock) { // lockDocumentsOnLoad = lock; // if(lock) // lockedDocuments = new LockedDocumentMap(); // } public void addLockedDocument( DocumentImpl doc ) { // if (lockedDocuments != null) // lockedDocuments.add(doc); } // /** // * Release all locks on documents that have been locked // * during query execution. // * // *@see #setLockDocumentsOnLoad(boolean) // */ // public void releaseLockedDocuments() { // if(lockedDocuments != null) // lockedDocuments.unlock(); // lockDocumentsOnLoad = false; // lockedDocuments = null; // } // /** // * Release all locks on documents not being referenced by the sequence. // * This is called after query execution has completed. Only locks on those // * documents contained in the final result set will be preserved. All other // * locks are released as they are no longer needed. // * // * @param seq // * @throws XPathException // */ // public LockedDocumentMap releaseUnusedDocuments(Sequence seq) throws XPathException { // if(lockedDocuments == null) // return null; // // determine the set of documents referenced by nodes in the sequence // DocumentSet usedDocs = new DocumentSet(); // for(SequenceIterator i = seq.iterate(); i.hasNext(); ) { // Item next = i.nextItem(); // if(Type.subTypeOf(next.getType(), Type.NODE)) { // NodeValue node = (NodeValue) next; // if(node.getImplementationType() == NodeValue.PERSISTENT_NODE) { // DocumentImpl doc = ((NodeProxy)node).getDocument(); // if(!usedDocs.contains(doc.getDocId())) // usedDocs.add(doc, false); // } // } // } // LockedDocumentMap remaining = lockedDocuments.unlockSome(usedDocs); // lockDocumentsOnLoad = false; // lockedDocuments = null; // return remaining; // } public void setShared( boolean shared ) { isShared = shared; } public boolean isShared() { return( isShared ); } public void addModifiedDoc( DocumentImpl document ) { if( modifiedDocuments == null ) { modifiedDocuments = new DefaultDocumentSet(); } modifiedDocuments.add( document ); } public void reset() { reset( false ); } /** * Prepare this XQueryContext to be reused. This should be called when adding an XQuery to the cache. * * @param keepGlobals */ @Override public void reset(final boolean keepGlobals) { setRealUser(null); if(this.pushedUserFromHttpSession) { try { getBroker().popSubject(); } finally { this.pushedUserFromHttpSession = false; } } if( modifiedDocuments != null ) { try { Modification.checkFragmentation( this, modifiedDocuments ); } catch( final EXistException e ) { LOG.warn( "Error while checking modified documents: " + e.getMessage(), e ); } modifiedDocuments = null; } calendar = null; implicitTimeZone = null; resetDocumentBuilder(); contextSequence = null; contextItem = Sequence.EMPTY_SEQUENCE; if( !keepGlobals ) { // do not reset the statically known documents staticDocumentPaths = null; staticDocuments = null; } if( !isShared ) { lastVar = null; } fragmentStack = new Stack<MemTreeBuilder>(); callStack.clear(); protectedDocuments = null; if( !keepGlobals ) { globalVariables.clear(); } if( dynamicOptions != null ) { dynamicOptions.clear(); //clear any dynamic options } if( !isShared ) { watchdog.reset(); } for( final Module module : modules.values() ) { if (module instanceof ExternalModule && ((ModuleContext)((ExternalModule)module).getContext()).getParentContext() != this) { continue; } module.reset( this ); } if( !keepGlobals ) { mappedModules.clear(); } savedState.restore(); //remove the context-vars, subsequent execution of the query //may generate different values for the vars based on the //content of the db XQueryContextVars.clear(); attributes.clear(); clearUpdateListeners(); profiler.reset(); analyzed = false; } /** * Returns true if whitespace between constructed element nodes should be stripped by default. */ public boolean stripWhitespace() { return( stripWhitespace ); } public void setStripWhitespace( boolean strip ) { this.stripWhitespace = strip; } /** * Returns true if namespaces for constructed element and document nodes should be preserved on copy by default. */ public boolean preserveNamespaces() { return( preserveNamespaces ); } /** * The method <code>setPreserveNamespaces.</code> * * @param preserve a <code>boolean</code> value */ public void setPreserveNamespaces( final boolean preserve ) { this.preserveNamespaces = preserve; } /** * Returns true if namespaces for constructed element and document nodes * should be inherited on copy by default. */ public boolean inheritNamespaces() { return( inheritNamespaces ); } /** * The method <code>setInheritNamespaces.</code> * * @param inherit a <code>boolean</code> value */ public void setInheritNamespaces( final boolean inherit ) { this.inheritNamespaces = inherit; } /** * Returns true if order empty is set to greatest, otherwise false for order empty is least. */ public boolean orderEmptyGreatest() { return( orderEmptyGreatest ); } /** * The method <code>setOrderEmptyGreatest.</code> * * @param order a <code>boolean</code> value */ public void setOrderEmptyGreatest( final boolean order ) { this.orderEmptyGreatest = order; } /** * Get modules * * @return iterator over all modules imported into this context */ public Iterator<Module> getModules() { return( modules.values().iterator() ); } /** * Get root modules * * @return iterator over all modules registered in the entire context tree */ public Iterator<Module> getRootModules() { return( getAllModules() ); } public Iterator<Module> getAllModules() { return( allModules.values().iterator() ); } /** * Get the built-in module registered for the given namespace URI. * * @param namespaceURI * * @return built-in module */ public Module getModule( String namespaceURI ) { return( modules.get( namespaceURI ) ); } public Module getRootModule( String namespaceURI ) { return( allModules.get( namespaceURI ) ); } public void setModule( String namespaceURI, Module module ) { if( module == null ) { modules.remove( namespaceURI ); // unbind the module } else { modules.put( namespaceURI, module ); } setRootModule( namespaceURI, module ); } protected void setRootModule( String namespaceURI, Module module ) { if( module == null ) { allModules.remove( namespaceURI ); // unbind the module return; } if( allModules.get( namespaceURI ) != module ) { setModulesChanged(); } allModules.put( namespaceURI, module ); } void setModulesChanged() { this.modulesChanged = true; } /** * For compiled expressions: check if the source of any module imported by the current * query has changed since compilation. */ public boolean checkModulesValid() { for (final Module module : allModules.values() ) { if( !module.isInternalModule() ) { if( !( (ExternalModule)module ).moduleIsValid( getBroker() ) ) { LOG.debug( "Module with URI " + module.getNamespaceURI() + " has changed and needs to be reloaded" ); return( false ); } } } return( true ); } public void analyzeAndOptimizeIfModulesChanged( Expression expr ) throws XPathException { if (analyzed) {return;} analyzed = true; for (final Module module : expr.getContext().modules.values()) { if( !module.isInternalModule() ) { final Expression root = ((ExternalModule)module).getRootExpression(); ((ExternalModule)module).getContext().analyzeAndOptimizeIfModulesChanged(root); } } expr.analyze( new AnalyzeContextInfo() ); if( optimizationsEnabled() ) { final Optimizer optimizer = new Optimizer( this ); expr.accept( optimizer ); if( optimizer.hasOptimized() ) { reset( true ); expr.resetState( true ); expr.analyze( new AnalyzeContextInfo() ); } } modulesChanged = false; } /** * Load a built-in module from the given class name and assign it to the namespace URI. The specified class should be a subclass of {@link * Module}. The method will try to instantiate the class. If the class is not found or an exception is thrown, the method will silently fail. The * namespace URI has to be equal to the namespace URI declared by the module class. Otherwise, the module is not loaded. * * @param namespaceURI * @param moduleClass * * @return Module */ public Module loadBuiltInModule( String namespaceURI, String moduleClass ) { Module module = null; if (namespaceURI != null) {module = getModule( namespaceURI );} if( module != null ) { // LOG.debug("module " + namespaceURI + " is already present"); return( module ); } return( initBuiltInModule( namespaceURI, moduleClass ) ); } @SuppressWarnings( "unchecked" ) protected Module initBuiltInModule( String namespaceURI, String moduleClass ) { Module module = null; try { // lookup the class final Class<?> mClass = Class.forName( moduleClass ); if( !( Module.class.isAssignableFrom( mClass ) ) ) { LOG.info( "failed to load module. " + moduleClass + " is not an instance of org.exist.xquery.Module." ); return( null ); } //instantiateModule( namespaceURI, (Class<Module>)mClass ); // INOTE: expathrepo module = instantiateModule( namespaceURI, (Class<Module>)mClass, (Map<String, Map<String, List<? extends Object>>>) getBroker().getConfiguration().getProperty(PROPERTY_MODULE_PARAMETERS)); //LOG.debug("module " + module.getNamespaceURI() + " loaded successfully."); } catch( final ClassNotFoundException e ) { LOG.warn( "module class " + moduleClass + " not found. Skipping..." ); } return( module ); } protected Module instantiateModule( String namespaceURI, Class<Module> mClass, Map<String, Map<String, List<? extends Object>>> moduleParameters) { Module module = null; try { final Constructor<Module> cnstr = mClass.getConstructor(Map.class); module = cnstr.newInstance(moduleParameters.get(namespaceURI)); if(namespaceURI != null && !module.getNamespaceURI().equals(namespaceURI)) { LOG.warn( "the module declares a different namespace URI. Expected: " + namespaceURI + " found: " + module.getNamespaceURI() ); return( null ); } if((getPrefixForURI( module.getNamespaceURI() ) == null) && (module.getDefaultPrefix().length() > 0)) { declareNamespace( module.getDefaultPrefix(), module.getNamespaceURI() ); } modules.put(module.getNamespaceURI(), module); allModules.put(module.getNamespaceURI(), module); } catch(final InstantiationException ie) { LOG.warn("error while instantiating module class " + mClass.getName(), ie); } catch(final IllegalAccessException iae) { LOG.warn("error while instantiating module class " + mClass.getName(), iae); } catch(final XPathException xpe) { LOG.warn("error while instantiating module class " + mClass.getName(), xpe); } catch(final NoSuchMethodException nsme) { LOG.warn("error while instantiating module class " + mClass.getName(), nsme); } catch(final InvocationTargetException ite) { LOG.warn("error while instantiating module class " + mClass.getName(), ite); } return module; } /** * Declare a user-defined function. All user-defined functions are kept in a single hash map. * * @param function * * @throws XPathException */ public void declareFunction( UserDefinedFunction function ) throws XPathException { // TODO: redeclaring functions should be forbidden. however, throwing an // exception will currently break util:eval. final QName name = function.getSignature().getName(); if(Namespaces.XML_NS.equals(name.getNamespaceURI())) { throw new XPathException(function, ErrorCodes.XQST0045, "Function '" + name + "' is in the forbidden namespace '" + Namespaces.XML_NS + "'" ); } if(Namespaces.SCHEMA_NS.equals(name.getNamespaceURI())) { throw new XPathException(function, ErrorCodes.XQST0045, "Function '" + name + "' is in the forbidden namespace '" + Namespaces.SCHEMA_NS + "'"); } if(Namespaces.SCHEMA_INSTANCE_NS.equals(name.getNamespaceURI())) { throw new XPathException(function, ErrorCodes.XQST0045, "Function '" + name + "' is in the forbidden namespace '" + Namespaces.SCHEMA_INSTANCE_NS + "'"); } if(Namespaces.XPATH_FUNCTIONS_NS.equals(name.getNamespaceURI())) { throw new XPathException(function, ErrorCodes.XQST0045, "Function '" + name + "' is in the forbidden namespace '" + Namespaces.XPATH_FUNCTIONS_NS + "'"); } if("".equals( name.getNamespaceURI())) { throw new XPathException(function, ErrorCodes.XQST0060, "Every declared function name must have a non-null namespace URI, but function '" + name + "' does not meet this requirement."); } declaredFunctions.put( function.getSignature().getFunctionId(), function ); // if (declaredFunctions.get(function.getSignature().getFunctionId()) == null) // declaredFunctions.put(function.getSignature().getFunctionId(), function); // else // throw new XPathException("XQST0034: function " + function.getName() + " is already defined with the same arity"); } /** * Resolve a user-defined function. * * @param name * @param argCount * * @return user-defined function * * @throws XPathException */ public UserDefinedFunction resolveFunction( QName name, int argCount ) throws XPathException { final FunctionId id = new FunctionId( name, argCount ); final UserDefinedFunction func = declaredFunctions.get( id ); return( func ); } public Iterator<FunctionSignature> getSignaturesForFunction( QName name ) { final ArrayList<FunctionSignature> signatures = new ArrayList<FunctionSignature>( 2 ); for( final UserDefinedFunction func : declaredFunctions.values() ) { if( func.getName().equals( name ) ) { signatures.add( func.getSignature() ); } } return( signatures.iterator() ); } public Iterator<UserDefinedFunction> localFunctions() { return( declaredFunctions.values().iterator() ); } /** * Declare a local variable. This is called by variable binding expressions like "let" and "for". * * @param var * * @return LocalVariable * * @throws XPathException */ public LocalVariable declareVariableBinding( LocalVariable var ) throws XPathException { if( lastVar == null ) { lastVar = var; } else { lastVar.addAfter( var ); lastVar = var; } var.setStackPosition( getCurrentStackSize() ); return( var ); } /** * Declare a global variable as by "declare variable". * * @param var * * @return Variable * * @throws XPathException */ public Variable declareGlobalVariable( Variable var ) throws XPathException { globalVariables.put( var.getQName(), var ); var.setStackPosition( getCurrentStackSize() ); return( var ); } public void undeclareGlobalVariable( QName name ) { globalVariables.remove(name); } /** * Declare a user-defined variable. * * <p>The value argument is converted into an XPath value (@see XPathUtil#javaObjectToXPath(Object)).</p> * * @param qname the qualified name of the new variable. Any namespaces should have been declared before. * @param value a Java object, representing the fixed value of the variable * * @return the created Variable object * * @throws XPathException if the value cannot be converted into a known XPath value or the variable QName references an unknown * namespace-prefix. */ public Variable declareVariable( String qname, Object value ) throws XPathException { return( declareVariable( QName.parse( this, qname, null ), value ) ); } public Variable declareVariable( QName qn, Object value ) throws XPathException { Variable var; final Module module = getModule( qn.getNamespaceURI() ); if( module != null ) { var = module.declareVariable( qn, value ); return( var ); } final Sequence val = XPathUtil.javaObjectToXPath( value, this ); var = globalVariables.get( qn ); if( var == null ) { var = new VariableImpl( qn ); globalVariables.put( qn, var ); } if( var.getSequenceType() != null ) { int actualCardinality; if( val.isEmpty() ) { actualCardinality = Cardinality.EMPTY; } else if( val.hasMany() ) { actualCardinality = Cardinality.MANY; } else { actualCardinality = Cardinality.ONE; } //Type.EMPTY is *not* a subtype of other types ; checking cardinality first if( !Cardinality.checkCardinality( var.getSequenceType().getCardinality(), actualCardinality ) ) { throw( new XPathException( "XPTY0004: Invalid cardinality for variable $" + var.getQName() + ". Expected " + Cardinality.getDescription( var.getSequenceType().getCardinality() ) + ", got " + Cardinality.getDescription( actualCardinality ) ) ); } //TODO : ignore nodes right now ; they are returned as xs:untypedAtomicType if( !Type.subTypeOf( var.getSequenceType().getPrimaryType(), Type.NODE ) ) { if( !val.isEmpty() && !Type.subTypeOf( val.getItemType(), var.getSequenceType().getPrimaryType() ) ) { throw( new XPathException( "XPTY0004: Invalid type for variable $" + var.getQName() + ". Expected " + Type.getTypeName( var.getSequenceType().getPrimaryType() ) + ", got " + Type.getTypeName( val.getItemType() ) ) ); } //Here is an attempt to process the nodes correctly } else { //Same as above : we probably may factorize if( !val.isEmpty() && !Type.subTypeOf( val.getItemType(), var.getSequenceType().getPrimaryType() ) ) { throw( new XPathException( "XPTY0004: Invalid type for variable $" + var.getQName() + ". Expected " + Type.getTypeName( var.getSequenceType().getPrimaryType() ) + ", got " + Type.getTypeName( val.getItemType() ) ) ); } } } //TODO : should we allow global variable *re*declaration ? var.setValue( val ); return( var ); } /** * Try to resolve a variable. * * @param name the qualified name of the variable as string * * @return the declared Variable object * * @throws XPathException if the variable is unknown */ public Variable resolveVariable( String name ) throws XPathException { final QName qn = QName.parse( this, name, null ); return( resolveVariable( qn ) ); } /** * Try to resolve a variable. * * @param qname the qualified name of the variable * * @return the declared Variable object * * @throws XPathException if the variable is unknown */ public Variable resolveVariable( QName qname ) throws XPathException { Variable var; // check if the variable is declared local var = resolveLocalVariable( qname ); // check if the variable is declared in a module if( var == null ) { final Module module = getModule( qname.getNamespaceURI() ); if( module != null ) { var = module.resolveVariable( qname ); } } // check if the variable is declared global if( var == null ) { var = globalVariables.get( qname ); } //if (var == null) // throw new XPathException("variable $" + qname + " is not bound"); return( var ); } protected Variable resolveGlobalVariable(QName qname) { return globalVariables.get(qname); } protected Variable resolveLocalVariable( QName qname ) throws XPathException { final LocalVariable end = contextStack.isEmpty() ? null : contextStack.peek(); for( LocalVariable var = lastVar; var != null; var = var.before ) { if( var == end ) { return( null ); } if( qname.equals( var.getQName() ) ) { return( var ); } } return( null ); } public boolean isVarDeclared( QName qname ) { final Module module = getModule( qname.getNamespaceURI() ); if( module != null ) { if( module.isVarDeclared( qname ) ) { return( true ); } } return( globalVariables.get( qname ) != null ); } public Map<QName, Variable> getVariables() { final Map<QName, Variable> variables = new HashMap<QName, Variable>(); variables.putAll( globalVariables ); final LocalVariable end = contextStack.isEmpty() ? null : (LocalVariable)contextStack.peek(); for( LocalVariable var = lastVar; var != null; var = var.before ) { if( var == end ) { break; } variables.put( var.getQName(), var ); } return( variables ); } public Map<QName, Variable> getLocalVariables() { final Map<QName, Variable> variables = new HashMap<QName, Variable>(); final LocalVariable end = contextStack.isEmpty() ? null : (LocalVariable)contextStack.peek(); for ( LocalVariable var = lastVar; var != null; var = var.before ) { if ( var == end ) { break; } variables.put( var.getQName(), var ); } return ( variables ); } /** * Return a copy of all currently visible local variables. * Used by {@link InlineFunction} to implement closures. * * @return currently visible local variables as a stack */ public List<Variable> getLocalStack() { final List<Variable> variables = new ArrayList<Variable>(10); final LocalVariable end = contextStack.isEmpty() ? null : contextStack.peek(); for ( LocalVariable var = lastVar; var != null; var = var.before ) { if ( var == end ) { break; } variables.add( new LocalVariable(var, true) ); } return ( variables ); } public Map<QName, Variable> getGlobalVariables() { final Map<QName, Variable> variables = new HashMap<QName, Variable>(); variables.putAll( globalVariables ); return( variables ); } /** * Restore a saved stack of local variables. Used to implement closures. * * @param stack * @throws XPathException */ public void restoreStack(List<Variable> stack) throws XPathException { for (int i = stack.size() - 1; i > -1; i--) { declareVariableBinding((LocalVariable)stack.get(i)); } } /** * Turn on/off XPath 1.0 backwards compatibility. * * <p>If turned on, comparison expressions will behave like in XPath 1.0, i.e. if any one of the operands is a number, the other operand will be * cast to a double.</p> * * @param backwardsCompatible */ public void setBackwardsCompatibility( boolean backwardsCompatible ) { this.backwardsCompatible = backwardsCompatible; } /** * XPath 1.0 backwards compatibility turned on? * * <p>In XPath 1.0 compatible mode, additional conversions will be applied to values if a numeric value is expected.</p> */ public boolean isBackwardsCompatible() { return( this.backwardsCompatible ); } public boolean isRaiseErrorOnFailedRetrieval() { return( raiseErrorOnFailedRetrieval ); } public Database getDatabase() { return db; } /** * Get the DBBroker instance used for the current query. * * <p>The DBBroker is the main database access object, providing access to all internal database functions.</p> * * @return DBBroker instance */ public DBBroker getBroker() { return db.getActiveBroker(); } /** * Get the user which executes the current query. * * @return user * @deprecated use getCurrentSubject */ public Subject getUser() { return getSubject(); } /** * Get the subject which executes the current query. * * @return subject */ public Subject getSubject() { return getBroker().getCurrentSubject(); } /** * If there is a HTTP Session, and a User has been stored in the session then this will return the user object from the session. * * @return The user or null if there is no session or no user */ public Subject getUserFromHttpSession() { final RequestModule myModule = (RequestModule)getModule( RequestModule.NAMESPACE_URI ); //Sanity check : one may *not* want to bind the module ! if( myModule == null ) { return( null ); } Variable var = null; try { var = myModule.resolveVariable( RequestModule.REQUEST_VAR ); } catch( final XPathException xpe ) { return( null ); } if( ( var != null ) && ( var.getValue() != null ) ) { if( var.getValue().getItemType() == Type.JAVA_OBJECT ) { final JavaObjectValue reqValue = (JavaObjectValue)var.getValue().itemAt( 0 ); if( reqValue.getObject() instanceof RequestWrapper) { final RequestWrapper req = (RequestWrapper) reqValue.getObject(); final Object user = req.getAttribute(HTTP_REQ_ATTR_USER); final Object passAttr = req.getAttribute(HTTP_REQ_ATTR_PASS); if (user != null) { final String password = passAttr == null ? null : passAttr.toString(); try { return getBroker().getBrokerPool().getSecurityManager().authenticate(user.toString(), password); } catch (final AuthenticationException e) { LOG.error("User can not be authenticated: " + user.toString()); } } else { if (req.getSession() != null) { return (Subject) req.getSession().getAttribute(HTTP_SESSIONVAR_XMLDB_USER); } } } } } return( null ); } /** The builder used for creating in-memory document fragments. */ private MemTreeBuilder documentBuilder = null; /** * Get the document builder currently used for creating temporary document fragments. A new document builder will be created on demand. * * @return document builder */ @Override public MemTreeBuilder getDocumentBuilder() { if(documentBuilder == null) { documentBuilder = new MemTreeBuilder(this); documentBuilder.startDocument(); } return documentBuilder; } @Override public MemTreeBuilder getDocumentBuilder(boolean explicitCreation) { if(documentBuilder == null) { documentBuilder = new MemTreeBuilder(this); documentBuilder.startDocument(explicitCreation); } return documentBuilder; } private void resetDocumentBuilder() { setDocumentBuilder(null); } private void setDocumentBuilder(MemTreeBuilder documentBuilder) { this.documentBuilder = documentBuilder; } /** * Returns the shared name pool used by all in-memory documents which are created within this query context. Create a name pool for every document * would be a waste of memory, especially since it is likely that the documents contain elements or attributes with similar names. * * @return the shared name pool */ public NamePool getSharedNamePool() { if( sharedNamePool == null ) { sharedNamePool = new NamePool(); } return( sharedNamePool ); } /* DebuggeeJoint methods */ public XQueryContext getContext() { return( null ); } public void prologEnter(Expression expr) { if (debuggeeJoint != null) { debuggeeJoint.prologEnter(expr); } } public void expressionStart( Expression expr ) throws TerminatedException { if( debuggeeJoint != null ) { debuggeeJoint.expressionStart( expr ); } } public void expressionEnd( Expression expr ) { if( debuggeeJoint != null ) { debuggeeJoint.expressionEnd( expr ); } } public void stackEnter( Expression expr ) throws TerminatedException { if( debuggeeJoint != null ) { debuggeeJoint.stackEnter( expr ); } } public void stackLeave( Expression expr ) { if( debuggeeJoint != null ) { debuggeeJoint.stackLeave( expr ); } } /* Methods delegated to the watchdog */ public void proceed() throws TerminatedException { getWatchDog().proceed( null ); } public void proceed( Expression expr ) throws TerminatedException { getWatchDog().proceed( expr ); } public void proceed( Expression expr, MemTreeBuilder builder ) throws TerminatedException { getWatchDog().proceed( expr, builder ); } public void setWatchDog( XQueryWatchDog watchdog ) { this.watchdog = watchdog; } public XQueryWatchDog getWatchDog() { return( watchdog ); } /** * Push any document fragment created within the current execution context on the stack. */ public void pushDocumentContext() { fragmentStack.push(getDocumentBuilder()); resetDocumentBuilder(); } public void popDocumentContext() { if( !fragmentStack.isEmpty() ) { setDocumentBuilder(fragmentStack.pop()); } } /** * Set the base URI for the evaluation context. * * <p>This is the URI returned by the fn:base-uri() function.</p> * * @param uri */ public void setBaseURI( AnyURIValue uri ) { setBaseURI( uri, false ); } /** * Set the base URI for the evaluation context. * * <p>A base URI specified via the base-uri directive in the XQuery prolog overwrites any other setting.</p> * * @param uri * @param setInProlog */ public void setBaseURI( AnyURIValue uri, boolean setInProlog ) { if( baseURISetInProlog ) { return; } if( uri == null ) { baseURI = AnyURIValue.EMPTY_URI; } baseURI = uri; baseURISetInProlog = setInProlog; } /** * Set the path to a base directory where modules should be loaded from. Relative module paths will be resolved against this directory. The * property is usually set by the XQueryServlet or XQueryGenerator, but can also be specified manually. * * @param path */ @Override public void setModuleLoadPath(String path) { this.moduleLoadPath = path; } @Override public String getModuleLoadPath() { return moduleLoadPath; } /** * The method <code>isBaseURIDeclared.</code> * * @return a <code>boolean</code> value */ public boolean isBaseURIDeclared() { if( ( baseURI == null ) || baseURI.equals( AnyURIValue.EMPTY_URI ) ) { return( false ); } else { return( true ); } } /** * Get the base URI of the evaluation context. * * <p>This is the URI returned by the fn:base-uri() function.</p> * * @return base URI of the evaluation context * * @exception XPathException if an error occurs */ public AnyURIValue getBaseURI() throws XPathException { // the base URI in the static context is established according to the // principles outlined in [RFC3986] Section 5.1—that is, it defaults // first to the base URI of the encapsulating entity, then to the URI // used to retrieve the entity, and finally to an implementation-defined // default. If the URILiteral in the base URI declaration is a relative // URI, then it is made absolute by resolving it with respect to this // same hierarchy. // It is not intrinsically an error if this process fails to establish // an absolute base URI; however, the base URI in the static context // is then undefined, and any attempt to use its value may result in // an error [err:XPST0001]. if( ( baseURI == null ) || baseURI.equals( AnyURIValue.EMPTY_URI ) ) { //throw new XPathException("err:XPST0001: base URI of the static context has not been assigned a value."); // We catch and resolve this to the XmlDbURI.ROOT_COLLECTION_URI // at least in DocumentImpl so maybe we should do it here./ljo } return( baseURI ); } /** * Set the current context position, i.e. the position of the currently processed item in the context sequence. This value is required by some * expressions, e.g. fn:position(). * * @param pos * @param sequence */ public void setContextSequencePosition( int pos, Sequence sequence ) { contextPosition = pos; contextSequence = sequence; } /** * Get the current context position, i.e. the position of the currently processed item in the context sequence. * * @return current context position */ public int getContextPosition() { return( contextPosition ); } public Sequence getContextSequence() { return( contextSequence ); } public void pushInScopeNamespaces() { pushInScopeNamespaces( true ); } /** * Push all in-scope namespace declarations onto the stack. * * @param inherit */ @SuppressWarnings( "unchecked" ) public void pushInScopeNamespaces( boolean inherit ) { //TODO : push into an inheritedInScopeNamespaces HashMap... and return an empty HashMap final HashMap<String, String> m = (HashMap)inScopeNamespaces.clone(); final HashMap<String, String> p = (HashMap)inScopePrefixes.clone(); namespaceStack.push( inheritedInScopeNamespaces ); namespaceStack.push( inheritedInScopePrefixes ); namespaceStack.push( inScopeNamespaces ); namespaceStack.push( inScopePrefixes ); //Current namespaces now become inherited just like the previous inherited ones if( inherit ) { inheritedInScopeNamespaces = (HashMap)inheritedInScopeNamespaces.clone(); inheritedInScopeNamespaces.putAll( m ); inheritedInScopePrefixes = (HashMap)inheritedInScopePrefixes.clone(); inheritedInScopePrefixes.putAll( p ); } else { inheritedInScopeNamespaces = new HashMap<String, String>(); inheritedInScopePrefixes = new HashMap<String, String>(); } //TODO : consider dynamic instanciation inScopeNamespaces = new HashMap<String, String>(); inScopePrefixes = new HashMap<String, String>(); } public void popInScopeNamespaces() { inScopePrefixes = namespaceStack.pop(); inScopeNamespaces = namespaceStack.pop(); inheritedInScopePrefixes = namespaceStack.pop(); inheritedInScopeNamespaces = namespaceStack.pop(); } @SuppressWarnings( "unchecked" ) public void pushNamespaceContext() { HashMap<String, String> m = (HashMap)staticNamespaces.clone(); HashMap<String, String> p = (HashMap)staticPrefixes.clone(); namespaceStack.push( staticNamespaces ); namespaceStack.push( staticPrefixes ); staticNamespaces = m; staticPrefixes = p; } public void popNamespaceContext() { staticPrefixes = namespaceStack.pop(); staticNamespaces = namespaceStack.pop(); } /** * Returns the last variable on the local variable stack. The current variable context can be restored by passing the return value to {@link * #popLocalVariables(LocalVariable)}. * * @param newContext * * @return last variable on the local variable stack */ public LocalVariable markLocalVariables( boolean newContext ) { if( newContext ) { if( lastVar == null ) { lastVar = new LocalVariable( QName.EMPTY_QNAME ); } contextStack.push( lastVar ); } variableStackSize++; return( lastVar ); } public void popLocalVariables(LocalVariable var) { popLocalVariables(var, null); } /** * Restore the local variable stack to the position marked by variable var. * * @param var * */ public void popLocalVariables(LocalVariable var, Sequence resultSeq) { if( var != null ) { // clear all variables registered after var. they should be out of scope. LocalVariable outOfScope = var.after; while (outOfScope != null) { if (outOfScope != var && !outOfScope.isClosureVar()) { outOfScope.destroy(this, resultSeq); } outOfScope = outOfScope.after; } // reset the stack var.after = null; if( !contextStack.isEmpty() && ( var == contextStack.peek() ) ) { contextStack.pop(); } } lastVar = var; variableStackSize--; } /** * Returns the current size of the stack. This is used to determine where a variable has been declared. * * @return current size of the stack */ public int getCurrentStackSize() { return( variableStackSize ); } /* ----------------- Function call stack ------------------------ */ /** * Report the start of a function execution. Adds the reported function signature to the function call stack. * * @param signature */ public void functionStart( FunctionSignature signature ) { callStack.push( signature ); } /** * Report the end of the currently executed function. Pops the last function signature from the function call stack. */ public void functionEnd() { if( callStack.isEmpty() ) { LOG.warn( "Function call stack is empty, but XQueryContext.functionEnd() was called. This " + "could indicate a concurrency issue (shared XQueryContext?)" ); } else { callStack.pop(); } } /** * Check if the specified function signature is found in the current function called stack. If yes, the function might be tail recursive and needs * to be optimized. * * @param signature */ public boolean tailRecursiveCall( FunctionSignature signature ) { return( callStack.contains( signature ) ); } /* ----------------- Module imports ------------------------ */ public void mapModule( String namespace, XmldbURI uri ) { mappedModules.put( namespace, uri ); } /** * Import a module and make it available in this context. The prefix and location parameters are optional. If prefix is null, the default prefix * specified by the module is used. If location is null, the module will be read from the namespace URI. * * @param namespaceURI * @param prefix * @param location * * @throws XPathException */ public Module importModule( String namespaceURI, String prefix, String location ) throws XPathException { if(prefix != null && ("xml".equals(prefix) || "xmlns".equals(prefix))) { throw new XPathException(ErrorCodes.XQST0070, "The prefix declared for a module import must not be 'xml' or 'xmlns'."); } if(namespaceURI != null && namespaceURI.isEmpty()) { throw new XPathException(ErrorCodes.XQST0088, "The first URILiteral in a module import must be of nonzero length."); } Module module = null; if (namespaceURI != null) {module = getRootModule( namespaceURI );} if( module != null ) { LOG.debug( "Module " + namespaceURI + " already present." ); // Set locally to remember the dependency in case it was inherited. setModule( namespaceURI, module ); } else { // if location is not specified, try to resolve in expath repo if (location == null && namespaceURI != null) { module = resolveInEXPathRepository(namespaceURI, prefix); } if ( module == null ) { if( location == null && namespaceURI != null) { // check if there's a static mapping in the configuration location = getModuleLocation( namespaceURI ); if( location == null ) { location = namespaceURI; } } //Is the module's namespace mapped to a URL ? if( mappedModules.containsKey( location ) ) { location = mappedModules.get( location ).toString(); } // is it a Java module? if( location.startsWith( JAVA_URI_START ) ) { location = location.substring( JAVA_URI_START.length() ); module = loadBuiltInModule( namespaceURI, location ); } else { Source moduleSource; if( location.startsWith( XmldbURI.XMLDB_URI_PREFIX ) || ( ( location.indexOf( ':' ) == -1 ) && moduleLoadPath.startsWith( XmldbURI.XMLDB_URI_PREFIX ) ) ) { // Is the module source stored in the database? try { XmldbURI locationUri = XmldbURI.xmldbUriFor( location ); if( moduleLoadPath.startsWith( XmldbURI.XMLDB_URI_PREFIX ) ) { final XmldbURI moduleLoadPathUri = XmldbURI.xmldbUriFor( moduleLoadPath ); locationUri = moduleLoadPathUri.resolveCollectionPath( locationUri ); } DocumentImpl sourceDoc = null; try { sourceDoc = getBroker().getXMLResource( locationUri.toCollectionPathURI(), LockMode.READ_LOCK ); if(sourceDoc == null) { throw moduleLoadException("Module location hint URI '" + location + "' does not refer to anything.", location); } if(( sourceDoc.getResourceType() != DocumentImpl.BINARY_FILE ) || !"application/xquery".equals(sourceDoc.getMetadata().getMimeType())) { throw moduleLoadException("Module location hint URI '" + location + "' does not refer to an XQuery.", location); } moduleSource = new DBSource( getBroker(), (BinaryDocument)sourceDoc, true ); // we don't know if the module will get returned, oh well module = compileOrBorrowModule( prefix, namespaceURI, location, moduleSource ); } catch(final PermissionDeniedException e) { throw moduleLoadException("Permission denied to read module source from location hint URI '" + location + ".", location, e); } finally { if(sourceDoc != null) { sourceDoc.getUpdateLock().release(LockMode.READ_LOCK); } } } catch(final URISyntaxException e) { throw moduleLoadException("Invalid module location hint URI '" + location + "'.", location, e); } } else { // No. Load from file or URL try { //TODO: use URIs to ensure proper resolution of relative locations moduleSource = SourceFactory.getSource( getBroker(), moduleLoadPath, location, true ); } catch(final MalformedURLException e) { throw moduleLoadException("Invalid module location hint URI '" + location + "'.", location, e); } catch(final IOException e) { throw moduleLoadException("Source for module '" + namespaceURI + "' not found module location hint URI '" + location + "'.", location, e); } catch(final PermissionDeniedException e) { throw moduleLoadException("Permission denied to read module source from location hint URI '" + location + ".", location, e); } // we don't know if the module will get returned, oh well module = compileOrBorrowModule(prefix, namespaceURI, location, moduleSource); } } } // NOTE: expathrepo related, closes the EXPath else (if module != null) } if(module != null) { if (namespaceURI == null) { namespaceURI = module.getNamespaceURI(); } if (prefix == null) { prefix = module.getDefaultPrefix(); } declareNamespace(prefix, namespaceURI); } return module; } protected XPathException moduleLoadException(final String message, final String moduleLocation) throws XPathException { return new XPathException(ErrorCodes.XQST0059, message, new ValueSequence(new StringValue(moduleLocation))); } protected XPathException moduleLoadException(final String message, final String moduleLocation, final Exception e) throws XPathException { return new XPathException(ErrorCodes.XQST0059, message, new ValueSequence(new StringValue(moduleLocation)), e); } /** * Returns the static location mapped to an XQuery source module, if known. * * @param namespaceURI the URI of the module * * @return the location string */ @SuppressWarnings( "unchecked" ) public String getModuleLocation( String namespaceURI ) { final Map<String, String> moduleMap = (Map)getBroker().getConfiguration().getProperty( PROPERTY_STATIC_MODULE_MAP ); return( moduleMap.get( namespaceURI ) ); } /** * Returns an iterator over all module namespace URIs which are statically mapped to a known location. * * @return an iterator */ @SuppressWarnings( "unchecked" ) public Iterator<String> getMappedModuleURIs() { final Map<String, String> moduleMap = (Map)getBroker().getConfiguration().getProperty( PROPERTY_STATIC_MODULE_MAP ); return( moduleMap.keySet().iterator() ); } private ExternalModule compileOrBorrowModule( String prefix, String namespaceURI, String location, Source source ) throws XPathException { final ExternalModule module = compileModule( prefix, namespaceURI, location, source ); if(module != null) { setModule(module.getNamespaceURI(), module); declareModuleVars(module); } return module; } /** * Compile Module * * @param prefix * @param namespaceURI * @param location * @param source * * @return The compiled module. * * @throws XPathException */ public ExternalModule compileModule( String prefix, String namespaceURI, String location, Source source ) throws XPathException { LOG.debug( "Loading module from " + location ); Reader reader; try { reader = source.getReader(); if( reader == null ) { throw(moduleLoadException("failed to load module: '" + namespaceURI + "' from: " + "'" + source + "', location: '" + location + "'. Source not found. ", location)); } if (namespaceURI == null) { final QName qname = source.isModule(); if (qname == null) {return null;} namespaceURI = qname.getNamespaceURI(); } } catch( final IOException e ) { throw(moduleLoadException("IO exception while loading module '" + namespaceURI + "'" + " from '" + source + "'", location, e)); } final ExternalModuleImpl modExternal = new ExternalModuleImpl(namespaceURI, prefix); setModule(namespaceURI, modExternal); final XQueryContext modContext = new ModuleContext( this, prefix, namespaceURI, location ); modExternal.setContext( modContext ); final XQueryLexer lexer = new XQueryLexer( modContext, reader ); final XQueryParser parser = new XQueryParser( lexer ); final XQueryTreeParser astParser = new XQueryTreeParser( modContext, modExternal ); try { parser.xpath(); if( parser.foundErrors() ) { LOG.debug( parser.getErrorMessage() ); throw( new XPathException( "error found while loading module from " + location + ": " + parser.getErrorMessage() ) ); } final AST ast = parser.getAST(); final PathExpr path = new PathExpr( modContext ); astParser.xpath( ast, path ); if( astParser.foundErrors() ) { throw( new XPathException( "error found while loading module from " + location + ": " + astParser.getErrorMessage(), astParser.getLastException() ) ); } modExternal.setRootExpression(path); if(namespaceURI != null && !modExternal.getNamespaceURI().equals(namespaceURI)) { throw( new XPathException( "namespace URI declared by module (" + modExternal.getNamespaceURI() + ") does not match namespace URI in import statement, which was: " + namespaceURI ) ); } // Set source information on module context // String sourceClassName = source.getClass().getName(); // modContext.setSourceKey(source.getKey().toString()); // Extract the source type from the classname by removing the package prefix and the "Source" suffix // modContext.setSourceType( sourceClassName.substring( 17, sourceClassName.length() - 6 ) ); modExternal.setSource( source ); modContext.setSource(source); modExternal.setIsReady(true); return( modExternal ); } catch( final RecognitionException e ) { throw( new XPathException( e.getLine(), e.getColumn(), "error found while loading module from " + location + ": " + e.getMessage() ) ); } catch( final TokenStreamException e ) { throw( new XPathException( "error found while loading module from " + location + ": " + e.getMessage(), e ) ); } catch( final XPathException e ) { e.prependMessage( "Error while loading module " + location + ": " ); throw( e ); } catch( final Exception e ) { e.printStackTrace(); throw( new XPathException( "Internal error while loading module: " + location, e ) ); } finally { try { if( reader != null ) { reader.close(); } } catch( final IOException e ) { LOG.warn( "Error while closing module source: " + e.getMessage(), e ); } } } private void declareModuleVars( Module module ) { final String moduleNS = module.getNamespaceURI(); for( final Iterator<Variable> i = globalVariables.values().iterator(); i.hasNext(); ) { final Variable var = i.next(); if( moduleNS.equals( var.getQName().getNamespaceURI() ) ) { module.declareVariable( var ); i.remove(); } } } /** * Add a forward reference to an undeclared function. Forward references will be resolved later. * * @param call */ public void addForwardReference( FunctionCall call ) { forwardReferences.push( call ); } /** * Resolve all forward references to previously undeclared functions. * * @throws XPathException */ public void resolveForwardReferences() throws XPathException { while( !forwardReferences.empty() ) { final FunctionCall call = forwardReferences.pop(); final UserDefinedFunction func = call.getContext().resolveFunction( call.getQName(), call.getArgumentCount() ); if( func == null ) { throw( new XPathException( call, ErrorCodes.XPST0017, "Call to undeclared function: " + call.getQName().getStringValue() ) ); } call.resolveForwardReference( func ); } } /** * Get environment variables. The variables shall not change * during execution of query. * * @return Map of environment variables */ public Map<String, String> getEnvironmentVariables(){ if(envs==null){ envs = System.getenv(); } return envs; } /** * Gets the Effective user * i.e. the user that the query is executing as * * @return The Effective User */ public Subject getEffectiveUser() { return getBroker().getCurrentSubject(); } /** * Gets the Real User * i.e. the user that initiated execution of the query * Note this is not necessarily the same as the user that the * query is executing as * @see org.exist.xquery.XQueryContext#getEffectiveUser() * * @return The Real User */ public Subject getRealUser() { return realUser; } protected void setRealUser(final Subject realUser) { this.realUser = realUser; } /* ----------------- Save state ------------------------ */ private class SavedState { private HashMap<String, Module> modulesSaved = null; private HashMap<String, Module> allModulesSaved = null; private HashMap<String, String> staticNamespacesSaved = null; private HashMap<String, String> staticPrefixesSaved = null; @SuppressWarnings("unchecked") void save() { if (modulesSaved == null) { modulesSaved = (HashMap<String, Module>) modules.clone(); allModulesSaved = (HashMap<String, Module>) allModules.clone(); staticNamespacesSaved = (HashMap<String, String>) staticNamespaces.clone(); staticPrefixesSaved = (HashMap<String, String>) staticPrefixes.clone(); } } void restore() { if (modulesSaved != null) { modules = modulesSaved; modulesSaved = null; allModules = allModulesSaved; allModulesSaved = null; staticNamespaces = staticNamespacesSaved; staticNamespacesSaved = null; staticPrefixes = staticPrefixesSaved; staticPrefixesSaved = null; } } } /** * Before a dynamic import, make sure relevant parts of the current context a saved * to the stack. This is important for util:import-module. The context will be restored * during {@link #reset()}. */ public void saveState() { savedState.save(); } public boolean optimizationsEnabled() { return( enableOptimizer ); } /** * for static compile-time options i.e. declare option * * @param qnameString * @param contents * * @throws XPathException */ public void addOption( String qnameString, String contents ) throws XPathException { if( staticOptions == null ) { staticOptions = new ArrayList<Option>(); } addOption( staticOptions, qnameString, contents ); } /** * for dynamic run-time options i.e. util:declare-option * * @param qnameString * @param contents * * @throws XPathException */ public void addDynamicOption( String qnameString, String contents ) throws XPathException { if( dynamicOptions == null ) { dynamicOptions = new ArrayList<Option>(); } addOption( dynamicOptions, qnameString, contents ); } private void addOption( List<Option> options, String qnameString, String contents ) throws XPathException { final QName qn = QName.parse( this, qnameString, defaultFunctionNamespace ); final Option option = new Option( qn, contents ); //if the option exists, remove it so we can add the new option for( int i = 0; i < options.size(); i++ ) { if( options.get( i ).equals( option ) ) { options.remove( i ); break; } } //add option options.add( option ); // check predefined options if( Option.PROFILE_QNAME.compareTo( qn ) == 0 ) { // configure profiling profiler.configure( option ); } else if( Option.TIMEOUT_QNAME.compareTo( qn ) == 0 ) { watchdog.setTimeoutFromOption( option ); } else if( Option.OUTPUT_SIZE_QNAME.compareTo( qn ) == 0 ) { watchdog.setMaxNodesFromOption( option ); } else if( Option.OPTIMIZE_QNAME.compareTo( qn ) == 0 ) { final String[] params = option.tokenizeContents(); if( params.length > 0 ) { final String[] param = Option.parseKeyValuePair( params[0] ); if( param != null && "enable".equals( param[0] ) ) { if( "yes".equals( param[1] ) ) { enableOptimizer = true; } else { enableOptimizer = false; } } } } //TODO : not sure how these 2 options might/have to be related else if( Option.OPTIMIZE_IMPLICIT_TIMEZONE.compareTo( qn ) == 0 ) { //TODO : error check final Duration duration = TimeUtils.getInstance().newDuration( option.getContents() ); implicitTimeZone = new SimpleTimeZone( (int)duration.getTimeInMillis( new Date() ), "XQuery context" ); } else if( Option.CURRENT_DATETIME.compareTo( qn ) == 0 ) { //TODO : error check final DateTimeValue dtv = new DateTimeValue( option.getContents() ); calendar = (XMLGregorianCalendar)dtv.calendar.clone(); } } public Option getOption( QName qname ) { /* * check dynamic options that were declared at run-time * first as these have precedence and then check * static options that were declare at compile time */ if( dynamicOptions != null ) { for( final Option option : dynamicOptions ) { if( qname.compareTo( option.getQName() ) == 0 ) { return( option ); } } } if( staticOptions != null ) { for( final Option option : staticOptions ) { if( qname.compareTo( option.getQName() ) == 0 ) { return( option ); } } } return( null ); } public Pragma getPragma( String name, String contents ) throws XPathException { final QName qname = QName.parse( this, name ); if( "".equals( qname.getNamespaceURI() ) ) { throw( new XPathException( "XPST0081: pragma's ('" + name + "') namespace URI is empty" ) ); } else if( Namespaces.EXIST_NS.equals( qname.getNamespaceURI() ) ) { contents = StringValue.trimWhitespace( contents ); if( TimerPragma.TIMER_PRAGMA.equals(qname) ) { return( new TimerPragma( qname, contents ) ); } if( Optimize.OPTIMIZE_PRAGMA.equals(qname) ) { return( new Optimize( this, qname, contents, true ) ); } if( ForceIndexUse.EXCEPTION_IF_INDEX_NOT_USED_PRAGMA.equals(qname) ) { return( new ForceIndexUse( qname, contents ) ); } if( ProfilePragma.PROFILING_PRAGMA.equals(qname) ) { return( new ProfilePragma( qname, contents ) ); } if( NoIndexPragma.NO_INDEX_PRAGMA.equals(qname) ) { return( new NoIndexPragma( qname, contents ) ); } } return( null ); } /** * Store the supplied data to a temporary document fragment. * * @param doc * * @return TemporaryDoc fragment * * @throws XPathException */ public DocumentImpl storeTemporaryDoc( org.exist.dom.memtree.DocumentImpl doc ) throws XPathException { try { final DocumentImpl targetDoc = getBroker().storeTempResource( doc ); if( targetDoc == null ) { throw( new XPathException( "Internal error: failed to store temporary doc fragment" ) ); } LOG.warn( "Stored: " + targetDoc.getDocId() + ": " + targetDoc.getURI(), new Throwable() ); return( targetDoc ); } catch( final EXistException e ) { throw( new XPathException( TEMP_STORE_ERROR, e ) ); } catch( final PermissionDeniedException e ) { throw( new XPathException( TEMP_STORE_ERROR, e ) ); } catch( final LockException e ) { throw( new XPathException( TEMP_STORE_ERROR, e ) ); } } public void setAttribute( String attribute, Object value ) { attributes.put( attribute, value ); } public Object getAttribute( String attribute ) { return( attributes.get( attribute ) ); } /** * Set an XQuery Context variable. General variable storage in the xquery context * * @param name The variable name * @param XQvar The variable value, may be of any xs: type */ public void setXQueryContextVar( String name, Object XQvar ) { XQueryContextVars.put( name, XQvar ); } /** * Get an XQuery Context variable. General variable storage in the xquery context * * @param name The variable name * * @return The variable value indicated by name. */ public Object getXQueryContextVar( String name ) { return( XQueryContextVars.get( name ) ); } /** * Load the default prefix/namespace mappings table and set up internal functions. * * @param config */ @SuppressWarnings( "unchecked" ) protected void loadDefaults( Configuration config ) { this.watchdog = new XQueryWatchDog( this ); /* SymbolTable syms = broker.getSymbols(); String[] pfx = syms.defaultPrefixList(); namespaces = new HashMap(pfx.length); prefixes = new HashMap(pfx.length); String sym; for (int i = 0; i < pfx.length; i++) { sym = syms.getDefaultNamespace(pfx[i]); namespaces.put(pfx[i], sym); prefixes.put(sym, pfx[i]); } */ loadDefaultNS(); // Switch: enable optimizer Object param = config.getProperty( PROPERTY_ENABLE_QUERY_REWRITING ); enableOptimizer = ( param != null ) && "yes".equals(param.toString()); // Switch: Backward compatibility param = config.getProperty( PROPERTY_XQUERY_BACKWARD_COMPATIBLE ); backwardsCompatible = ( param == null ) || "yes".equals(param.toString()); // Switch: raiseErrorOnFailedRetrieval final Boolean option = ( (Boolean)config.getProperty( PROPERTY_XQUERY_RAISE_ERROR_ON_FAILED_RETRIEVAL ) ); raiseErrorOnFailedRetrieval = ( option != null ) && option.booleanValue(); // Get map of built-in modules final Map<String, Class<Module>> builtInModules = (Map)config.getProperty( PROPERTY_BUILT_IN_MODULES ); if( builtInModules != null ) { // Iterate on all map entries for( final Map.Entry<String, Class<Module>> entry : builtInModules.entrySet() ) { // Get URI and class final String namespaceURI = entry.getKey(); final Class<Module> moduleClass = entry.getValue(); // first check if the module has already been loaded in the parent context final Module module = getModule( namespaceURI ); if( module == null ) { // Module does not exist yet, instantiate instantiateModule( namespaceURI, moduleClass, (Map<String, Map<String, List<? extends Object>>>)config.getProperty(PROPERTY_MODULE_PARAMETERS)); } else if( ( getPrefixForURI( module.getNamespaceURI() ) == null ) && ( module.getDefaultPrefix().length() > 0 ) ) { // make sure the namespaces of default modules are known, // even if they were imported in a parent context try { declareNamespace( module.getDefaultPrefix(), module.getNamespaceURI() ); } catch( final XPathException e ) { LOG.warn( "Internal error while loading default modules: " + e.getMessage(), e ); } } } } } /** * Load default namespaces, e.g. xml, xsi, xdt, fn, local, exist and dbgp. */ protected void loadDefaultNS(){ try { // default namespaces staticNamespaces.put( "xml", Namespaces.XML_NS ); staticPrefixes.put( Namespaces.XML_NS, "xml" ); declareNamespace( "xs", Namespaces.SCHEMA_NS ); declareNamespace( "xsi", Namespaces.SCHEMA_INSTANCE_NS ); //required for backward compatibility declareNamespace( "xdt", Namespaces.XPATH_DATATYPES_NS ); declareNamespace( "fn", Namespaces.XPATH_FUNCTIONS_NS ); declareNamespace( "local", Namespaces.XQUERY_LOCAL_NS ); declareNamespace( Namespaces.W3C_XQUERY_XPATH_ERROR_PREFIX, Namespaces.W3C_XQUERY_XPATH_ERROR_NS ); //*not* as standard NS declareNamespace( Namespaces.EXIST_NS_PREFIX, Namespaces.EXIST_NS ); declareNamespace( Namespaces.EXIST_JAVA_BINDING_NS_PREFIX, Namespaces.EXIST_JAVA_BINDING_NS ); declareNamespace( Namespaces.EXIST_XQUERY_XPATH_ERROR_PREFIX, Namespaces.EXIST_XQUERY_XPATH_ERROR_NS ); //TODO : include "err" namespace ? declareNamespace( "dbgp", Debuggee.NAMESPACE_URI ); } catch( final XPathException e ) { //ignored because it should never happen LOG.debug(e); } } public void registerUpdateListener( UpdateListener listener ) { if( updateListener == null ) { updateListener = new ContextUpdateListener(); final DBBroker broker = getBroker(); broker.getBrokerPool().getNotificationService().subscribe( updateListener ); } updateListener.addListener( listener ); } protected void clearUpdateListeners() { if( updateListener != null ) { final DBBroker broker = getBroker(); broker.getBrokerPool().getNotificationService().unsubscribe( updateListener ); } updateListener = null; } /** * Check if the XQuery contains options that define serialization settings. If yes, * copy the corresponding settings to the current set of output properties. * * @param properties the properties object to which serialization parameters will be added. * * @throws XPathException if an error occurs while parsing the option */ public void checkOptions(Properties properties) throws XPathException { checkLegacyOptions(properties); if(dynamicOptions != null) { for(final Option option : dynamicOptions) { if (Namespaces.XSLT_XQUERY_SERIALIZATION_NS.equals(option.getQName().getNamespaceURI())) { properties.put(option.getQName().getLocalPart(), option.getContents()); } } } if( staticOptions != null ) { for(final Option option : staticOptions) { if (Namespaces.XSLT_XQUERY_SERIALIZATION_NS.equals(option.getQName().getNamespaceURI())) { if (!properties.containsKey(option.getQName().getLocalPart())) {properties.put(option.getQName().getLocalPart(), option.getContents());} } } } } /** * Legacy method to check serialization properties set via option exist:serialize. * * @param properties * @throws XPathException */ private void checkLegacyOptions( Properties properties ) throws XPathException { final Option pragma = getOption( Option.SERIALIZE_QNAME ); if( pragma == null ) { return; } final String[] contents = pragma.tokenizeContents(); for( int i = 0; i < contents.length; i++ ) { final String[] pair = Option.parseKeyValuePair( contents[i] ); if( pair == null ) { throw( new XPathException( "Unknown parameter found in " + pragma.getQName().getStringValue() + ": '" + contents[i] + "'" ) ); } LOG.debug( "Setting serialization property from pragma: " + pair[0] + " = " + pair[1] ); properties.setProperty( pair[0], pair[1] ); } } public void setDebuggeeJoint( DebuggeeJoint joint ) { //XXX: if (debuggeeJoint != null) ??? debuggeeJoint = joint; } public DebuggeeJoint getDebuggeeJoint() { return( debuggeeJoint ); } public boolean isDebugMode() { return( ( debuggeeJoint != null ) && isVarDeclared( Debuggee.SESSION ) ); } public boolean requireDebugMode() { return isVarDeclared( Debuggee.SESSION ); } private List<BinaryValue> binaryValueInstances; @Override public void registerBinaryValueInstance(final BinaryValue binaryValue) { if(binaryValueInstances == null) { binaryValueInstances = new ArrayList<>(); } if(cleanupTasks.isEmpty() || !cleanupTasks.stream().filter(ct -> ct instanceof BinaryValueCleanupTask).findFirst().isPresent()) { cleanupTasks.add(new BinaryValueCleanupTask()); } binaryValueInstances.add(binaryValue); } /** * Cleanup Task which is responsible for relasing the streams * of any {@link BinaryValue} which have been used during * query execution */ private static class BinaryValueCleanupTask implements CleanupTask { @Override public void cleanup(final XQueryContext context) { if (context.binaryValueInstances != null) { for (final BinaryValue bv : context.binaryValueInstances) { try { bv.close(); } catch (final IOException ioe) { LOG.error("Unable to close binary value: " + ioe.getMessage(), ioe); } } context.binaryValueInstances.clear(); } } } @Override public String getCacheClass() { return (String) getBroker().getConfiguration().getProperty(Configuration.BINARY_CACHE_CLASS_PROPERTY); } public void destroyBinaryValue(BinaryValue value) { if (binaryValueInstances != null) { for (int i = binaryValueInstances.size() - 1; i > -1; i--) { final BinaryValue bv = binaryValueInstances.get(i); if (bv == value) { binaryValueInstances.remove(i); return; } } } } public void setXQueryVersion(int version) { xqueryVersion=version; } public int getXQueryVersion(){ return xqueryVersion; } @Override public Source getSource() { return source; } @Override public void setSource(final Source source) { this.source = source; } // ==================================================================================== private static class ContextUpdateListener implements UpdateListener { private List<UpdateListener> listeners = new ArrayList<UpdateListener>(); public void addListener( UpdateListener listener ) { synchronized( listeners ) { // TODO field must be final? listeners.add( listener ); } } public void documentUpdated( DocumentImpl document, int event ) { synchronized( listeners ) { // TODO field must be final? for( final UpdateListener listener : listeners ) { if( listener != null ) { listener.documentUpdated( document, event ); } } } } public void unsubscribe() { synchronized( listeners ) { // TODO field must be final? for( final UpdateListener listener : listeners ) { if( listener != null ) { listener.unsubscribe(); } } listeners.clear(); } } @Override public void nodeMoved(NodeId oldNodeId, NodeHandle newNode) { for(final UpdateListener listener : listeners) { listener.nodeMoved(oldNodeId, newNode); } } public void debug() { LOG.debug(String.format("XQueryContext: %s document update listeners", listeners.size())); for (int i = 0; i < listeners.size(); i++) { ((UpdateListener) listeners.get(i)).debug(); } } } private final List<CleanupTask> cleanupTasks = new ArrayList<>(); public void registerCleanupTask(final CleanupTask cleanupTask) { cleanupTasks.add(cleanupTask); } public interface CleanupTask { void cleanup(final XQueryContext context); } @Override public void runCleanupTasks() { for(final CleanupTask cleanupTask : cleanupTasks) { try { cleanupTask.cleanup(this); } catch(final Throwable t) { LOG.error("Cleaning up XQueryContext: Ignoring: " + t.getMessage(), t); } } // now it is safe to clear the cleanup tasks list as we know they have run // do not move this anywhere else cleanupTasks.clear(); } }