/* This program 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, either version 3 of
the License, or (at your option) any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package org.opentripplanner.api.servlet;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A Servlet filter that constructs new objects of a given class, using reflection to pull their
* field values directly from the query parameters in an HttpRequest. It then seeds the request
* scope by storing a reference to the constructed object as an attribute of the HttpRequest itself.
*
* An instance of the requested class is first instantiated via its 0-argument constructor. Any
* initialization and defaults should be handled at this point.
*
* Next, field and setter method names are matched with query parameters in the incoming
* HttpRequest. Fields whose declared type has a constructor taking a single String argument
* (including String itself) will be set from the query parameter having the same name, if one
* exists. Setter methods will also be considered if 1) they have a single argument, and 2) that
* argument's class has a constructor with a single String argument.
*
* Query parameters are matched with setter methods according to the usual convention:
* changing the first character to upper case and prepending 'set'. A setter method invocation will
* be preferred to directly setting the field with the corresponding name, if it exists.
*
* @author andrewbyrd
*/
public class ReflectiveQueryScraper {
private static final Logger LOG = LoggerFactory.getLogger(ReflectiveQueryScraper.class);
protected final Class<?> targetClass;
private final Set<Target> targets;
public ReflectiveQueryScraper(Class<?> targetClass) {
this.targetClass = targetClass;
targets = new HashSet<Target>();
for (Field field : targetClass.getFields()) {
Target target = FieldTarget.instanceFor(field);
if (target != null)
targets.add(target);
}
for (Method method : targetClass.getMethods()) {
Target target = MethodTarget.instanceFor(method);
if (target != null)
targets.add(target);
}
LOG.info("initialized query scraper for: {}", targetClass);
for (Target t : targets)
LOG.info("-- {}", t);
}
public Object scrape(ServletRequest request) {
Object obj = null;
try {
obj = targetClass.newInstance();
for (Target t : targets)
t.apply(request, obj);
} catch (Exception e) {
LOG.warn("exception {} while scraping {}", e, targetClass);
}
return obj;
}
private static abstract class Target {
final String param;
final Constructor<?> constructor;
private Target (String param, Constructor<?> constructor) {
this.param = param; // upper/lower case?
this.constructor = constructor;
}
// NOTE: hashCode and equals reference only the param name. Collisions are intentional.
@Override public int hashCode() {
return param.hashCode();
}
@Override public boolean equals(Object other) {
return other instanceof Target && ((Target)other).param == this.param;
}
boolean apply(ServletRequest req, Object obj) throws Exception {
String value = req.getParameter(param);
if (value == null)
return false;
try {
apply0(obj, constructor.newInstance(value));
return true;
} catch (Exception e) {
LOG.warn("exception {} while applying {}", e, this);
return false;
}
}
abstract void apply0(Object obj, Object value) throws Exception;
abstract Member getTarget();
}
private static class FieldTarget extends Target {
final Field target;
private FieldTarget(Field field, Constructor<?> cons) {
super(field.getName(), cons);
target = field;
}
static Target instanceFor(Field f) {
Constructor<?> c = stringConstructor(f.getType());
if (c == null)
return null;
return new FieldTarget(f, c);
}
@Override
void apply0(Object obj, Object value) throws Exception {
target.set(obj, value);
}
@Override
Member getTarget () { return target; }
@Override
public String toString () {
return String.format("%s %s = %s('%s')", target.getType().getSimpleName(),
target.getName(), constructor.getName(), param);
}
}
// TODO: setters match param names with query parameters (allowing multiple parameters);
// setFoo disables direct setting of field 'foo'
private static class MethodTarget extends Target {
final Method target;
private MethodTarget(String param, Method method, Constructor<?> cons) {
super(param, cons);
target = method;
}
static Target instanceFor(Method method) {
String methodName = method.getName();
Class<?>[] params = method.getParameterTypes();
if (params.length != 1)
return null;
Constructor<?> c = stringConstructor(params[0]);
if (c == null)
return null;
if ( ! methodName.startsWith("set"))
return null;
if (methodName.length() == 3)
return null;
String baseName = methodName.substring(3,4).toLowerCase() + methodName.substring(4);
return new MethodTarget(baseName, method, c);
}
@Override
void apply0(Object obj, Object value) throws Exception {
target.invoke(obj, value);
}
@Override
Member getTarget () { return target; }
@Override
public String toString () {
return String.format("%s(%s('%s'))", target.getName(), constructor.getName(), param);
}
}
public static Constructor<?> stringConstructor(Class<?> clazz) {
clazz = filterPrimitives(clazz);
for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
Class<?>[] params = constructor.getParameterTypes();
if (params.length != 1)
continue;
if (params[0].equals(String.class))
return constructor;
}
return null;
}
// Amazingly, there is really no better way of doing this.
// Guava does provide wrap/unwrap implementations in Primitives.
public static Class<?> filterPrimitives(Class<?> clazz) {
if (WRAPPERS.containsKey(clazz))
return WRAPPERS.get(clazz);
return clazz;
}
public static Map<Class<?>, Class<?>> WRAPPERS = new HashMap<Class<?>, Class<?>>();
static {
WRAPPERS.put(boolean.class, Boolean.class);
WRAPPERS.put(byte.class, Byte.class);
WRAPPERS.put(double.class, Double.class);
WRAPPERS.put(float.class, Float.class);
WRAPPERS.put(int.class, Integer.class);
WRAPPERS.put(long.class, Long.class);
WRAPPERS.put(short.class, Short.class);
}
}