/***
* Copyright (c) 2009 Caelum - www.caelum.com.br/opensource
* All rights reserved.
*
* 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.
*/
/***
*
* Copyright (c) 2009 Caelum - www.caelum.com.br/opensource All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer. 2. Redistributions in
* binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution. 3. Neither the name of the
* copyright holders nor the names of its contributors may be used to endorse or
* promote products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package br.com.caelum.vraptor.http.route;
import static java.util.Arrays.asList;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.enterprise.inject.Vetoed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import br.com.caelum.vraptor.Path;
import br.com.caelum.vraptor.controller.ControllerMethod;
import br.com.caelum.vraptor.controller.DefaultControllerMethod;
import br.com.caelum.vraptor.controller.HttpMethod;
import br.com.caelum.vraptor.core.Converters;
import br.com.caelum.vraptor.http.EncodingHandler;
import br.com.caelum.vraptor.http.Parameter;
import br.com.caelum.vraptor.http.ParameterNameProvider;
import br.com.caelum.vraptor.proxy.MethodInvocation;
import br.com.caelum.vraptor.proxy.Proxifier;
import br.com.caelum.vraptor.proxy.SuperMethod;
import br.com.caelum.vraptor.util.StringUtils;
import com.google.common.base.Joiner;
/**
* Should be used in one of two ways, either configure the type and invoke the
* method or pass the method (java reflection) object.
*
* If not specified, the built route will have the lowest priority (higher value
* of priority), so will be the last to be used.
*
* @author Guilherme Silveira
*/
@Vetoed
public class DefaultRouteBuilder implements RouteBuilder {
private static final Logger logger = LoggerFactory.getLogger(DefaultRouteBuilder.class);
private static final List<?> CHARACTER_TYPES = asList(char.class, Character.class);
private static final List<?> DECIMAL_TYPES = asList(Double.class, BigDecimal.class, double.class, Float.class, float.class);
private static final List<?> BOOLEAN_TYPES = asList(Boolean.class, boolean.class);
private static final List<?> NUMERIC_TYPES = asList(Integer.class, Long.class, int.class, long.class, BigInteger.class, Short.class, short.class);
private final Set<HttpMethod> supportedMethods = EnumSet.noneOf(HttpMethod.class);
private final DefaultParameterControlBuilder builder = new DefaultParameterControlBuilder();
private Route strategy = new NoStrategy();
private int priority = Path.LOWEST;
private final Proxifier proxifier;
private final TypeFinder finder;
private final Converters converters;
private final ParameterNameProvider nameProvider;
private final Evaluator evaluator;
private final String originalUri;
private final EncodingHandler encodingHandler;
public DefaultRouteBuilder(Proxifier proxifier, TypeFinder finder, Converters converters, ParameterNameProvider nameProvider,
Evaluator evaluator, String uri, EncodingHandler encodingHandler) {
this.proxifier = proxifier;
this.finder = finder;
this.converters = converters;
this.nameProvider = nameProvider;
this.evaluator = evaluator;
this.originalUri = uri;
this.encodingHandler = encodingHandler;
}
public class DefaultParameterControlBuilder implements ParameterControlBuilder {
private final Map<String, String> parameters = new HashMap<>();
private String name;
private DefaultParameterControlBuilder withParameter(String name) {
this.name = name;
return this;
}
@Override
public DefaultRouteBuilder ofType(Class<?> type) {
parameters.put(name, regexFor(type));
return DefaultRouteBuilder.this;
}
private String regexFor(Class<?> type) {
if (NUMERIC_TYPES.contains(type)) {
return "-?\\d+";
} else if (CHARACTER_TYPES.contains(type)){
return ".";
} else if (DECIMAL_TYPES.contains(type)) {
return "-?\\d*\\.?\\d+";
} else if (BOOLEAN_TYPES.contains(type)) {
return "true|false";
} else if (Enum.class.isAssignableFrom(type)) {
return Joiner.on("|").join(type.getEnumConstants());
}
return "[^/]+";
}
@Override
public DefaultRouteBuilder matching(String regex) {
parameters.put(name, regex);
return DefaultRouteBuilder.this;
}
private ParametersControl build() {
return new DefaultParametersControl(originalUri, parameters, converters, evaluator,encodingHandler);
}
}
@Override
public DefaultParameterControlBuilder withParameter(String name) {
return builder.withParameter(name);
}
@Override
public <T> T is(final Class<T> type) {
MethodInvocation<T> handler = new MethodInvocation<T>() {
@Override
public Object intercept(Object proxy, Method method, Object[] args, SuperMethod superMethod) {
boolean alreadySetTheStrategy = !strategy.getClass().equals(NoStrategy.class);
if (alreadySetTheStrategy) {
// the virtual machine might be invoking the finalize
return null;
}
is(type, method);
return null;
}
};
return proxifier.proxify(type, handler);
}
@Override
public void is(Class<?> type, Method method) {
addParametersInfo(method);
ControllerMethod controllerMethod = DefaultControllerMethod.instanceFor(type, method);
Parameter[] parameterNames = nameProvider.parametersFor(method);
this.strategy = getRouteStrategy(controllerMethod, parameterNames);
logger.info(String.format("%-50s%s -> %10s", originalUri,
this.supportedMethods.isEmpty() ? "[ALL]" : this.supportedMethods, method));
}
/**
* Override this method to change the default Route implementation
* @param controllerMethod The ControllerMethod
* @param parameterNames parameters of the method
* @return Route representation
*/
protected Route getRouteStrategy(ControllerMethod controllerMethod, Parameter[] parameterNames) {
return new FixedMethodStrategy(originalUri, controllerMethod, this.supportedMethods, builder.build(), priority, parameterNames);
}
private void addParametersInfo(Method method) {
String[] parameters = StringUtils.extractParameters(originalUri);
Map<String, Class<?>> types = finder.getParameterTypes(method, sanitize(parameters));
for (Entry<String, Class<?>> entry : types.entrySet()) {
if (!builder.parameters.containsKey(entry.getKey())) {
builder.withParameter(entry.getKey()).ofType(entry.getValue());
}
}
for (String parameter : parameters) {
String[] split = parameter.split(":");
if (split.length >= 2 && !builder.parameters.containsKey(parameter)) {
builder.withParameter(parameter).matching(split[1]);
}
}
}
private static String[] sanitize(String[] parameters) {
String[] sanitized = new String[parameters.length];
for (int i = 0; i < parameters.length; i++) {
sanitized[i] = parameters[i].replaceAll("(\\:.*|\\*)$", "");
}
return sanitized;
}
/**
* Accepts also this http method request. If this method is not invoked, any
* http method is supported, otherwise all parameters passed are supported.
*
* @param method
* @return
*/
@Override
public DefaultRouteBuilder with(HttpMethod method) {
this.supportedMethods.add(method);
return this;
}
@Override
public DefaultRouteBuilder with(Set<HttpMethod> methods) {
this.supportedMethods.addAll(methods);
return this;
}
/**
* Changes Route priority
*
* @param priority
* @return
*/
@Override
public DefaultRouteBuilder withPriority(int priority) {
this.priority = priority;
return this;
}
@Override
public Route build() {
if (strategy instanceof NoStrategy) {
throw new IllegalRouteException("You have created a route, but did not specify any method to be invoked: "
+ originalUri);
}
return strategy;
}
@Override
public String toString() {
if (supportedMethods.isEmpty()) {
return String.format("<< Route: %s => %s >>", originalUri, this.strategy.toString());
}
return String.format("<< Route: %s %s=> %s >>", originalUri, supportedMethods, this.strategy.toString());
}
}