All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.daisy.common.xpath.saxon.ReflexiveExtensionFunctionProvider Maven / Gradle / Ivy

The newest version!
package org.daisy.common.xpath.saxon;

import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.xpath.XPath;

import org.daisy.common.saxon.SaxonHelper;
import org.daisy.common.saxon.SaxonOutputValue;
import org.daisy.common.transform.TransformerException;

import net.sf.saxon.dom.DocumentBuilderImpl;
import net.sf.saxon.expr.XPathContext;
import net.sf.saxon.lib.ExtensionFunctionCall;
import net.sf.saxon.lib.ExtensionFunctionDefinition;
import net.sf.saxon.om.Item;
import net.sf.saxon.om.NodeInfo;
import net.sf.saxon.om.Sequence;
import net.sf.saxon.om.StructuredQName;
import net.sf.saxon.s9api.XdmNode;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.value.SequenceExtent;
import net.sf.saxon.value.SequenceType;
import net.sf.saxon.xpath.XPathFactoryImpl;

/**
 * Poor man's implementation of Saxon's reflexive
 * extension function mechanism, which is only available in the PE and EE versions.
 */
public abstract class ReflexiveExtensionFunctionProvider implements ExtensionFunctionProvider {

	private final List definitions;

	public Collection getDefinitions() {
		return definitions;
	}

	protected ReflexiveExtensionFunctionProvider(Class definition) {
		Map> methods = new HashMap<>();
		for (Constructor constructor : definition.getConstructors()) {
			if (Modifier.isPublic(constructor.getModifiers())) {
				List list = methods.get("new");
				if (list == null) {
					list = new ArrayList<>();
					methods.put("new", list);
				}
				list.add(constructor);
				methods.put("new", list);
			}
		}
		for (Method method : definition.getDeclaredMethods()) {
			if (Modifier.isPublic(method.getModifiers())) {
				if ("toString".equals(method.getName())
				    && method.getParameterCount() == 0
				    && !Modifier.isStatic(method.getModifiers())) {
					// skip because the method can already be called through the string() function:
					// ObjectValue.getStringValueCS() calls Object.toString()
					continue;
				}
				List list = methods.get(method.getName());
				if (list == null) {
					list = new ArrayList<>();
					methods.put(method.getName(), list);
				}
				list.add(method);
				methods.put(method.getName(), list);
			}
		}
		definitions = new ArrayList<>();
		for (List m : methods.values()) {
			Collections.sort(m, (a, b) -> new Integer(a.getParameterCount()).compareTo(b.getParameterCount()));
			definitions.add(extensionFunctionDefinitionFromMethods(m));
		}
	}

	/**
	 * @param methods Collection of constructors of the same class, or methods of the same class and
	 *                with the same name. Assumed to be sorted by number of parameters (from least
	 *                to most number of parameters).
	 */
	private ExtensionFunctionDefinition extensionFunctionDefinitionFromMethods(Collection methods)
			throws IllegalArgumentException {
		ExtensionFunctionDefinition ret = null;
		for (Executable m : methods) {
			ExtensionFunctionDefinition def = extensionFunctionDefinitionFromMethod(m);
			if (ret == null)
				ret = def;
			else {
				ExtensionFunctionDefinition defA = ret;
				ExtensionFunctionDefinition defB = def;
				StructuredQName funcName = defA.getFunctionQName();
				if (!defB.getFunctionQName().equals(funcName))
					throw new IllegalArgumentException(); // should not happen
				SequenceType[] argTypes = defB.getArgumentTypes();
				if (defB.getMinimumNumberOfArguments() <= defA.getMaximumNumberOfArguments()
				    || !Arrays.equals(Arrays.copyOfRange(argTypes, 0, defA.getArgumentTypes().length), defA.getArgumentTypes())) {
					if (m instanceof Constructor)
						throw new IllegalArgumentException("Incompatible constructors");
					else // m instanceof Method
						throw new IllegalArgumentException("Incompatible '" + m.getName() + "' methods");
				}
				int minArgs = defA.getMinimumNumberOfArguments();
				int maxArgs = defB.getMaximumNumberOfArguments();
				ret = new ExtensionFunctionDefinition() {
						@Override
						public StructuredQName getFunctionQName() {
							return funcName;
						}
						@Override
						public int getMinimumNumberOfArguments() {
							return minArgs;
						}
						@Override
						public int getMaximumNumberOfArguments() {
							return maxArgs;
						}
						@Override
						public SequenceType[] getArgumentTypes() {
							return argTypes;
						}
						@Override
						public SequenceType getResultType(SequenceType[] suppliedArgTypes) {
							if (suppliedArgTypes.length >= defB.getMinimumNumberOfArguments())
								return defB.getResultType(suppliedArgTypes);
							else if (suppliedArgTypes.length <= defA.getMaximumNumberOfArguments())
								return defA.getResultType(suppliedArgTypes);
							else
								throw new IllegalArgumentException(
									"Function " + funcName + " can not be called with " + suppliedArgTypes.length + " arguments");
						}
						@Override
						public ExtensionFunctionCall makeCallExpression() {
							return new ExtensionFunctionCall() {
								ExtensionFunctionCall callA = null;
								ExtensionFunctionCall callB = null;
								@Override
								public Sequence call(XPathContext ctxt, Sequence[] args) throws XPathException {
									if (args.length <= defA.getMaximumNumberOfArguments()) {
										if (callA == null)
											callA = defA.makeCallExpression();
										return callA.call(ctxt, args);
									} else if (args.length >= defB.getMinimumNumberOfArguments()) {
										if (callB == null)
											callB = defB.makeCallExpression();
										return callB.call(ctxt, args);
									} else
										throw new IllegalArgumentException(
											"Function " + funcName + " can not be called with " + args.length + " arguments");
								}
							};
						}
					};
			}
		}
		if (ret == null)
			throw new IllegalArgumentException();
		return ret;
	}

	private ExtensionFunctionDefinition extensionFunctionDefinitionFromMethod(Executable method)
			throws IllegalArgumentException {
		assert method instanceof Constructor || method instanceof Method;
		if (method.isVarArgs())
			throw new IllegalArgumentException(); // vararg functions not supported
		else {
			Class declaringClass = method.getDeclaringClass();
			boolean isInnerClass = Arrays.stream(getClass().getClasses()).anyMatch(declaringClass::equals)
				// not static nested
				&& !Modifier.isStatic(declaringClass.getModifiers());
			boolean isConstructor = method instanceof Constructor;
			boolean isStatic = isConstructor || Modifier.isStatic(method.getModifiers());
			boolean requiresXMLStreamWriter = false;
			boolean requiresXPath = false;
			boolean requiresDocumentBuilder = false;
			for (Class t : method.getParameterTypes()) {
				if (t.equals(XPath.class)) {
					if (requiresXPath)
						throw new IllegalArgumentException(); // only one XPath argument allowed
					requiresXPath = true;
				} else if (t.equals(DocumentBuilder.class)) {
					if (requiresDocumentBuilder)
						throw new IllegalArgumentException(); // only one DocumentBuilder argument allowed
					requiresDocumentBuilder = true;
				} else if (t.equals(XMLStreamWriter.class)) {
					if (requiresXMLStreamWriter)
						throw new IllegalArgumentException(); // only one XMLStreamWriter argument allowed
					requiresXMLStreamWriter = true;
				}
			}
			Type[] javaArgumentTypes; { // arguments of Java method/constructor
				Type[] types = method.getGenericParameterTypes();
				javaArgumentTypes = isConstructor && isInnerClass
					// in case of constructor of inner class, first element is type of enclosing instance
					? Arrays.copyOfRange(types, 1, types.length)
					: types;
			}
			SequenceType[] argumentTypes = new SequenceType[ // arguments of XPath function
				javaArgumentTypes.length
				+ (isStatic ? 0 : 1)
				- (requiresXMLStreamWriter ? 1 : 0)
				- (requiresXPath ? 1 : 0)
				- (requiresDocumentBuilder ? 1 : 0)
			];
			int i = 0;
			if (!isStatic)
				argumentTypes[i++] = SequenceType.SINGLE_ITEM; // must be special wrapper item
			for (Type t : javaArgumentTypes)
				if (!t.equals(XMLStreamWriter.class) &&
				    !t.equals(XPath.class) &&
				    !t.equals(DocumentBuilder.class))
					argumentTypes[i++] = SaxonHelper.sequenceTypeFromType(t);
			SequenceType resultType; {
				if (isConstructor) {
					if (requiresXMLStreamWriter)
						throw new IllegalArgumentException(); // no XMLStreamWriter argument allowed in case of constructor
					resultType = SequenceType.SINGLE_ITEM; // special wrapper item
				} else {
					Type t = ((Method)method).getGenericReturnType();
					if (requiresXMLStreamWriter) {
						if (!t.equals(Void.TYPE))
							throw new IllegalArgumentException(); // XMLStreamWriter argument only allowed when return type is void
						resultType = SequenceType.NODE_SEQUENCE;
					} else
						resultType = SaxonHelper.sequenceTypeFromType(t);
				}
			}
			return new ExtensionFunctionDefinition() {
				@Override
				public SequenceType[] getArgumentTypes() {
					return argumentTypes;
				}
				@Override
				public StructuredQName getFunctionQName() {
					return new StructuredQName(declaringClass.getSimpleName(),
					                           declaringClass.getName(),
					                           isConstructor ? "new" : method.getName());
				}
				@Override
				public SequenceType getResultType(SequenceType[] suppliedArgTypes) {
					return resultType;
				}
				@Override
				public ExtensionFunctionCall makeCallExpression() {
					return new ExtensionFunctionCall() {
						@Override
						public Sequence call(XPathContext ctxt, Sequence[] args) throws XPathException {
							try {
								if (args.length != argumentTypes.length)
									throw new IllegalArgumentException(); // should not happen
								int i = 0;
								Object instance = null;
								if (!isStatic) {
									Item item = SaxonHelper.getSingleItem(args[i++]);
									try {
										instance = SaxonHelper.objectFromItem(item, declaringClass);
									} catch (IllegalArgumentException e) {
										throw new IllegalArgumentException(
											"Expected ObjectValue<" + declaringClass.getSimpleName() + ">" + ", but got: " + item, e);
									}
								}
								List nodeListFromXMLStreamWriter = null;
								Object[] javaArgs = new Object[ // arguments passed to Method.invoke() in addition to instance,
								                                // or to Constructor.newInstance()
									method.getParameterCount()
								];
								int j = 0;
								if (isConstructor && isInnerClass)
									// in case of constructor of inner class, first argument is enclosing instance
									javaArgs[j++] = ReflexiveExtensionFunctionProvider.this;
								for (Type type : javaArgumentTypes) {
									if (type.equals(XMLStreamWriter.class)) {
										List list = new ArrayList<>();
										nodeListFromXMLStreamWriter = list;
										XMLStreamWriter xmlStreamWriterArg = new SaxonOutputValue(
											item -> {
												if (item instanceof XdmNode)
													list.add(((XdmNode)item).getUnderlyingNode());
												else
													throw new RuntimeException(); // should not happen
											},
											ctxt.getConfiguration()
										).asXMLStreamWriter();
										javaArgs[j++] = xmlStreamWriterArg;
									} else if (type.equals(XPath.class))
										javaArgs[j++] = new XPathFactoryImpl(ctxt.getConfiguration()).newXPath();
									else if (type.equals(DocumentBuilder.class)) {
										DocumentBuilderImpl b = new DocumentBuilderImpl();
										b.setConfiguration(ctxt.getConfiguration());
										javaArgs[j++] = b;
									} else if (type instanceof ParameterizedType
									           && ((ParameterizedType)type).getRawType().equals(Iterator.class))
										javaArgs[j++] = SaxonHelper.iteratorFromSequence(
											args[i++],
											((ParameterizedType)type).getActualTypeArguments()[0]);
									else if (type instanceof ParameterizedType
									           && ((ParameterizedType)type).getRawType().equals(Iterable.class))
										javaArgs[j++] = SaxonHelper.iterableFromSequence(
											args[i++],
											((ParameterizedType)type).getActualTypeArguments()[0]);
									else if (type instanceof ParameterizedType
									           && ((ParameterizedType)type).getRawType().equals(Optional.class)) {
										Optional item = SaxonHelper.getOptionalItem(args[i++]);
										javaArgs[j++] = item.isPresent()
											? Optional.of(SaxonHelper.objectFromItem(item.get(), ((ParameterizedType)type).getActualTypeArguments()[0]))
											: Optional.empty();
									} else if (type.equals(URI.class)) {
										// could be empty because sequenceTypeFromType() returned OPTIONAL_ANY_URI
										Optional item = SaxonHelper.getOptionalItem(args[i++]);
										if (!item.isPresent())
											throw new IllegalArgumentException("Expected xs:anyURI but got an empty sequence");
										javaArgs[j++] = SaxonHelper.objectFromItem(item.get(), type);
									} else
										javaArgs[j++] = SaxonHelper.objectFromItem(SaxonHelper.getSingleItem(args[i++]), type);
								}
								Object result; {
									try {
										if (isConstructor)
											result = ((Constructor)method).newInstance(javaArgs);
										else // instance is null in case of static method
											result = ((Method)method).invoke(instance, javaArgs);
									} catch (InstantiationException|InvocationTargetException e) {
										Throwable cause = e.getCause();
										if (cause instanceof XPathException)
											throw (XPathException)cause;
										else if (cause instanceof TransformerException) {
											// TransformerException is just a wrapper of the actual exception, so unwrap
											// it. Note that if the TransformerException is an instance of
											// XProcErrorException, unwrapping it will yield an XProcException.
											XPathException xpe = new XPathException(cause.getMessage(), cause.getCause());
											QName code = ((TransformerException)cause).getCode();
											xpe.setErrorCodeQName(new StructuredQName(code.getPrefix(),
											                                          code.getNamespaceURI(),
											                                          code.getLocalPart()));
											throw xpe;
										} else
											throw new XPathException(cause);
									} catch (IllegalAccessException e) {
										throw new RuntimeException(); // should not happen
									}
								}
								if (nodeListFromXMLStreamWriter != null)
									return new SequenceExtent(nodeListFromXMLStreamWriter);
								if (result instanceof Optional)
									return SaxonHelper.sequenceFromObject(((Optional)result).orElse(null));
								else
									return SaxonHelper.sequenceFromObject(result);
							} catch (RuntimeException e) {
								throw new XPathException("Unexpected error in " + getFunctionQName().getClarkName(), e);
							}
						}
					};
				}
			};
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy