/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2009, Open Source Geospatial Foundation (OSGeo)
*
* This library 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;
* version 2.1 of the License.
*
* This library 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.
*/
package org.geotools.process.factory;
import java.awt.RenderingHints.Key;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import org.geotools.data.Parameter;
import org.geotools.data.Query;
import org.geotools.process.Process;
import org.geotools.process.ProcessException;
import org.geotools.process.ProcessFactory;
import org.geotools.process.RenderingProcess;
import org.geotools.util.Converters;
import org.geotools.util.SimpleInternationalString;
import org.opengis.coverage.grid.GridGeometry;
import org.opengis.feature.type.Name;
import org.opengis.util.InternationalString;
import org.opengis.util.ProgressListener;
public abstract class AnnotationDrivenProcessFactory implements ProcessFactory {
static final String INVERT_GRID_GEOMETRY = "invertGridGeometry";
static final String INVERT_QUERY = "invertQuery";
String namespace;
InternationalString title;
public AnnotationDrivenProcessFactory(InternationalString title, String namespace) {
this.namespace = namespace;
this.title = title;
}
protected abstract DescribeProcess getProcessDescription(Name name);
protected abstract Method method(String localPart);
public InternationalString getTitle() {
return title;
}
public InternationalString getDescription(Name name) {
DescribeProcess info = getProcessDescription(name);
if (info != null) {
return new SimpleInternationalString(info.description());
} else {
return null;
}
}
public Map<String, Parameter<?>> getParameterInfo(Name name) {
// build the parameter descriptions by using the DescribeParameter
// annotations
Method method = method(name.getLocalPart());
Map<String, Parameter<?>> input = new LinkedHashMap<String, Parameter<?>>();
Annotation[][] params = method.getParameterAnnotations();
Class<?>[] paramTypes = method.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) {
if (!(ProgressListener.class.isAssignableFrom(paramTypes[i]))) {
Parameter<?> param = paramInfo(method.getDeclaringClass(), i, paramTypes[i], params[i]);
input.put(param.key, param);
}
}
return input;
}
@SuppressWarnings("unchecked")
public Map<String, Parameter<?>> getResultInfo(Name name, Map<String, Object> parameters)
throws IllegalArgumentException {
Method method = method(name.getLocalPart());
// look for the DescribeResult annotations (the result could be a
// key/value
// map holding multiple results)
Map<String, Parameter<?>> result = new LinkedHashMap<String, Parameter<?>>();
for (Annotation annotation : method.getAnnotations()) {
if (annotation instanceof DescribeResult) {
DescribeResult info = (DescribeResult) annotation;
// see if a type has been declared, otherwise use the annotation
addResult(method, result, info);
} else if(annotation instanceof DescribeResults) {
DescribeResults info = (DescribeResults) annotation;
for (DescribeResult dr : info.value()) {
addResult(method, result, dr);
}
}
}
// if annotation is not found, return a generic description using
// the method return type
if (result.isEmpty()) {
if (!Void.class.equals(method.getReturnType())) {
Parameter<?> VALUE = new Parameter("result", method.getReturnType(),
"Process result", "No description is available");
result.put(VALUE.key, VALUE);
}
}
return result;
}
private void addResult(Method method, Map<String, Parameter<?>> result, DescribeResult info) {
Class resultType = info.type();
if (Object.class.equals(resultType)) {
resultType = method.getReturnType();
}
int min = info.primary() ? 0 : 1;
Parameter resultParam = new Parameter(info.name(), resultType, new SimpleInternationalString(info.name()),
new SimpleInternationalString(info.description()), min > 0, min, 1, null,
null);
result.put(resultParam.key, resultParam);
}
public InternationalString getTitle(Name name) {
DescribeProcess info = getProcessDescription(name);
if (info != null) {
return new SimpleInternationalString(info.description());
} else {
return null;
}
}
public String getVersion(Name name) {
DescribeProcess info = getProcessDescription(name);
if (info != null) {
return info.version();
} else {
return null;
}
}
public boolean supportsProgress(Name name) {
return false;
}
public boolean isAvailable() {
return true;
}
public Map<Key, ?> getImplementationHints() {
return null;
}
@SuppressWarnings("unchecked")
Parameter<?> paramInfo(Class process, int i, Class<?> type, Annotation[] paramAnnotations) {
DescribeParameter info = null;
for (Annotation annotation : paramAnnotations) {
if (annotation instanceof DescribeParameter) {
info = (DescribeParameter) annotation;
break;
}
}
// handle collection type and multiplicity
boolean collection = Collection.class.isAssignableFrom(type);
int min = 1;
int max = 1;
if (collection) {
if (info != null) {
type = info.collectionType();
if (type == null) {
type = Object.class;
}
min = info.min() > -1 ? info.min() : 0;
max = info.max() > -1 ? info.max() : Integer.MAX_VALUE;
} else {
type = Object.class;
min = 0;
max = Integer.MAX_VALUE;
}
} else if (type.isArray()) {
if (info != null) {
min = info.min() > -1 ? info.min() : 0;
max = info.max() > -1 ? info.max() : Integer.MAX_VALUE;
} else {
min = 0;
max = Integer.MAX_VALUE;
}
type = type.getComponentType();
} else {
if (info != null) {
if (info.min() > 1) {
throw new IllegalArgumentException("The non collection parameter at index " + i
+ " cannot have a min multiplicity > 1");
}
min = info.min() > -1 ? info.min() : 1;
if (info.max() > 1) {
throw new IllegalArgumentException("The non collection parameter at index " + i
+ " cannot have a max multiplicity > 1");
}
max = info.max() > -1 ? info.max() : 1;
}
}
if (min > max) {
throw new IllegalArgumentException(
"Min occurrences > max occurrences for parameter at index " + i);
}
if (min == 0 && max == 1 && type.isPrimitive()) {
throw new IllegalArgumentException("Optional values cannot be primitives, " +
"use the associated object wrapper instead: " + info.name() + " in process " + process.getName());
}
// finally build the parameter
if (info != null) {
return new Parameter(info.name(), type, new SimpleInternationalString(info.name()),
new SimpleInternationalString(info.description()), min > 0, min, max, null,
null);
} else {
return new Parameter("arg" + i, type, new SimpleInternationalString("Argument " + i),
new SimpleInternationalString("Input " + type.getName() + " value"), min > 0,
min, max, null, null);
}
}
public Process create(Name name) {
Method meth = method(name.getLocalPart());
Object process = createProcessBean(name);
if (process != null && (lookupMethod(process, INVERT_GRID_GEOMETRY) != null
|| lookupMethod(process, INVERT_QUERY) != null)) {
return new RenderingProcessInvocation(meth, process);
} else {
return new ProcessInvocation(meth, process);
}
}
/**
* Looks up a method in an object (by simple name)
* @param targetObject
* @param methodName
* @return
*/
Method lookupMethod(Object targetObject, String methodName) {
Method method = null;
for (Method m : targetObject.getClass().getMethods()) {
if (Modifier.isPublic(m.getModifiers()) && methodName.equals(m.getName())) {
method = m;
break;
}
}
return method;
}
/**
* Creates the bean upon which the process execution method will be invoked. Can be null in case
* the method is a static one
*
* @param name
* @return
*/
protected abstract Object createProcessBean(Name name);
/**
* Executes the method as a process
*/
class ProcessInvocation implements Process {
Method method;
Object targetObject;
public ProcessInvocation(Method method, Object targetObject) {
this.method = method;
this.targetObject = targetObject;
}
@SuppressWarnings("unchecked")
public Map<String, Object> execute(Map<String, Object> input, ProgressListener monitor)
throws ProcessException {
Object[] args = buildProcessArguments(method, input, monitor, false);
// invoke and grab result
Object value = null;
try {
value = method.invoke(targetObject, args);
} catch (IllegalAccessException e) {
// report the exception and exit
if (monitor != null) {
monitor.exceptionOccurred(e);
}
throw new ProcessException(e);
} catch (InvocationTargetException e) {
Throwable t = e.getTargetException();
// report the exception and exit
if (monitor != null) {
monitor.exceptionOccurred(t);
}
if (t instanceof ProcessException) {
throw ((ProcessException) t);
} else {
throw new ProcessException(t);
}
}
// build up the result
if (value instanceof Object[]) {
// handle the case the implementor used a positional array for
// returning multiple outputs
Object values[] = (Object[]) value;
Map<String, Object> result = new LinkedHashMap<String, Object>();
int i = 0;
for (Annotation annotation : method.getAnnotations()) {
if (i >= values.length)
break; // no more values to encode
Object obj = values[i];
if (annotation instanceof DescribeResult) {
DescribeResult info = (DescribeResult) annotation;
addResult(result, obj, info);
}
if (annotation instanceof DescribeResults) {
DescribeResults info = (DescribeResults) annotation;
for (DescribeResult dr : info.value()) {
addResult(result, obj, dr);
}
}
}
return result;
} else if (value instanceof Map) {
Map<String, Object> result = new LinkedHashMap<String, Object>();
Map<String, Object> map = (Map<String, Object>) value;
for (Annotation annotation : method.getAnnotations()) {
if (annotation instanceof DescribeResult) {
DescribeResult info = (DescribeResult) annotation;
Object resultValue = map.get(info.name());
if(resultValue != null) {
addResult(result, resultValue, info);
}
}
if (annotation instanceof DescribeResults) {
DescribeResults info = (DescribeResults) annotation;
for (DescribeResult dr : info.value()) {
Object resultValue = map.get(dr.name());
if(resultValue != null) {
addResult(result, resultValue, dr);
}
}
}
}
return result;
} else if (!Void.class.equals(method.getReturnType())) {
// handle the single result case
Map<String, Object> result = new LinkedHashMap<String, Object>();
DescribeResult dr = method.getAnnotation(DescribeResult.class);
if (dr != null) {
result.put(dr.name(), value);
} else {
result.put("result", value);
}
return result;
}
// handle the case where the method returns void
return null;
}
private void addResult(Map<String, Object> result, Object obj, DescribeResult info) {
if (info.type().isInstance(obj)) {
result.put(info.name(), obj);
} else {
throw new IllegalArgumentException(method.getName()
+ " unable to encode result " + obj + " as " + info.type());
}
}
protected Object[] buildProcessArguments(Method method, Map<String, Object> input, ProgressListener monitor, boolean skip)
throws ProcessException {
// build the array of arguments we'll use to invoke the method
Class<?>[] paramTypes = method.getParameterTypes();
Annotation[][] annotations = method.getParameterAnnotations();
Object args[] = new Object[paramTypes.length];
for (int i = 0; i < args.length; i++) {
if (ProgressListener.class.equals(paramTypes[i])) {
// pass in the monitor
args[i] = monitor;
} else {
// if we can skip and there is no annotation, skip
if((annotations[i] == null || annotations[i].length == 0) && skip) {
continue;
}
// find the corresponding argument in the input
// map and set it
Class<? extends Object> target = targetObject == null ? null : targetObject.getClass();
Parameter p = paramInfo(target, i, paramTypes[i], annotations[i]);
Object value = input.get(p.key);
// this takes care of array/collection conversions among
// others
args[i] = Converters.convert(value, paramTypes[i]);
// check the conversion was successful
if (args[i] == null && value != null) {
throw new ProcessException("Could not convert " + value
+ " to target type " + paramTypes[i].getName());
}
// check multiplicity is respected
if (p.minOccurs > 0 && value == null) {
throw new ProcessException("Parameter " + p.key
+ " is missing but has min multiplicity > 0");
} else if (p.maxOccurs > 1) {
int size = -1;
if(args[i] == null) {
size = 0;
} else if (paramTypes[i].isArray()) {
size = Array.getLength(args[i]);
} else {
size = ((Collection) args[i]).size();
}
if (size < p.minOccurs) {
throw new ProcessException("Parameter " + p.key + " has " + size
+ " elements but min occurrences is " + p.minOccurs);
}
if (size > p.maxOccurs) {
throw new ProcessException("Parameter " + p.key + " has " + size
+ " elements but max occurrences is " + p.maxOccurs);
}
}
}
}
return args;
}
}
/**
* Executes the method as a process
*/
class RenderingProcessInvocation extends ProcessInvocation implements RenderingProcess {
public RenderingProcessInvocation(Method method, Object targetObject) {
super(method, targetObject);
}
public Query invertQuery(Map<String, Object> input, Query targetQuery,
GridGeometry gridGeometry) throws ProcessException {
Method method = lookupMethod(targetObject, INVERT_QUERY);
if (method == null) {
return targetQuery;
}
try {
Object[] args = buildProcessArguments(method, input, null, true);
args[args.length - 2] = targetQuery;
args[args.length - 1] = gridGeometry;
return (Query) method.invoke(targetObject, args);
} catch (IllegalAccessException e) {
throw new ProcessException(e);
} catch (InvocationTargetException e) {
Throwable t = e.getTargetException();
if (t instanceof ProcessException) {
throw ((ProcessException) t);
} else {
throw new ProcessException(t);
}
}
}
public GridGeometry invertGridGeometry(Map<String, Object> input, Query targetQuery,
GridGeometry targetGridGeometry) throws ProcessException {
Method method = lookupMethod(targetObject, INVERT_GRID_GEOMETRY);
if (method == null) {
return targetGridGeometry;
}
try {
Object[] args = buildProcessArguments(method, input, null, true);
args[args.length - 2] = targetQuery;
args[args.length - 1] = targetGridGeometry;
return (GridGeometry) method.invoke(targetObject, args);
} catch (IllegalAccessException e) {
throw new ProcessException(e);
} catch (InvocationTargetException e) {
Throwable t = e.getTargetException();
if (t instanceof ProcessException) {
throw ((ProcessException) t);
} else {
throw new ProcessException(t);
}
}
}
}
}