WebInterfaceDispatcher.java
/***************************************************************************
Copyright 2012 Emily Estes
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
***************************************************************************/
package net.metanotion.web.concrete;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.metanotion.util.Dispatcher;
import net.metanotion.util.MapToListDispatcher;
import net.metanotion.util.Message;
import net.metanotion.util.NamedParameterMapper;
import net.metanotion.util.ObjectParameterMapper;
import net.metanotion.util.Pair;
import net.metanotion.util.ReflectionJsonParameterDispatcher;
import net.metanotion.util.ReflectionListDispatcher;
import net.metanotion.util.ResultTransformerDispatcher;
import net.metanotion.util.ServiceListDispatcherMixin;
import net.metanotion.util.TransactionListDispatcherMixin;
import net.metanotion.util.Unknown;
import net.metanotion.util.types.JsonMagicTypeDictionary;
import net.metanotion.web.HttpMethod;
import net.metanotion.web.HttpDefault;
import net.metanotion.web.HttpAny;
import net.metanotion.web.HttpDelete;
import net.metanotion.web.HttpGet;
import net.metanotion.web.HttpHead;
import net.metanotion.web.HttpNone;
import net.metanotion.web.HttpPost;
import net.metanotion.web.HttpPut;
import net.metanotion.web.RequestReader;
import net.metanotion.web.RequestStream;
import net.metanotion.web.RequestObject;
/** <p>Generate a "basic" dispatcher via reflection on an object or interface suitable for use with web requests(making
use of annotations to suggest how to dispatch a method). This class is geared for making a relatively straightforware
mapping from HTTP requests to methods. The resource/URI is presumed to be the method name(this works best with things
like the {@link net.metanotion.web.concrete.URIPrefixDispatcher} operating in reducing mode where resource prefixes are
stripped), and the method parameters are referenced by name and come from either the query string or a form encoded
request body. This class is built on the {@link net.metanotion.util.MapToListDispatcher} and the
{@link net.metanotion.util.ReflectionListDispatcher}.</p>
<p>To exert more control over how HTTP requests are mapped to methods, several annotations can be used on methods.
<ul>
<li>In order to match request variables(query string or form inputs) to their corresponding parameter, this class
must be able to extract the names of all the parameters. In the case of Java interfaces, this is not possible,
however, in that case this class will use the {@link javax.inject.Named} annotation on parameters to extract
the name. In the case of regular objects, parameter names can be extracted via byte code disassembly only if
the class was compiled with debug mode and no name mangling occurred. In most cases, the safest thing is to
use the {@literal @}name annotation.</li>
<li>{@link net.metanotion.web.HttpDelete}, {@link net.metanotion.web.HttpGet}, {@link net.metanotion.web.HttpHead},
{@link net.metanotion.web.HttpPost}, {@link net.metanotion.web.HttpPut} are used to denote a method can be used
with the corresponding HTTP verb. The {@link net.metanotion.web.HttpAny} annotation means that ANY HTTP
verb is allowed, and {@link net.metanotion.web.HttpNone} means that the method should <b>not</b> be exposed
over HTTP.</li>
<li>{@link net.metanotion.web.HttpDefault} denotes a method as being appropriate in the case that no suitable
method can be found.</li>
<li>{@link net.metanotion.util.Service} when used on a parameter means that instead of looking for the parameter
in an HTTP request, the parameter is a service provided by the session instance the method is being dispatched
against.</li>
</ul></p>
@param <O> The final type of the class this dispatcher will dispatch against.
*/
public final class WebInterfaceDispatcher<O> implements Dispatcher<Unknown, RequestObject> {
private static final Logger logger = LoggerFactory.getLogger(WebInterfaceDispatcher.class);
private static final ObjectParameterMapper objectMapper = new ObjectParameterMapper();
/** The keys to this map are HTTP verbs. The values are the set of methods that are allowable for that HTTP verb. */
private final Map<HttpMethod, Set<String>> verbMethodMap = new HashMap<>();
private final Map<String,Parse> requestParams = new HashMap<>();
/** The name of the default method to use when no other suitable method can be found. If this is null, then there
is no default method. */
private final String defaultMethod;
private final Dispatcher<Unknown, Map.Entry<String,Object>> mapDisp;
private static Map<String,Iterable<String>> getParamMap(final Class klazz) throws IOException {
final NamedParameterMapper namedMapper = new NamedParameterMapper();
final Map<String,Iterable<String>> paramMap = namedMapper.read(klazz);
if(!namedMapper.isNamed()) {
logger.debug("Parameters aren't @Named, using byte code disassembly");
objectMapper.read(klazz, paramMap);
}
return paramMap;
}
private interface Parse { public Map<String,Object> parse(Map<String,Object> params, RequestObject ro); }
private static final class ParseParams implements Parse {
public final boolean nullOp;
private final String param;
private final boolean isReader;
public ParseParams(final Method m, final Iterable<String> paramNames) {
final Class[] params = m.getParameterTypes();
final Annotation[][] parameterAnnotations = m.getParameterAnnotations();
final Iterator<String> it = paramNames.iterator();
int i = 0;
for(Class c: params) {
for(Annotation a: parameterAnnotations[i]) {
if(a instanceof RequestReader) {
this.param = it.next();
this.nullOp = false;
this.isReader = true;
return;
} else if(a instanceof RequestStream) {
this.param = it.next();
this.nullOp = false;
this.isReader = false;
return;
}
}
i++;
it.next();
}
this.param = null;
this.isReader = false;
this.nullOp = true;
}
@Override public Map<String,Object> parse(final Map<String,Object> params, final RequestObject ro) {
logger.debug("Storing: {}, {}", param, isReader);
if(isReader) {
params.put(param, ro.getRawPost());
} else {
params.put(param, ro.getByteStream());
}
return params;
}
}
private static <O> Dispatcher<O, Map.Entry<String,Object>> makeDefaultDispatcher(final Class<O> klazz,
final Map<String,Iterable<String>> paramMap) {
final ReflectionListDispatcher<O> rld = new ReflectionListDispatcher<O>(klazz);
final ReflectionJsonParameterDispatcher<O> rjpd =
new ReflectionJsonParameterDispatcher<O>(klazz, new JsonMagicTypeDictionary(), rld);
final ServiceListDispatcherMixin<O> sldm = new ServiceListDispatcherMixin<O>(klazz, rjpd);
final TransactionListDispatcherMixin<O> tldm = new TransactionListDispatcherMixin<O>(klazz, sldm);
final MapToListDispatcher<Unknown> mtld = new MapToListDispatcher<Unknown>(paramMap, tldm);
return new ResultTransformerDispatcher(Unknown.class, JsonUtil.generateJsonResultTransformers(klazz), mtld);
}
/** Create a dispatcher for the class suitable for mapping HTTP Requests({@link net.metanotion.web.RequestObject})
to method calls.
@param klazz The class to create a dispatcher for.
@throws IOException if there is an error extracting parameter names from the class.
*/
public WebInterfaceDispatcher(final Class<O> klazz) throws IOException {
this(klazz, WebInterfaceDispatcher.getParamMap(klazz));
}
/** Create a dispatcher for the class suitable for mapping HTTP Requests({@link net.metanotion.web.RequestObject})
to method calls using a provided parameter map.
@param klazz The class to create a dispatcher for.
@param paramMap A Map of method names to values listing the name of the parameters in parameter order.
*/
public WebInterfaceDispatcher(final Class<O> klazz, final Map<String,Iterable<String>> paramMap) {
this(klazz, paramMap, WebInterfaceDispatcher.makeDefaultDispatcher(klazz, paramMap));
}
/** Create a dispatcher for the class suitable for mapping HTTP Requests({@link net.metanotion.web.RequestObject})
to method calls using a provided parameter map and delegating to the dispatcher provided This constructor
essentially just reflects for the HTTP method annotations on method.
@param klazz The class to create a dispatcher for.
@param paramMap A Map of method names to values listing the name of the parameters in parameter order.
@param mapDisp The dispatcher to delegate to after decoding the request object.
@see net.metanotion.web.HttpAny
@see net.metanotion.web.HttpDefault
@see net.metanotion.web.HttpDelete
@see net.metanotion.web.HttpGet
@see net.metanotion.web.HttpHead
@see net.metanotion.web.HttpNone
@see net.metanotion.web.HttpPost
@see net.metanotion.web.HttpPut
@see net.metanotion.util.MapToListDispatcher
@see net.metanotion.util.ReflectionListDispatcher
*/
public WebInterfaceDispatcher(final Class<O> klazz,
final Map<String,Iterable<String>> paramMap,
final Dispatcher<O, Map.Entry<String,Object>> mapDisp) {
this.mapDisp = new RequestObjectParamDispatcherMixin<>(klazz, paramMap, mapDisp);
String def = null;
verbMethodMap.put(HttpMethod.DELETE, new HashSet<String>());
verbMethodMap.put(HttpMethod.GET, new HashSet<String>());
verbMethodMap.put(HttpMethod.HEAD, new HashSet<String>());
verbMethodMap.put(HttpMethod.POST, new HashSet<String>());
verbMethodMap.put(HttpMethod.PUT, new HashSet<String>());
for(final Field f: klazz.getFields()) {
final String name = f.getName();
if(f.getAnnotation(HttpNone.class) != null) { continue; }
if(f.getAnnotation(HttpDefault.class) != null) {
if((def != null) && (f.getDeclaringClass().equals(klazz))) {
def = name;
} else if(def == null) {
def = name;
}
}
addHttpMethodAnnotations(verbMethodMap, f, name);
}
for(final Method m: klazz.getMethods()) {
final String name = m.getName();
final ParseParams p = new ParseParams(m, paramMap.get(name));
if(!p.nullOp) { this.requestParams.put(name, p); }
if(m.getAnnotation(HttpNone.class) != null) { continue; }
if(m.getAnnotation(HttpDefault.class) != null) {
if((def != null) && (m.getDeclaringClass().equals(klazz))) {
def = name;
} else if(def == null) {
def = name;
}
}
addHttpMethodAnnotations(verbMethodMap, m, name);
}
this.defaultMethod = def;
}
private static final void addHttpMethodAnnotations(final Map<HttpMethod, Set<String>> verbMethodMap,
final AnnotatedElement e, final String name) {
if((e.getAnnotation(HttpAny.class) != null) ||
((e.getAnnotation(HttpDelete.class) == null) &&
(e.getAnnotation(HttpGet.class) == null) &&
(e.getAnnotation(HttpHead.class) == null) &&
(e.getAnnotation(HttpPost.class) == null) &&
(e.getAnnotation(HttpPut.class) == null))) {
verbMethodMap.get(HttpMethod.DELETE).add(name);
verbMethodMap.get(HttpMethod.GET).add(name);
verbMethodMap.get(HttpMethod.HEAD).add(name);
verbMethodMap.get(HttpMethod.POST).add(name);
verbMethodMap.get(HttpMethod.PUT).add(name);
} else {
if(e.getAnnotation(HttpDelete.class) != null) { verbMethodMap.get(HttpMethod.DELETE).add(name); }
if(e.getAnnotation(HttpGet.class) != null) { verbMethodMap.get(HttpMethod.GET).add(name); }
if(e.getAnnotation(HttpHead.class) != null) { verbMethodMap.get(HttpMethod.HEAD).add(name); }
if(e.getAnnotation(HttpPost.class) != null) { verbMethodMap.get(HttpMethod.POST).add(name); }
if(e.getAnnotation(HttpPut.class) != null) { verbMethodMap.get(HttpMethod.PUT).add(name); }
}
}
@Override public Message<Unknown> dispatch(final RequestObject ro) {
final String resource = ro.getResource();
final String method = verbMethodMap.get(ro.getMethod()).contains(resource) ? resource : this.defaultMethod;
final Parse p = requestParams.get(method);
if(p != null) {
final Map<String,Object> params = new HashMap<>();
return mapDisp.dispatch(new Pair<String,Object>(method, new DelegatingRequestObject(ro, p.parse(params, ro))));
} else {
return mapDisp.dispatch(new Pair<String,Object>(method, ro));
}
}
}