/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2009-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* 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.geotoolkit.metadata;
import java.util.Map;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.lang.reflect.Type;
import java.lang.reflect.Method;
import java.lang.reflect.WildcardType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.InvocationTargetException;
import org.opengis.util.FactoryException;
/**
* The method used for creating an instance of some specific class using a factory.
* This is used by {@link MetadataFactory} only.
* <p>
* Factory method starts with {@link #create}, and their first parameter type is {@code Map},
* {@code Map<String,?>} or {@code Map<String,Object>}.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.03
*
* @since 3.03
* @module
*/
final class FactoryMethod {
/**
* A dummy {@code FactoryMethod} used by {@link MetadataFactory} as a sentinel
* values when we have determined that there is no factory method available.
*/
static final FactoryMethod NULL = new FactoryMethod(null, null);
/**
* The method to invoke on a factory.
*/
private final Method method;
/**
* The factory instance on which to invoke the method.
*/
private final Object factory;
/**
* Creates a new {@code FactoryMethod} object.
*
* @param method The method to invoke on a factory.
* @param factory The factory instance on which to invoke the method.
*/
private FactoryMethod(final Method method, final Object factory) {
this.method = method;
this.factory = factory;
}
/**
* Creates a new {@code FactoryMethod} instance for creating an object of the given type
* from the given factory. The first suitable factory is used. If no suitable factory is
* found, then this method returns {@code null}.
*
* @param type The type of the object to create.
* @param factories The factories to try.
* @return A new {@code FactoryMethod}, or {@code null} if none.
*/
static FactoryMethod find(final Class<?> type, final Object[] factories) {
for (final Object factory : factories) {
for (final Method method : factory.getClass().getMethods()) {
if (method.isSynthetic() || method.isAnnotationPresent(Deprecated.class)) {
// Ignores synthetic and deprecated methods.
continue;
}
final String name = method.getName();
if (!name.startsWith("create")) {
continue;
}
if (!type.isAssignableFrom(method.getReturnType())) {
continue;
}
Type[] types = method.getGenericParameterTypes();
if (types.length == 0) {
continue;
}
/*
* We have found a "create" method with a suitable return type. Now check
* the parameters. The first one must be either the Map raw parameter type,
* the Map<String,?> type or the Map<String,Object> one.
*/
final Type mapType = types[0];
if (mapType != Map.class) {
if (!(mapType instanceof ParameterizedType)) {
continue;
}
types = ((ParameterizedType) mapType).getActualTypeArguments();
if (types.length != 2 ||
!bounds(types[0]).isAssignableFrom(String.class) ||
!bounds(types[1]).isAssignableFrom(Object.class))
{
continue;
}
return new FactoryMethod(method, factory);
}
}
}
return null;
}
/**
* Returns the upper bounds of the given type. The type must be either a (@link Class} or
* a {@link WildcardType}. We assume that no other type can occur in this implementation.
*/
private static Class<?> bounds(Type type) {
while (type instanceof WildcardType) {
final Type[] types = ((WildcardType) type).getUpperBounds();
if (types.length == 1) {
type = types[0];
}
}
return (Class<?>) type;
}
/**
* Invokes the {@code Factory.create(Map, ...)} method with the given properties.
* The values in the given map that are assignable to the parameter type expected
* by the factory method are extracted from the map and assigned to the parameter
* array.
* <p>
* In the special case where this {@code FactoryMethod} is {@link #NULL}, this
* method returns {@code null}.
*/
Object create(Map<String,?> properties) throws FactoryException {
if (method == null) {
return null;
}
final Class<?>[] types = method.getParameterTypes();
final Object[] parameters = new Object[types.length];
if (types.length > 1) {
final Map<String,Object> reduced = new LinkedHashMap<>(properties);
properties = reduced;
for (int i=1; i<types.length; i++) {
final Class<?> expected = types[i];
for (final Iterator<Object> it=reduced.values().iterator(); it.hasNext();) {
final Object value = it.next();
if (expected.isInstance(value)) {
parameters[i] = value;
it.remove();
break;
}
}
}
}
parameters[0] = properties;
try {
return method.invoke(factory, parameters);
} catch (IllegalAccessException e) {
// Should never happen, because we asked for public methods only.
throw new AssertionError(e);
} catch (InvocationTargetException e) {
final Throwable cause = e.getCause();
if (cause instanceof FactoryException) {
throw (FactoryException) cause;
}
throw new FactoryException(cause.getLocalizedMessage(), cause);
}
}
/**
* Returns {@code true} if the given object is equals to this factory method.
*/
@Override
public boolean equals(final Object other) {
if (other instanceof FactoryMethod) {
final FactoryMethod that = (FactoryMethod) other;
return method.equals(that.method) && factory == that.factory;
}
return false;
}
/**
* Defined for consistency with {@link #equals} but not used.
*/
@Override
public int hashCode() {
return method.hashCode() ^ System.identityHashCode(factory);
}
}