/* * eXist Open Source Native XML Database * Copyright (C) 2001-2017 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.functions.fn; import com.evolvedbinary.j8fu.function.ConsumerE; import org.exist.dom.QName; import org.exist.xquery.*; import org.exist.xquery.functions.map.AbstractMapType; import org.exist.xquery.functions.map.MapType; import org.exist.xquery.parser.XQueryAST; import org.exist.xquery.value.*; import java.util.*; import java.util.function.Consumer; /** * Implements fn:load-xquery-module. Creates a temporary context for the imported module, so the * current XQuery execution context is not polluted. * * eXist does currently not support setting external variables in a library module or defining a context * sequence for variables. The "context-item" and "variables" options are thus ignored. * * @author Wolfgang */ public class LoadXQueryModule extends BasicFunction { public final static FunctionSignature LOAD_XQUERY_MODULE_1 = new FunctionSignature( new QName("load-xquery-module", Function.BUILTIN_FUNCTION_NS, FnModule.PREFIX), "Provides access to the public functions and global variables of a dynamically-loaded XQuery library module.", new SequenceType[] { new FunctionParameterSequenceType("module-uri", Type.STRING, Cardinality.EXACTLY_ONE, "The target namespace of the module") }, new FunctionReturnSequenceType( Type.MAP, Cardinality.EXACTLY_ONE, "a map with two entries: 1) 'variables': a map with one entry for each public global variable declared in " + "the library module. The key of the entry is the name of the variable, as an xs:QName value; the " + "associated value is the value of the variable; 2) 'functions': a map which contains one " + "entry for each public function declared in the library module, except that when two functions have " + "the same name (but different arity), they share the same entry. The key of the entry is the name of the " + "function(s), as an xs:QName value; the associated value is a map A. This map (A) contains one entry for each " + "function with the given name; its key is the arity of the function, as an xs:integer value, and its associated " + "value is the function itself, as a function item. The function can be invoked using the rules for dynamic " + "function invocation.") ); public final static FunctionSignature LOAD_XQUERY_MODULE_2 = new FunctionSignature( new QName("load-xquery-module", Function.BUILTIN_FUNCTION_NS, FnModule.PREFIX), "Provides access to the public functions and global variables of a dynamically-loaded XQuery library module.", new SequenceType[] { new FunctionParameterSequenceType("module-uri", Type.STRING, Cardinality.EXACTLY_ONE, "The target namespace of the module"), new FunctionParameterSequenceType("options", Type.MAP, Cardinality.EXACTLY_ONE, "Options for loading the module") }, new FunctionReturnSequenceType( Type.MAP, Cardinality.EXACTLY_ONE, "a map with two entries: 1) 'variables': a map with one entry for each public global variable declared in " + "the library module. The key of the entry is the name of the variable, as an xs:QName value; the " + "associated value is the value of the variable; 2) 'functions': a map which contains one " + "entry for each public function declared in the library module, except that when two functions have " + "the same name (but different arity), they share the same entry. The key of the entry is the name of the " + "function(s), as an xs:QName value; the associated value is a map A. This map (A) contains one entry for each " + "function with the given name; its key is the arity of the function, as an xs:integer value, and its associated " + "value is the function itself, as a function item. The function can be invoked using the rules for dynamic " + "function invocation.") ); public final static StringValue OPTIONS_LOCATION_HINTS = new StringValue("location-hints"); public final static StringValue OPTIONS_XQUERY_VERSION = new StringValue("xquery-version"); public final static StringValue OPTIONS_VARIABLES = new StringValue("variables"); public final static StringValue OPTIONS_CONTEXT_ITEM = new StringValue("context-item"); public final static StringValue OPTIONS_VENDOR = new StringValue("vendor-options"); public final static StringValue RESULT_FUNCTIONS = new StringValue("functions"); public final static StringValue RESULT_VARIABLES = new StringValue("variables"); public LoadXQueryModule(XQueryContext context, FunctionSignature signature) { super(context, signature); } @Override public Sequence eval(Sequence[] args, Sequence contextSequence) throws XPathException { final String targetNamespace = args[0].getStringValue(); if (targetNamespace.length() == 0) { throw new XPathException(this, ErrorCodes.FOQM0001, "Target namespace must be a string with length > 0"); } Sequence locationHints = Sequence.EMPTY_SEQUENCE; String xqVersion = getXQueryVersion(context.getXQueryVersion()); AbstractMapType externalVars = new MapType(context); Sequence contextItem = Sequence.EMPTY_SEQUENCE; // evaluate options if (getArgumentCount() == 2) { final AbstractMapType map = (AbstractMapType) args[1].itemAt(0); locationHints = map.get(OPTIONS_LOCATION_HINTS); final Sequence versions = map.get(OPTIONS_XQUERY_VERSION); if (!versions.isEmpty()) { xqVersion = versions.itemAt(0).getStringValue(); } final Sequence vars = map.get(OPTIONS_VARIABLES); if (!vars.isEmpty()) { if (vars.hasOne() && vars.itemAt(0).getType() == Type.MAP) { externalVars = (AbstractMapType) vars.itemAt(0); } else { throw new XPathException(this, ErrorCodes.XPTY0004, "Option 'variables' must be a map"); } } contextItem = map.get(OPTIONS_CONTEXT_ITEM); if (contextItem.getItemCount() > 1) { throw new XPathException(this, ErrorCodes.XPTY0004, "Option 'context-item' must contain zero or one " + "items"); } } // create temporary context so main context is not polluted final XQueryContext tempContext = new XQueryContext(context.getBroker().getBrokerPool()); tempContext.setModuleLoadPath(context.getModuleLoadPath()); setExternalVars(externalVars, tempContext::declareGlobalVariable); Module loadedModule = null; try { if (locationHints.isEmpty()) { // no location hint given, resolve from statically known modules loadedModule = tempContext.importModule(targetNamespace, null, null); } else { // try to resolve the module from one of the location hints for (final SequenceIterator i = locationHints.iterate(); i.hasNext(); ) { final String location = i.nextItem().getStringValue(); final Module importedModule = tempContext.importModule(null, null, location); if (importedModule != null && importedModule.getNamespaceURI().equals(targetNamespace)) { loadedModule = importedModule; break; } } } } catch (XPathException e) { if (e.getErrorCode() == ErrorCodes.XQST0059) { // importModule may throw exception if no location is given and module cannot be resolved throw new XPathException(this, ErrorCodes.FOQM0002, "Module with URI " + targetNamespace + " not found"); } throw new XPathException(this, ErrorCodes.FOQM0003, "Error found when importing module: " + e.getMessage()); } // not found, raise error if (loadedModule == null) { throw new XPathException(this, ErrorCodes.FOQM0002, "Module with URI " + targetNamespace + " not found"); } if (!xqVersion.equals(getXQueryVersion(tempContext.getXQueryVersion()))) { throw new XPathException(ErrorCodes.FOQM0003, "Imported module has wrong XQuery version: " + getXQueryVersion(tempContext.getXQueryVersion())); } final Module module = loadedModule; module.setContextItem(contextItem); setExternalVars(externalVars, module::declareVariable); final MapType result = new MapType(context); final ValueSequence functionSeq = new ValueSequence(); addFunctionRefsFromModule(this, tempContext, functionSeq, module); final MapType functions = new MapType(context); for (final SequenceIterator i = functionSeq.iterate(); i.hasNext(); ) { final FunctionReference ref = (FunctionReference) i.nextItem(); final FunctionSignature signature = ref.getSignature(); final QNameValue qn = new QNameValue(context, signature.getName()); final MapType entry; if (functions.contains(qn)) { entry = (MapType) functions.get(qn); } else { entry = new MapType(context); functions.add(qn, entry); } entry.add(new IntegerValue(signature.getArgumentCount()), ref); } result.add(RESULT_FUNCTIONS, functions); final MapType variables = new MapType(context); for (final Iterator<QName> i = module.getGlobalVariables(); i.hasNext(); ) { final QName name = i.next(); try { final Variable var = module.resolveVariable(name); variables.add(new QNameValue(context, name), var.getValue()); } catch (XPathException e) { throw new XPathException(this, ErrorCodes.FOQM0005, "Incorrect type for external variable " + name); } } result.add(RESULT_VARIABLES, variables); return result; } private void setExternalVars(final AbstractMapType externalVars, final ConsumerE<Variable, XPathException> setter) throws XPathException { for (final Map.Entry<AtomicValue, Sequence> entry: externalVars) { if (!Type.subTypeOf(entry.getKey().getType(), Type.QNAME)) { throw new XPathException(this, ErrorCodes.XPTY0004, "name of external variable must be a qname: " + entry.getKey()); } final Variable var = new VariableImpl(((QNameValue) entry.getKey()).getQName()); var.setValue(entry.getValue()); setter.accept(var); } } public static void addFunctionRefsFromModule(Expression parent, XQueryContext tempContext, ValueSequence resultSeq, Module module) throws XPathException { final FunctionSignature signatures[] = module.listFunctions(); for (final FunctionSignature signature : signatures) { if (!signature.isPrivate()) { if (module.isInternalModule()) { int arity; if (signature.isOverloaded()) { arity = signature.getArgumentTypes().length; } else { arity = signature.getArgumentCount(); } final FunctionDef def = ((InternalModule)module).getFunctionDef(signature.getName(), arity); final XQueryAST ast = new XQueryAST(); ast.setLine(parent.getLine()); ast.setColumn(parent.getColumn()); final List<Expression> args = new ArrayList<Expression>(arity); for (int i = 0; i < arity; i++) { args.add(new Function.Placeholder(tempContext)); } final Function fn = Function.createFunction(tempContext, ast, def); fn.setArguments(args); final InternalFunctionCall call = new InternalFunctionCall(fn); final FunctionCall ref = FunctionFactory.wrap(tempContext, call); resultSeq.addAll(new FunctionReference(ref)); } else { final UserDefinedFunction func = ((ExternalModule) module).getFunction(signature.getName(), signature.getArgumentCount(), tempContext); // could be null if private function if (func != null) { // create function reference final FunctionCall funcCall = new FunctionCall(tempContext, func); funcCall.setLocation(parent.getLine(), parent.getColumn()); resultSeq.add(new FunctionReference(funcCall)); } } } } } private static String getXQueryVersion(int version) { return String.valueOf(version / 10) + '.' + String.valueOf(version % 10); } }