From 091bac427125d019683e7f31657442a4f06e6385 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Tue, 21 May 2013 19:26:04 +0200 Subject: [PATCH] add minimal dependency injection framework --- opentripplanner-api-webapp/pom.xml | 6 + .../api/standalone/InjectedRESTResource.java | 27 ++++ .../api/standalone/JerseyInjector.java | 39 +++++ .../api/standalone/OTPComponentProvider.java | 143 ++++++++++++++++++ .../opentripplanner/api/standalone/Thing.java | 7 + opentripplanner-routing/pom.xml | 6 + 6 files changed, 228 insertions(+) create mode 100644 opentripplanner-api-webapp/src/main/java/org/opentripplanner/api/standalone/InjectedRESTResource.java create mode 100644 opentripplanner-api-webapp/src/main/java/org/opentripplanner/api/standalone/JerseyInjector.java create mode 100644 opentripplanner-api-webapp/src/main/java/org/opentripplanner/api/standalone/OTPComponentProvider.java create mode 100644 opentripplanner-api-webapp/src/main/java/org/opentripplanner/api/standalone/Thing.java diff --git a/opentripplanner-api-webapp/pom.xml b/opentripplanner-api-webapp/pom.xml index c37db959af7..db095365ded 100644 --- a/opentripplanner-api-webapp/pom.xml +++ b/opentripplanner-api-webapp/pom.xml @@ -164,6 +164,12 @@ com.sun.jersey jersey-grizzly2 + + + javax.inject + javax.inject + 1 + junit diff --git a/opentripplanner-api-webapp/src/main/java/org/opentripplanner/api/standalone/InjectedRESTResource.java b/opentripplanner-api-webapp/src/main/java/org/opentripplanner/api/standalone/InjectedRESTResource.java new file mode 100644 index 00000000000..2330ad9e78f --- /dev/null +++ b/opentripplanner-api-webapp/src/main/java/org/opentripplanner/api/standalone/InjectedRESTResource.java @@ -0,0 +1,27 @@ +package org.opentripplanner.api.standalone; + +import javax.annotation.Resource; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +import com.sun.jersey.spi.resource.Singleton; + +@Path("/inject") +@Singleton +public class InjectedRESTResource { + + // @Resource Thing t; + + @GET + @Produces({ MediaType.TEXT_PLAIN }) + public String get( + @QueryParam("x") @DefaultValue("4") double x, + @QueryParam("y") @DefaultValue("6") double y) { + return "x=" + x + " y=" + y; + } + +} diff --git a/opentripplanner-api-webapp/src/main/java/org/opentripplanner/api/standalone/JerseyInjector.java b/opentripplanner-api-webapp/src/main/java/org/opentripplanner/api/standalone/JerseyInjector.java new file mode 100644 index 00000000000..8232b18256c --- /dev/null +++ b/opentripplanner-api-webapp/src/main/java/org/opentripplanner/api/standalone/JerseyInjector.java @@ -0,0 +1,39 @@ +package org.opentripplanner.api.standalone; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.Resource; +import javax.ws.rs.ext.Provider; + +import com.sun.jersey.core.spi.component.ComponentContext; +import com.sun.jersey.core.spi.component.ComponentScope; +import com.sun.jersey.spi.inject.Injectable; +import com.sun.jersey.spi.inject.InjectableProvider; + +@Provider +public class JerseyInjector implements InjectableProvider { + + static Map m = new HashMap(); + + public static void put(Type key, Object value) { + m.put(key, value); + } + + @Override + public ComponentScope getScope() { + return ComponentScope.Singleton; + } + + @Override + public Injectable getInjectable(ComponentContext ic, Resource r, final Type t) { + return new Injectable() { + @Override + public Object getValue() { + return m.get(t); + } + }; + } + +} diff --git a/opentripplanner-api-webapp/src/main/java/org/opentripplanner/api/standalone/OTPComponentProvider.java b/opentripplanner-api-webapp/src/main/java/org/opentripplanner/api/standalone/OTPComponentProvider.java new file mode 100644 index 00000000000..53c4a0baf75 --- /dev/null +++ b/opentripplanner-api-webapp/src/main/java/org/opentripplanner/api/standalone/OTPComponentProvider.java @@ -0,0 +1,143 @@ +package org.opentripplanner.api.standalone; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.sun.jersey.core.spi.component.ComponentContext; +import com.sun.jersey.core.spi.component.ComponentScope; +import com.sun.jersey.core.spi.component.ioc.IoCComponentProvider; +import com.sun.jersey.core.spi.component.ioc.IoCComponentProviderFactory; +import com.sun.jersey.core.spi.component.ioc.IoCFullyManagedComponentProvider; + +/** + * Ultra-minimal implementation of a subset of JSR-330 dependency injection. + * + * Jersey IoCComponentProviders must implement one of IoCManagedComponentProvider, + * IoCFullyManagedComponentProvider, IoCInstantiatedComponentProvider, + * or IoCProxiedComponentProvider. This is checked in + * com.sun.jersey.core.spi.component.ioc.IoCProviderFactory.wrap(Class, IoCComponentProvider) + */ +public class OTPComponentProvider implements IoCFullyManagedComponentProvider { + + final Object o; + + public OTPComponentProvider(Object o) { + this.o = o; + } + + @Override + public ComponentScope getScope() { + return ComponentScope.Singleton; // we only bind objects in singleton scope + } + + @Override + public Object getInstance() { + return o; + } + + public static class Factory implements IoCComponentProviderFactory { + + private static final Logger LOG = LoggerFactory.getLogger(Factory.class); + + private Map, Object> bindings = new HashMap, Object>(); + private List bindingOrder = new ArrayList(); + + private boolean locked = false; + + public void bind(Class key, Object value) { + if (locked) { + LOG.error("Attempt to add a binding to a completed set of bindings."); + } else { + if (key.isInstance(value)) { + bindings.put(key, value); + bindingOrder.add(value); + } else { + LOG.error("Type mismatch in binding: " + key + " " + value); + } + } + } + + @Override + public IoCComponentProvider getComponentProvider(Class c) { + if ( ! bindings.containsKey(c)) { + LOG.debug("Requested component provider for unbound " + c); + return null; // let Jersey internal provider take care of it + } + LOG.info("Returning component provider for " + c); + return new OTPComponentProvider(bindings.get(c)); + } + + /** Ignore the ComponentContext, wrap the 1-arg version. */ + @Override + public IoCComponentProvider getComponentProvider(ComponentContext cc, Class c) { + return getComponentProvider(c); + } + + /** Call this method after all bindings have been established. */ + public void doneBinding() { + locked = true; + injectFields(); + invokePostConstructMethods(); + } + + /** Perform field injection on all bound instances, in the order they were bound. */ + private void injectFields() { + for (Object instance : bindingOrder) { + LOG.debug("Performing field injection on instance of class {}", + instance.getClass().getName()); + for (Field field : instance.getClass().getDeclaredFields()) { + LOG.debug("Considering field {} for injection", field.getName()); + if (field.isAnnotationPresent(Inject.class)) { + Object obj = bindings.get(field.getType()); + LOG.info("Injecting field {} on instance of {}", field.getName(), + instance.getClass()); + if (obj != null) { + try { + LOG.info("Injected object is {}", obj); + field.setAccessible(true); + field.set(instance, obj); + } catch (Exception ex) { + LOG.error("Failed to perform field injection: {}", ex.toString()); + ex.printStackTrace(); + } + } else { + LOG.error("Found no binding for injection."); + } + } + } + } + } + + /** Call all @PostConstruct-annotated methods on bound instances, in the order they were bound. */ + private void invokePostConstructMethods() { + // do not look at the binding class (key), but the concrete type of the instance (value) + for (Object instance : bindingOrder) { + // iterate over all declared methods, not just the public ones + for (Method method : instance.getClass().getDeclaredMethods()) { + if (method.isAnnotationPresent(PostConstruct.class)) { + try { + LOG.info("Calling post-construct {}", method); + method.setAccessible(true); + method.invoke(instance); + } catch (Exception ex) { + LOG.error("Failed to invoke post-construct: {}", ex.toString()); + ex.printStackTrace(); + } + } + } + } + } + + } + +} diff --git a/opentripplanner-api-webapp/src/main/java/org/opentripplanner/api/standalone/Thing.java b/opentripplanner-api-webapp/src/main/java/org/opentripplanner/api/standalone/Thing.java new file mode 100644 index 00000000000..c1915d59dd4 --- /dev/null +++ b/opentripplanner-api-webapp/src/main/java/org/opentripplanner/api/standalone/Thing.java @@ -0,0 +1,7 @@ +package org.opentripplanner.api.standalone; + +public class Thing { + + private int nothing = 0; + +} diff --git a/opentripplanner-routing/pom.xml b/opentripplanner-routing/pom.xml index 6f0ebb4c011..b6dd6d47c4a 100644 --- a/opentripplanner-routing/pom.xml +++ b/opentripplanner-routing/pom.xml @@ -76,6 +76,12 @@ org.geotools gt-geojson + + + javax.inject + javax.inject + 1 +