/** * The contents of this file are subject to the Open Software License * Version 3.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://www.opensource.org/licenses/osl-3.0.txt * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See * the License for the specific language governing rights and limitations * under the License. */ package org.mulgara.query.filter.value; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.WeakHashMap; import org.apache.log4j.Logger; import org.jrdf.vocabulary.RDF; import org.mulgara.parser.MulgaraParserException; import org.mulgara.query.FunctionResolverRegistry; import org.mulgara.query.QueryException; import org.mulgara.query.filter.RDFTerm; import org.mulgara.query.rdf.XSD; import javax.xml.namespace.QName; import javax.xml.xpath.XPathFunction; import javax.xml.xpath.XPathFunctionException; import javax.xml.xpath.XPathFunctionResolver; /** * Executes a function that isn't defined in these packages. If the function is external * then the answer is presumed to be scalar. URIs are automatically treated as IRIs, and anything else * is treated as a literal. * * @created Apr 22, 2008 * @author Paula Gearon * @copyright © 2008 <a href="http://www.topazproject.org/">The Topaz Project</a> * @licence <a href="{@docRoot}/../../LICENCE.txt">Open Software License v3.0</a> */ public class ExternalFn extends AbstractAccessorFn implements NumericExpression { /** Generated Serialization ID for RMI */ private static final long serialVersionUID = 5748124115023875223L; /** The logger */ private final static Logger logger = Logger.getLogger(ExternalFn.class.getName()); /** A URI containing the namespace for XSD */ private final static URI XSD_URI = URI.create(XSD.NAMESPACE); /** The scheme for XSD */ private final static String XSD_SCHEME = XSD_URI.getScheme(); /** The scheme-specific part for XSD */ private final static String XSD_PART = XSD_URI.getSchemeSpecificPart(); /** The function to be run. This will be mapped to a functor or reflection code. */ private URI fnUri; /** This is a constructor function. */ private boolean isConstructor = false; /** The external function to execute. */ XPathFunction extFn = null; /** Caches functions by their label of iri/#args. */ private static Map<String,XPathFunction> fnCache = new WeakHashMap<String,XPathFunction>(); /** Indicates that an error will always be returned from this method. */ private boolean unrecoverableError = false; /** * Create a new function instance. * @param fn The function to run. * @param operands The arguments of the function. */ public ExternalFn(IRI fn, RDFTerm... operands) throws MulgaraParserException { super(operands); fnUri = fn.getValue(); if (isCast(fnUri)) { if (operands.length != 1) throw new MulgaraParserException("Cast operation can only take a single parameter"); isConstructor = true; } else { extFn = findFunction(fn, operands.length); if (extFn == null) logger.debug("Unknown function: " + fn); } } /** * Tests if the URI is used for casting a literal. Anything that is in the XSD namespace, * or the rdf:XMLLiteral type, is considered to be a construction operation. * @param u The URI to test. * @return <code>true</code> If the URI is a known type for casting. */ private boolean isCast(URI u) { if (XSD_SCHEME.equals(fnUri.getScheme()) && XSD_PART.equals(fnUri.getSchemeSpecificPart())) return true; if (XSD.DOM.equals(fnUri.getScheme())) return true; if (RDF.XML_LITERAL.equals(u) || RDF.XML_LITERAL_ABBR.equals(u)) return true; return false; } // The ValueLiteral interface /** * @see org.mulgara.query.filter.value.ValueLiteral#getLexical() * @throws QueryException if this function does not resolve to a literal. */ public String getLexical() throws QueryException { RDFTerm result = resolve(); if (result.isLiteral()) return ((ValueLiteral)result).getLexical(); throw new QueryException("Type Error: Not valid to ask the lexical form of a: " + result.getClass().getSimpleName()); } /** * @see org.mulgara.query.filter.value.ValueLiteral#getLang() * @throws QueryException if this function does not resolve to a literal. */ public SimpleLiteral getLang() throws QueryException { RDFTerm result = resolve(); if (result.isLiteral()) return ((ValueLiteral)result).getLang(); throw new QueryException("Type Error: Not valid to ask the language of a: " + result.getClass().getSimpleName()); } /** * @see org.mulgara.query.filter.value.ValueLiteral#getType() * @throws QueryException if this function does not resolve to a literal. */ public IRI getType() throws QueryException { RDFTerm result = resolve(); if (result.isLiteral()) return ((ValueLiteral)result).getType(); throw new QueryException("Type Error: Not valid to ask the type of a: " + result.getClass().getSimpleName()); } /** @see org.mulgara.query.filter.AbstractFilterValue#isSimple() */ public boolean isSimple() throws QueryException { RDFTerm result = resolve(); if (result.isLiteral()) return ((ValueLiteral)result).isSimple(); throw new QueryException("Type Error: Not valid to check if a non-literal is a simple literal: " + result.getClass().getSimpleName()); } // The RDFTerm interface /** @see org.mulgara.query.filter.RDFTerm#isBlank() */ public boolean isBlank() throws QueryException { return resolve().isBlank(); } /** @see org.mulgara.query.filter.RDFTerm#isIRI() */ public boolean isIRI() throws QueryException { return resolve().isIRI(); } /** * {@inheritDoc} * The operation of this method is depended on the context in which it was called. * If it is called without a context owner, then this means it was called during * Filter construction, and we want to indicate that it is valid to treat this as a literal. * @return <code>true</code> if there is no context, or else it calls isLiteral on the resolved value. */ public boolean isLiteral() throws QueryException { return getContextOwner() == null ? true : resolve().isLiteral(); } /** @see java.lang.Object#toString() */ public String toString() { String result = "function[" + fnUri + "]"; if (extFn != null) result += " -> " + extFn.getClass().getName(); return result; } /** * Resolve the value of the function. * @return The resolution of the function * @throws QueryException if the function does not resolve */ protected RDFTerm resolve() throws QueryException { if (isConstructor) { List<Object> args = resolveArgs(); assert args.size() == 1; Object value = args.get(0); // being a cast we'll have to resort to the lowest common denominator of "string" // and let the TypedLiteral work it out for us if (XSD.isNumericType(fnUri) && value instanceof Number) return new NumericLiteral(NumericLiteral.getValueFor((Number)value, fnUri), fnUri); return TypedLiteral.newLiteral(value.toString(), fnUri, null); } if (extFn == null) { if (logger.isDebugEnabled()) logger.debug("Attempting to execute an unsupported function: " + fnUri + "(" + resolveArgs() + ")"); return Bool.FALSE; } if (unrecoverableError) return Bool.FALSE; Object result; try { result = extFn.evaluate(resolveArgs()); } catch (XPathFunctionException e) { if (invalidFunctionException(e)) { unrecoverableError = true; logger.error("Error executing XPathFunction", e); } else { if (logger.isDebugEnabled()) logger.debug("Error executing XPathFunction", e); } throw new QueryException("Error executing external function", e); } return (result.getClass() == URI.class) ? new IRI((URI)result) : TypedLiteral.newLiteral(result); } /** * A utility function to create a list of arguments to be passed to the external function. * @return A {@link List} of arbitrary objects to be passed as arguments to the external function. * @throws QueryException If any of the arguments could not be resolved. */ private List<Object> resolveArgs() throws QueryException { List<Object> result = new ArrayList<Object>(operands.length); for (int i = 0; i < operands.length; i++) { RDFTerm op = operands[i]; if (isConstructor && op.isBlank()) throw new QueryException("Type Error: Cannot cast a blank node."); result.add(op.getValue()); } return result; } /** * Extract a numeric value from this expression, if legal. This may result in a type exception. * @see org.mulgara.query.filter.value.NumericExpression#getNumber() * @return A numeric value for the resolved expression. * @throws QueryException If the resolved expression is not a numeric type. */ public Number getNumber() throws QueryException { RDFTerm result = resolve(); if (!result.isLiteral() && !(result instanceof NumericExpression)) throw new QueryException("Type Error: Not valid to ask the numeric form of a: " + result.getClass().getSimpleName()); return ((NumericExpression)result).getNumber(); } /** * Look for a function in the registered XPathFunctionResolvers. * @param iri The URI of the function to find. * @return The requested XPathFunction, or <code>null</code> if not found. */ private XPathFunction findFunction(IRI iri, int argCount) { String label = iri.toString() + "/" + argCount; XPathFunction result = fnCache.get(label); if (result == null) { QName fnName = iri.getQName(); if (fnName == null) return null; for (XPathFunctionResolver resolver: FunctionResolverRegistry.getFunctionResolverRegistry()) { try { result = resolver.resolveFunction(fnName, argCount); if (result != null) { fnCache.put(label, result); break; } } catch (Exception e) { // this resolver is unable to handle the given QName result = null; } } } return result; } /** * Test if an exception indicates that a method is unavailable. * @param e An exception thrown when an external method is invoked. * @return <code>true</code> if the exception indicates that the method can never be successful. */ private static boolean invalidFunctionException(Exception e) { Throwable t = e; do { if (t instanceof NoSuchMethodException) return true; } while ((t = t.getCause()) != null); return false; } }