/*
Copyright (c) 2012, Adam Retter
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Adam Retter Consulting nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL Adam Retter BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.exist.extensions.exquery.restxq.impl;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.EXistException;
import org.exist.dom.QName;
import org.exist.extensions.exquery.restxq.RestXqServiceCompiledXQueryCache;
import org.exist.extensions.exquery.restxq.impl.adapters.SequenceAdapter;
import org.exist.extensions.exquery.restxq.impl.adapters.TypeAdapter;
import org.exist.dom.memtree.DocumentImpl;
import org.exist.security.EffectiveSubject;
import org.exist.security.Permission;
import org.exist.security.PermissionDeniedException;
import org.exist.source.DBSource;
import org.exist.source.Source;
import org.exist.storage.BrokerPool;
import org.exist.storage.DBBroker;
import org.exist.storage.ProcessMonitor;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.AbstractExpression;
import org.exist.xquery.AnalyzeContextInfo;
import org.exist.xquery.CompiledXQuery;
import org.exist.xquery.Expression;
import org.exist.xquery.FunctionCall;
import org.exist.xquery.UserDefinedFunction;
import org.exist.xquery.VariableDeclaration;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQueryContext;
import org.exist.xquery.util.ExpressionDumper;
import org.exist.xquery.value.AnyURIValue;
import org.exist.xquery.value.AtomicValue;
import org.exist.xquery.value.Base64BinaryValueType;
import org.exist.xquery.value.BinaryValueFromInputStream;
import org.exist.xquery.value.BooleanValue;
import org.exist.xquery.value.DateTimeValue;
import org.exist.xquery.value.DateValue;
import org.exist.xquery.value.DecimalValue;
import org.exist.xquery.value.DoubleValue;
import org.exist.xquery.value.FloatValue;
import org.exist.xquery.value.FunctionParameterSequenceType;
import org.exist.xquery.value.FunctionReference;
import org.exist.xquery.value.IntegerValue;
import org.exist.xquery.value.Item;
import org.exist.xquery.value.QNameValue;
import org.exist.xquery.value.SequenceType;
import org.exist.xquery.value.StringValue;
import org.exist.xquery.value.TimeValue;
import org.exist.xquery.value.Type;
import org.exist.xquery.value.ValueSequence;
import org.exquery.http.HttpRequest;
import org.exquery.restxq.Namespace;
import org.exquery.restxq.ResourceFunction;
import org.exquery.restxq.ResourceFunctionExecuter;
import org.exquery.restxq.RestXqServiceException;
import org.exquery.xquery.Sequence;
import org.exquery.xquery.TypedArgumentValue;
import org.exquery.xquery.TypedValue;
import org.exquery.xquery3.FunctionSignature;
/**
*
* @author Adam Retter <adam.retter@googlemail.com>
*/
public class ResourceFunctionExecutorImpl implements ResourceFunctionExecuter {
private final static Logger LOG = LogManager.getLogger(ResourceFunctionExecutorImpl.class);
public final static QName XQ_VAR_BASE_URI = new QName("base-uri", Namespace.ANNOTATION_NS);
public final static QName XQ_VAR_URI = new QName("uri", Namespace.ANNOTATION_NS);
//TODO generalise with RequestModule
private final static String EXQ_REQUEST_ATTR = "exquery-request";
private final BrokerPool brokerPool;
private final String uri;
private final String baseUri;
public ResourceFunctionExecutorImpl(final BrokerPool brokerPool, final String baseUri, final String uri) {
this.brokerPool = brokerPool;
this.baseUri = baseUri;
this.uri = uri;
}
private BrokerPool getBrokerPool() {
return brokerPool;
}
@Override
public Sequence execute(final ResourceFunction resourceFunction, final Iterable<TypedArgumentValue> arguments, final HttpRequest request) throws RestXqServiceException {
final RestXqServiceCompiledXQueryCache cache = RestXqServiceCompiledXQueryCacheImpl.getInstance();
CompiledXQuery xquery = null;
ProcessMonitor processMonitor = null;
try(final DBBroker broker = getBrokerPool().getBroker()) {
//ensure we can execute the function before going any further
checkSecurity(broker, resourceFunction.getXQueryLocation());
//get a compiled query service from the cache
xquery = cache.getCompiledQuery(broker, resourceFunction.getXQueryLocation());
//find the function that we will execute
final UserDefinedFunction fn = findFunction(xquery, resourceFunction.getFunctionSignature());
final XQueryContext xqueryContext = xquery.getContext();
//set the request object - can later be used by the EXQuery Request Module
xqueryContext.setAttribute(EXQ_REQUEST_ATTR, request);
//TODO this is a workaround?
declareVariables(xqueryContext);
//START workaround: evaluate global variables in modules, as they are reset by XQueryContext.reset()
final Expression rootExpr = xqueryContext.getRootExpression();
for(int i = 0; i < rootExpr.getSubExpressionCount(); i++) {
final Expression subExpr = rootExpr.getSubExpression(i);
if(subExpr instanceof VariableDeclaration) {
subExpr.eval(null);
}
}
//END workaround
//setup monitoring
processMonitor = broker.getBrokerPool().getProcessMonitor();
xqueryContext.getProfiler().traceQueryStart();
processMonitor.queryStarted(xqueryContext.getWatchDog());
//create a function call
final FunctionReference fnRef = new FunctionReference(new FunctionCall(xqueryContext, fn));
//convert the arguments
final org.exist.xquery.value.Sequence[] fnArgs = convertToExistFunctionArguments(xqueryContext, fn, arguments);
//execute the function call
fnRef.analyze(new AnalyzeContextInfo());
//if setUid/setGid, determine the effectiveSubject to use for execution
final Optional<EffectiveSubject> effectiveSubject = getEffectiveSubject(xquery);
try {
effectiveSubject.ifPresent(broker::pushSubject); //switch to effective user if setUid/setGid
final org.exist.xquery.value.Sequence result = fnRef.evalFunction(null, null, fnArgs);
return new SequenceAdapter(result);
} finally {
//switch back from effective user if setUid/setGid
if(effectiveSubject.isPresent()) {
broker.popSubject();
}
}
} catch(final URISyntaxException | EXistException | XPathException | PermissionDeniedException use) {
throw new RestXqServiceException(use.getMessage(), use);
} finally {
//clear down monitoring
if(processMonitor != null) {
xquery.getContext().getProfiler().traceQueryEnd(xquery.getContext());
processMonitor.queryCompleted(xquery.getContext().getWatchDog());
}
if(xquery != null) {
//return the compiled query to the pool
cache.returnCompiledQuery(resourceFunction.getXQueryLocation(), xquery);
}
}
}
/**
* If the compiled xquery is setUid and/or setGid
* we return the EffectiveSubject that should be used
* for execution
*
* @param xquery The XQuery to determine the effective subject for
* @return Maybe an effective subject or empty if there is no setUid or setGid bits
*/
private Optional<EffectiveSubject> getEffectiveSubject(final CompiledXQuery xquery) {
final Optional<EffectiveSubject> effectiveSubject;
final Source src = xquery.getContext().getSource();
if(src instanceof DBSource) {
final DBSource dbSrc = (DBSource)src;
final Permission perm = dbSrc.getPermissions();
if(perm.isSetUid()) {
if(perm.isSetGid()) {
//setUid and SetGid
effectiveSubject = Optional.of(new EffectiveSubject(perm.getOwner(), perm.getGroup()));
} else {
//just setUid
effectiveSubject = Optional.of(new EffectiveSubject(perm.getOwner()));
}
} else if(perm.isSetGid()) {
//just setGid, so we use the current user as the effective user
effectiveSubject = Optional.of(new EffectiveSubject(xquery.getContext().getBroker().getCurrentSubject(), perm.getGroup()));
} else {
effectiveSubject = Optional.empty();
}
} else {
effectiveSubject = Optional.empty();
}
return effectiveSubject;
}
private void declareVariables(final XQueryContext xqueryContext) throws XPathException {
xqueryContext.declareVariable(XQ_VAR_BASE_URI, baseUri);
xqueryContext.declareVariable(XQ_VAR_URI, uri);
}
/**
* Ensures that the xqueryLocation has READ and EXECUTE access
*
* @param broker The current broker
* @param xqueryLocation The xquery to check permissions for
*
* @throws URISyntaxException if the xqueryLocation cannot be parsed
* @throws PermissionDeniedException if there is not READ and EXECUTE access on the xqueryLocation for the current user
*/
private void checkSecurity(final DBBroker broker, final URI xqueryLocation) throws URISyntaxException, PermissionDeniedException {
broker.getResource(XmldbURI.xmldbUriFor(xqueryLocation), Permission.READ | Permission.EXECUTE);
}
/**
* Lookup a Function in an XQuery given a Function Signature
*
* @param xquery The XQuery to interrogate
* @param functionSignature The Function Signature to use to match a Function
*
* @return The Function from the XQuery matching the Function Signature
*/
private UserDefinedFunction findFunction(final CompiledXQuery xquery, final FunctionSignature functionSignature) throws XPathException {
final QName fnName = QName.fromJavaQName(functionSignature.getName());
final int arity = functionSignature.getArgumentCount();
return xquery.getContext().resolveFunction(fnName, arity);
}
/**
* Creates converts function arguments from EXQuery to eXist-db types
*
* @param xqueryContext The XQuery Context of the XQuery containing the Function Call
* @param fn The Function in the XQuery to create a Function Call for
* @param arguments The arguments to be passed to the Function when its invoked
*
* @return The arguments ready to pass to the Function Call when it is invoked
*/
private org.exist.xquery.value.Sequence[] convertToExistFunctionArguments(final XQueryContext xqueryContext, final UserDefinedFunction fn, final Iterable<TypedArgumentValue> arguments) throws XPathException, RestXqServiceException {
final List<org.exist.xquery.value.Sequence> fnArgs = new ArrayList<>();
for(final SequenceType argumentType : fn.getSignature().getArgumentTypes()) {
final FunctionParameterSequenceType fnParameter = (FunctionParameterSequenceType)argumentType;
org.exist.xquery.value.Sequence fnArg = null;
boolean found = false;
for(final TypedArgumentValue argument : arguments) {
final String argumentName = argument.getArgumentName();
if(argumentName != null && argumentName.equals(fnParameter.getAttributeName())) {
fnArg = convertToExistSequence(xqueryContext, argument, fnParameter.getPrimaryType());
found = true;
break;
}
}
if(!found) {
//value is not always provided, e.g. by PathAnnotation, so use empty sequence
//TODO do we need to check the cardinality of the receiving arg to make sure it permits ZERO?
//argumentType.getCardinality();
//create the empty sequence
fnArg = org.exist.xquery.value.Sequence.EMPTY_SEQUENCE;
}
fnArgs.add(fnArg);
}
return fnArgs.toArray(new org.exist.xquery.value.Sequence[fnArgs.size()]);
}
//TODO this needs to be abstracted into EXQuery library / or not, see the TODOs below
private <X> TypedValue<X> convertToType(final XQueryContext xqueryContext, final String argumentName, final TypedValue typedValue, final org.exquery.xquery.Type destinationType, final Class<X> underlyingDestinationClass) throws RestXqServiceException {
//TODO consider changing Types that can be used as <T> to TypedValue to a set of interfaces for XDM types that
//require absolute minimal implementation, and we provide some default or abstract implementations if possible
final Item convertedValue;
try {
final int existDestinationType = TypeAdapter.toExistType(destinationType);
final Item value;
//TODO This type system is a complete mess:
//EXQuery XDM should not have any concrete types, just interfaces
//some of the abstract code in EXQuery needs to be able to instantiate types.
//Consider a factory or java.util.ServiceLoader pattern
if(typedValue instanceof org.exquery.xdm.type.StringTypedValue) {
value = new StringValue(((org.exquery.xdm.type.StringTypedValue)typedValue).getValue());
} else if(typedValue instanceof org.exquery.xdm.type.Base64BinaryTypedValue) {
value = BinaryValueFromInputStream.getInstance(xqueryContext, new Base64BinaryValueType(), ((org.exquery.xdm.type.Base64BinaryTypedValue)typedValue).getValue());
} else {
value = (Item)typedValue.getValue();
}
if(existDestinationType == value.getType()) {
convertedValue = value;
} else if(value instanceof AtomicValue) {
convertedValue = value.convertTo(existDestinationType);
} else {
LOG.warn("Could not convert parameter '" + argumentName + "' from '" + typedValue.getType().name() + "' to '" + destinationType.name() + "'.");
convertedValue = value;
}
} catch(final XPathException xpe) {
//TODO define an ErrorCode
throw new RestXqServiceException("TODO need to implement error code for problem with parameter conversion!: " + xpe.getMessage(), xpe);
}
return new TypedValue<X>() {
@Override
public org.exquery.xquery.Type getType() {
//return destinationType;
return TypeAdapter.toExQueryType(convertedValue.getType());
}
@Override
public X getValue() {
return (X)convertedValue;
}
};
}
private org.exist.xquery.value.Sequence convertToExistSequence(final XQueryContext xqueryContext, final TypedArgumentValue argument, final int fnParameterType) throws RestXqServiceException, XPathException {
final org.exist.xquery.value.Sequence sequence = new ValueSequence();
for(final TypedValue value : (Sequence<Object>)argument.getTypedValue()) {
final org.exquery.xquery.Type destinationType = TypeAdapter.toExQueryType(fnParameterType);
final Class destinationClass;
switch(fnParameterType) {
case Type.ITEM:
destinationClass = Item.class;
break;
case Type.DOCUMENT:
destinationClass = DocumentImpl.class; //TODO test this
break;
case Type.STRING:
destinationClass = StringValue.class;
break;
case Type.INT:
case Type.INTEGER:
destinationClass = IntegerValue.class;
break;
case Type.FLOAT:
destinationClass = FloatValue.class;
break;
case Type.DOUBLE:
destinationClass = DoubleValue.class;
break;
case Type.DECIMAL:
destinationClass = DecimalValue.class;
break;
case Type.DATE:
destinationClass = DateValue.class;
break;
case Type.DATE_TIME:
destinationClass = DateTimeValue.class;
break;
case Type.TIME:
destinationClass = TimeValue.class;
break;
case Type.QNAME:
destinationClass = QNameValue.class;
break;
case Type.ANY_URI:
destinationClass = AnyURIValue.class;
break;
case Type.BOOLEAN:
destinationClass = BooleanValue.class;
break;
default:
destinationClass = Item.class;
}
final TypedValue<? extends Item> val = convertToType(xqueryContext, argument.getArgumentName(), value, destinationType, destinationClass);
sequence.add(val.getValue());
}
return sequence;
}
public class DocumentImplExpressionAdapter extends AbstractExpression {
private final DocumentImpl doc;
public DocumentImplExpressionAdapter(final XQueryContext context, final DocumentImpl doc) {
super(context);
this.doc = doc;
}
@Override
public org.exist.xquery.value.Sequence eval(final org.exist.xquery.value.Sequence contextSequence, final Item contextItem) throws XPathException {
return doc;
}
@Override
public int returnsType() {
return Type.DOCUMENT;
}
@Override
public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException {
}
@Override
public void dump(final ExpressionDumper dumper) {
}
}
}