/*
* #%L
* Wisdom-Framework
* %%
* Copyright (C) 2013 - 2014 Wisdom Framework
* %%
* 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.
* #L%
*/
package org.wisdom.router;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import org.wisdom.api.Controller;
import org.wisdom.api.annotations.Parameter;
import org.wisdom.api.http.MimeTypes;
import org.wisdom.api.router.RouteUtils;
import org.wisdom.api.router.parameters.ActionParameter;
import org.wisdom.api.router.parameters.Source;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Common logic shared by all web socket callbacks.
*/
public class DefaultWebSocketCallback {
private final Controller controller;
private final Method method;
private final Pattern regex;
private final ImmutableList<String> parameterNames;
protected final WebSocketRouter router;
protected List<ActionParameter> arguments;
/**
* Creates the callback object.
*
* @param controller the controller
* @param method the method to call
* @param uri the listened uri indicating when the callback need to be called, it can use the Wisdom's URI syntax
* to specify dynamic parts.
* @param router the web socket router
*/
public DefaultWebSocketCallback(Controller controller, Method method, String uri, WebSocketRouter router) {
this.router = router;
this.controller = controller;
this.method = method;
this.regex = Pattern.compile(RouteUtils.convertRawUriToRegex(uri));
this.parameterNames = ImmutableList.copyOf(RouteUtils.extractParameters(uri));
}
/**
* @return the controller.
*/
public Controller getController() {
return controller;
}
/**
* @return the method.
*/
public Method getMethod() {
return method;
}
/**
* @return the computed URI regular expression.
*/
public Pattern getRegex() {
return regex;
}
/**
* Creates the list of parameter for the given method. WebSocket callbacks can only use {@link org.wisdom.api
* .annotations.Parameter}. {@link org.wisdom.router.OnMessageWebSocketCallback} instances also support the
* {@link org.wisdom.api.annotations.Body} annotation to retrieve the payload.
* <p>
* If a method's parameter is not annotated, this method fails.
*
* @param method the method
* @return the list of parameter
*/
public List<ActionParameter> buildArguments(Method method) {
List<ActionParameter> args = new ArrayList<>();
Annotation[][] annotations = method.getParameterAnnotations();
Class<?>[] typesOfParameters = method.getParameterTypes();
Type[] genericTypeOfParameters = method.getGenericParameterTypes();
for (int i = 0; i < annotations.length; i++) {
boolean sourceDetected = false;
for (int j = 0; !sourceDetected && j < annotations[i].length; j++) {
Annotation annotation = annotations[i][j];
if (annotation instanceof Parameter) {
Parameter parameter = (Parameter) annotation;
args.add(new ActionParameter(parameter.value(),
Source.PARAMETER, typesOfParameters[i], genericTypeOfParameters[i]));
sourceDetected = true;
}
}
if (!sourceDetected) {
// All parameters must have been annotated.
WebSocketRouter.getLogger().error("The method {} has a parameter without annotations indicating " +
" the injected data. Only @Parameter annotations are supported in web sockets callbacks.",
method.getName()
);
return Collections.emptyList();
}
}
return args;
}
/**
* Checks whether the given url matches the computed URI regular expression.
*
* @param url the url
* @return {@code true} if the url matches, {@code false} otherwise
*/
public boolean matches(String url) {
return regex.matcher(url).matches();
}
/**
* Checks that the callback is well-formed.
*
* @return {@code true} if the callback is well-formed, {@code false} otherwise.
*/
public boolean check() {
if (!method.getReturnType().equals(Void.TYPE)) {
WebSocketRouter.getLogger().error("The method {} annotated with a web socket callback is not well-formed. " +
"These methods receive only parameter annotated with @Parameter and do not return anything",
method.getName()
);
return false;
}
List<ActionParameter> localArguments = buildArguments(method);
if (localArguments == null) {
return false;
} else {
this.arguments = localArguments;
return true;
}
}
/**
* Gets the map of parameter (name - value).
*
* @param uri the uri
* @return the map of parameter
*/
public Map<String, String> getPathParametersEncoded(String uri) {
Map<String, String> map = Maps.newHashMap();
Matcher m = regex.matcher(uri);
if (m.matches()) {
for (int i = 1; i < m.groupCount() + 1; i++) {
map.put(parameterNames.get(i - 1), m.group(i));
}
}
return map;
}
/**
* Invokes the callback.
*
* @param uri the uri
* @param client the client identifier (the one having sent the message)
* @param content the payload of the message
* @throws InvocationTargetException when the callback throws an exception
* @throws IllegalAccessException when the callback cannot be called
*/
public void invoke(String uri, String client, byte[] content) throws
InvocationTargetException,
IllegalAccessException {
Map<String, String> values = getPathParametersEncoded(uri);
Object[] parameters = new Object[arguments.size()];
for (int i = 0; i < arguments.size(); i++) {
ActionParameter argument = arguments.get(i);
if (argument.getSource() == Source.PARAMETER) {
if (argument.getName().equals("client") && argument.getRawType().equals(String.class)) {
parameters[i] = client;
} else {
parameters[i] = router.converter().convertValue(values.get(argument.getName()),
argument.getRawType(), argument.getGenericType(), argument.getDefaultValue());
}
} else {
// Body
parameters[i] = transform(argument, content);
}
}
getMethod().invoke(getController(), parameters);
}
private Object transform(ActionParameter parameter, byte[] content) {
String data = new String(content, Charset.defaultCharset());
try {
return router.converter().convertValue(data, parameter.getRawType(), parameter.getGenericType(), null);
} catch (IllegalArgumentException | NoSuchElementException e) { //NOSONAR
// The NoSuchElementException is thrown when there are no suitable converter,
// while the IllegalArgumentException is thrown when the conversion fails. In both case,
// the conversion failed.
}
// For all the other cases, we need a binder, however, we have no idea about the type of message,
// for now we suppose it's json.
return router.engine().getBodyParserEngineForContentType(MimeTypes.JSON).invoke(content, parameter.getRawType());
}
}