![JAR search and dependency download from the Maven repository](/logo.png)
org.wicketstuff.rest.resource.AbstractRestResource Maven / Gradle / Ivy
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.wicketstuff.rest.resource;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.wicket.Application;
import org.apache.wicket.Session;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.authroles.authorization.strategies.role.IRoleCheckingStrategy;
import org.apache.wicket.authroles.authorization.strategies.role.Roles;
import org.apache.wicket.request.Url;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.request.http.WebResponse;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.request.resource.IResource;
import org.apache.wicket.util.collections.MultiMap;
import org.apache.wicket.util.convert.ConversionException;
import org.apache.wicket.util.convert.IConverter;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.validation.IErrorMessageSource;
import org.apache.wicket.validation.IValidationError;
import org.apache.wicket.validation.IValidator;
import org.apache.wicket.validation.Validatable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wicketstuff.rest.annotations.AuthorizeInvocation;
import org.wicketstuff.rest.annotations.MethodMapping;
import org.wicketstuff.rest.contenthandling.IWebSerialDeserial;
import org.wicketstuff.rest.resource.urlsegments.AbstractURLSegment;
import org.wicketstuff.rest.resource.urlsegments.visitor.ScoreMethodAndExtractPathVars;
import org.wicketstuff.rest.utils.collection.CollectionUtils;
import org.wicketstuff.rest.utils.http.HttpMethod;
import org.wicketstuff.rest.utils.http.HttpUtils;
import org.wicketstuff.rest.utils.reflection.MethodParameter;
import org.wicketstuff.rest.utils.reflection.ReflectionUtils;
import org.wicketstuff.rest.utils.wicket.AttributesWrapper;
import org.wicketstuff.rest.utils.wicket.MethodParameterContext;
import org.wicketstuff.rest.utils.wicket.bundle.DefaultBundleResolver;
/**
* Base class to build a resource that serves REST requests.
*
* @author andrea del bene
*
*/
public abstract class AbstractRestResource implements IResource
{
private static final long serialVersionUID = 1L;
public static final String NO_SUITABLE_METHOD_FOUND = "No suitable method found.";
public static final String USER_IS_NOT_ALLOWED = "User is not allowed to use this resource.";
private static final Logger log = LoggerFactory.getLogger(AbstractRestResource.class);
/**
* HashMap that stores every mapped method of the class. Mapped method are stored concatenating
* the number of the segments of their URL and their HTTP method (see annotation MethodMapping)
*/
private final Map> mappedMethods;
/**
* Another HashMap that stores every mapped method of the class.
* The key of the map is {@link Method}.
*/
private final Map mappedMethodsInfo;
/**
* HashMap that stores the validators registered by the resource.
*/
private final Map> declaredValidators = new HashMap<>();
/**
* The implementation of {@link IWebSerialDeserial} that is used to serialize/desiarilze objects
* to/from string (for example to/from JSON)
*/
private final T webSerialDeserial;
/** Role-checking strategy. */
private final IRoleCheckingStrategy roleCheckingStrategy;
/** Bundle resolver */
private final IErrorMessageSource bundleResolver;
/**
* Constructor with no role-checker (i.e we don't use annotation {@link AuthorizeInvocation}).
*
* @param serialDeserial
* General class that is used to serialize/desiarilze objects to string.
*/
public AbstractRestResource(T serialDeserial)
{
this(serialDeserial, null);
}
/**
* Main constructor that takes in input the object serializer/deserializer and the role-checking
* strategy to use.
*
* @param serialDeserial
* General class that is used to serialize/desiarilze objects to string
* @param roleCheckingStrategy
* the role-checking strategy.
*/
public AbstractRestResource(T serialDeserial, IRoleCheckingStrategy roleCheckingStrategy)
{
Args.notNull(serialDeserial, "serialDeserial");
onInitialize(serialDeserial);
this.webSerialDeserial = serialDeserial;
this.roleCheckingStrategy = roleCheckingStrategy;
this.mappedMethods = loadAnnotatedMethods();
this.mappedMethodsInfo = loadAnnotatedMethodsInfo();
this.bundleResolver = new DefaultBundleResolver(loadBoundleClasses());
}
/**
* Build a list of classes to use to search for a valid bundle. This list is
* made of the classes of the validators registered with abstractResource
* and of the class of the abstractResource.
*
* @param abstractResource
* the abstract REST resource that is using the validator
* @return the list of the classes to use.
*/
private List> loadBoundleClasses()
{
Collection> validators = declaredValidators.values();
List> validatorsClasses = ReflectionUtils.getElementsClasses(validators);
validatorsClasses.add(this.getClass());
return validatorsClasses;
}
/***
* Handles a REST request invoking one of the methods annotated with {@link MethodMapping}. If
* the annotated method returns a value, this latter is automatically serialized to a given
* string format (like JSON, XML, etc...) and written to the web response.
* If no method is found to serve the current request, a 400 HTTP code is returned to the
* client. Similarly, a 401 HTTP code is return if the user doesn't own one of the roles
* required to execute an annotated method (See {@link AuthorizeInvocation}).
*
* @param attributes
* the Attribute object of the current request
*/
@Override
public final void respond(Attributes attributes)
{
AttributesWrapper attributesWrapper = new AttributesWrapper(attributes);
WebResponse response = attributesWrapper.getWebResponse();
HttpMethod httpMethod = attributesWrapper.getHttpMethod();
// select the best "candidate" method to serve the request
ScoreMethodAndExtractPathVars mappedMethod = selectMostSuitedMethod(attributesWrapper);
if (mappedMethod != null)
{
handleMethodExecution(attributesWrapper, mappedMethod);
}
else
{
noSuitableMethodFound(response, httpMethod);
}
}
/**
* Handle the different steps (authorization, validation, etc...) involved in method execution.
*
* @param attributesWrapper
* wrapper for the current Attributes
* @param mappedMethod
* the mapped method to execute
*/
private void handleMethodExecution(AttributesWrapper attributesWrapper,
ScoreMethodAndExtractPathVars mappedMethod)
{
WebResponse response = attributesWrapper.getWebResponse();
MethodMappingInfo methodInfo = mappedMethod.getMethodInfo();
Attributes attributes = attributesWrapper.getOriginalAttributes();
String outputFormat = methodInfo.getOutputFormat();
// 1-check if user is authorized to invoke the method
if (!isUserAuthorized(methodInfo.getRoles()))
{
unauthorizedMethodAccess(response, methodInfo);
return;
}
// 2-extract method parameters
List> parametersValues = null;
try
{
parametersValues = extractMethodParameters(mappedMethod, attributesWrapper);
}
catch (RuntimeException e)
{
setResponseStatusCode(400);
return;
}
// 3-validate method parameters
List validationErrors = validateMethodParameters(methodInfo,
parametersValues);
if (validationErrors.size() > 0)
{
IValidationError error = validationErrors.get(0);
Serializable message = error.getErrorMessage(bundleResolver);
webSerialDeserial.objectToResponse(message, response, outputFormat);
response.setStatus(400);
return;
}
// 4-invoke method triggering the before-after hooks
onBeforeMethodInvoked(methodInfo, attributes);
Object result = invokeMappedMethod(methodInfo.getMethod(), parametersValues, response);
onAfterMethodInvoked(methodInfo, attributes, result);
// 5-set response content type
response.setContentType(outputFormat);
// 6-if the invoked method returns a value, it is written to response
if (result != null)
{
objectToResponse(result, response, outputFormat);
}
}
/**
* Check if user is allowed to run a method annotated with {@link AuthorizeInvocation}
*
* @param roles
* the user roles
* @return true if user is allowed, else otherwise
*/
private boolean isUserAuthorized(Roles roles)
{
if (roles.isEmpty())
{
return true;
}
else
{
return roleCheckingStrategy.hasAnyRole(roles);
}
}
/**
* Handle unauthorized user access. Default: set response Status to 401. Override this method to implement
* customized handling
* @param response Response-Object
* @param methodInfo The mapped method
*/
protected void unauthorizedMethodAccess(WebResponse response, MethodMappingInfo methodInfo)
{
response.write(USER_IS_NOT_ALLOWED);
response.setStatus(401);
}
/**
* This method can be used to write a standard error message to the current response object when
* no mapped method has been found for the current request.
*
* @param response
* the current response object
* @param httpMethod
* the HTTP method of the current request
*/
public void noSuitableMethodFound(WebResponse response, HttpMethod httpMethod)
{
response.setStatus(400);
response.write(NO_SUITABLE_METHOD_FOUND + " URL '" + extractUrlFromRequest() +
"' and HTTP method " + httpMethod);
}
/**
* Validate parameter values of the mapped method we want to execute.
*
* @param mappedMethod
* the target mapped methos
* @param parametersValues
* the parameter values
* @return the list of validation errors, it is empty if validation succeeds
*/
private List validateMethodParameters(MethodMappingInfo mappedMethod,
List> parametersValues)
{
List> methodParameters = mappedMethod.getMethodParameters();
List errors = new ArrayList();
for (MethodParameter> methodParameter : methodParameters)
{
String validatorKey = methodParameter.getValdatorKey();
if (!Strings.isEmpty(validatorKey))
{
int i = methodParameters.indexOf(methodParameter);
Object parameterValue = parametersValues.get(i);
validateMethodParameter(errors, validatorKey,
parameterValue);
}
}
return errors;
}
/**
* Validate a single parameter value of the mapped method we want to execute.
*
* @param errors
* the list of validation errors
* @param validatorKey
* the key for the current validator
* @param parameterValue
* the value for the current parameter
*/
private void validateMethodParameter(List errors,
String validatorKey, E parameterValue)
{
IValidator validator = getValidator(validatorKey, parameterValue);
Validatable validatable = new Validatable<>(parameterValue);
if (validator != null)
{
validator.validate(validatable);
errors.addAll(validatable.getErrors());
}
else
{
log.debug("No validator found for key '" + validatorKey + "'");
}
}
/**
* Invoked just before a mapped method is invoked to serve the current request.
*
* @param mappedMethod
* the mapped method.
* @param attributes
* the current Attributes object.
*/
protected void onBeforeMethodInvoked(MethodMappingInfo mappedMethod, Attributes attributes)
{
}
/**
* Invoked just after a mapped method has been invoked to serve the current request.
*
* @param mappedMethod
* the mapped method.
* @param attributes
* the current Attributes object.
* @param result
* the value returned by the invoked method.
*/
protected void onAfterMethodInvoked(MethodMappingInfo mappedMethod, Attributes attributes,
Object result)
{
}
/**
* Method invoked to serialize the result of the invoked method and write this value to the
* response.
*
* @param response
* The current response object.
* @param result
* The object to write to response.
* @param restMimeFormats
* The MIME type to use to serialize data
*/
public void objectToResponse(Object result, WebResponse response, String mimeType)
{
try
{
webSerialDeserial.objectToResponse(result, response, mimeType);
}
catch (Exception e)
{
throw new RuntimeException("Error writing object to response.", e);
}
}
/**
* Method invoked to select the most suited method to serve the current request.
*
* @param attributesWrapper
* the current attribute wrapper
* @return The "best" method found to serve the request.
*/
private ScoreMethodAndExtractPathVars selectMostSuitedMethod(AttributesWrapper attributesWrapper)
{
PageParameters pageParameters = attributesWrapper.getPageParameters();
List mappedMethodsCandidates = mappedMethods.get(pageParameters.getIndexedCount() +
"_" + attributesWrapper.getHttpMethod());
ScoreMethodAndExtractPathVars highiestScoredMethod = null;
// no method mapped
if (mappedMethodsCandidates == null || mappedMethodsCandidates.size() == 0)
return null;
/**
* To select the "best" method, a score is assigned to every mapped method. To calculate the
* score method calculateScore is executed for every segment.
*/
int highestScore = 0;
for (MethodMappingInfo mappedMethod : mappedMethodsCandidates)
{
ScoreMethodAndExtractPathVars scoredMethod =
new ScoreMethodAndExtractPathVars(mappedMethod, pageParameters);
for (AbstractURLSegment segment : mappedMethod.getSegments())
{
segment.accept(scoredMethod);
if(!scoredMethod.isSegmentValid())
{
break;
}
}
if(highestScore > 0 && scoredMethod.getScore() == highestScore)
{
// if we have more than one method with the highest score, throw
// ambiguous exception.
throwAmbiguousMethodsException(scoredMethod, highiestScoredMethod);
}
if (scoredMethod.getScore() >= highestScore)
{
highestScore = scoredMethod.getScore();
highiestScoredMethod = scoredMethod;
}
}
return highiestScoredMethod;
}
/**
* Throw an exception if two o more methods have the same "score" for the current request. See
* method selectMostSuitedMethod.
*
* @param list
* the list of ambiguous methods.
*/
private void throwAmbiguousMethodsException(ScoreMethodAndExtractPathVars... methods)
{
WebRequest request = getCurrentWebRequest();
String methodsNames = "";
for (ScoreMethodAndExtractPathVars method : methods)
{
if (!methodsNames.isEmpty())
methodsNames += ", ";
MethodMappingInfo urlMappingInfo = method.getMethodInfo();
methodsNames += urlMappingInfo.getMethod().getName();
}
throw new WicketRuntimeException("Ambiguous methods mapped for the current request: URL '" +
request.getClientUrl() + "', HTTP method " + HttpUtils.getHttpMethod(request) + ". " +
"Mapped methods: " + methodsNames);
}
/**
* Method called to initialize and configure the resource.
*
* @param objSerialDeserial
* the object serializer/deserializer
*/
protected void onInitialize(T objSerialDeserial)
{
}
/***
* Internal method to load class methods annotated with {@link MethodMapping}
*
* @return
* a map object contained annotated method. Mapped method are stored concatenating
* the number of the segments of their URL and their HTTP method (see annotation MethodMapping)
*/
private Map> loadAnnotatedMethods()
{
Method[] methods = getClass().getDeclaredMethods();
MultiMap mappedMethods = new MultiMap();
boolean isUsingAuthAnnot = false;
for (int i = 0; i < methods.length; i++)
{
Method method = methods[i];
MethodMapping methodMapped = method.getAnnotation(MethodMapping.class);
AuthorizeInvocation authorizeInvocation = method.getAnnotation(AuthorizeInvocation.class);
isUsingAuthAnnot = isUsingAuthAnnot || authorizeInvocation != null;
if (methodMapped != null)
{
HttpMethod httpMethod = methodMapped.httpMethod();
MethodMappingInfo methodMappingInfo = new MethodMappingInfo(methodMapped, method);
if (!webSerialDeserial.isMimeTypeSupported(methodMappingInfo.getInputFormat()) ||
!webSerialDeserial.isMimeTypeSupported(methodMappingInfo.getOutputFormat()))
throw new WicketRuntimeException(
"Mapped methods use a MIME type not supported by obj serializer/deserializer!");
mappedMethods.addValue(
methodMappingInfo.getSegmentsCount() + "_" + httpMethod.getMethod(),
methodMappingInfo);
}
}
// if AuthorizeInvocation has been found but no role-checker has been
// configured, throw an exception
if (isUsingAuthAnnot && roleCheckingStrategy == null)
throw new WicketRuntimeException(
"Annotation AuthorizeInvocation is used but no role-checking strategy has been set for the controller!");
return CollectionUtils.makeListMapImmutable(mappedMethods);
}
private Map loadAnnotatedMethodsInfo()
{
Map methodsInfo = new HashMap();
for (List methodInfoList : mappedMethods.values())
{
for (MethodMappingInfo methodMappedInfo : methodInfoList)
{
methodsInfo.put(methodMappedInfo.getMethod(), methodMappedInfo);
}
}
return Collections.unmodifiableMap(methodsInfo);
}
/***
* Invokes one of the resource methods annotated with {@link MethodMapping}.
*
* @param mappedMethod
* mapping info of the method.
* @param attributesWrapper
* Attributes wrapper for the current request.
* @return the value returned by the invoked method
*/
private List> extractMethodParameters(ScoreMethodAndExtractPathVars mappedMethod,
AttributesWrapper attributesWrapper)
{
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy