/**
* Abiquo community edition
* cloud management application for hybrid clouds
* Copyright (C) 2008-2010 - Abiquo Holdings S.L.
*
* This application is free software; you can redistribute it and/or
* modify it under the terms of the GNU LESSER GENERAL PUBLIC
* LICENSE as published by the Free Software Foundation under
* version 3 of the License
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* LESSER GENERAL PUBLIC LICENSE v.3 for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
/**
*
*/
package com.abiquo.api.wink;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import org.apache.wink.common.RuntimeContext;
import org.apache.wink.common.http.HttpHeadersEx;
import org.apache.wink.common.http.HttpStatus;
import org.apache.wink.common.internal.application.ApplicationValidator;
import org.apache.wink.common.internal.i18n.Messages;
import org.apache.wink.common.internal.lifecycle.LifecycleManagersRegistry;
import org.apache.wink.common.internal.registry.Injectable;
import org.apache.wink.common.internal.registry.metadata.MethodMetadata;
import org.apache.wink.common.internal.utils.HeaderUtils;
import org.apache.wink.common.internal.utils.MediaTypeUtils;
import org.apache.wink.server.internal.registry.MethodMetadataRecord;
import org.apache.wink.server.internal.registry.MethodRecord;
import org.apache.wink.server.internal.registry.ResourceInstance;
import org.apache.wink.server.internal.registry.ResourceRegistry;
import org.apache.wink.server.internal.registry.SubResourceInstance;
import org.apache.wink.server.internal.registry.SubResourceMethodRecord;
import org.apache.wink.server.internal.registry.SubResourceRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author jaume
*/
public class AbiquoResourceRegistry extends ResourceRegistry
{
private static final Logger logger = LoggerFactory.getLogger(ResourceRegistry.class);
public AbiquoResourceRegistry(LifecycleManagersRegistry factoryRegistry,
ApplicationValidator applicationValidator)
{
super(factoryRegistry, applicationValidator);
}
/**
* Attempts to find a resource method to invoke in the specified resource
*
* @param resource the resource to find the method in
* @param variablesValues a multivalued map of variables values that stores the variables of the
* templates that are matched during the search
* @param context the context of the current request
* @return
*/
@Override
public MethodRecord findMethod(ResourceInstance resource, RuntimeContext context)
throws WebApplicationException
{
List<MethodMetadata> methods = resource.getRecord().getMetadata().getResourceMethods();
List<MethodMetadataRecord> records = new LinkedList<MethodMetadataRecord>();
for (MethodMetadata metadata : methods)
{
records.add(new MethodMetadataRecord(metadata));
}
// filter the methods according to the spec
filterDispatchMethods(resource, records, context);
// select the best matching method
return selectBestMatchingMethod(records, context);
}
/**
* Attempts to find a sub-resource method to invoke in the specified resource
*
* @param pattern the regex pattern that the 'path' specified on the sub-resource method must
* match
* @param subResourceRecords a list of all the sub-resources (methods and locators) in the
* specified resource that matched the request
* @param resource the resource to find the method in
* @param variablesValues a multivalued map of variables values that stores the variables of the
* templates that are matched during the search
* @param context the context of the current request
* @return
*/
public SubResourceInstance findSubResourceMethod(String pattern,
List<SubResourceInstance> subResourceRecords, ResourceInstance resource,
RuntimeContext context) throws WebApplicationException
{
// extract the sub-resource methods that have the same path template
// as the first sub-resource method
List<SubResourceInstance> subResourceMethods =
extractSubResourceMethods(pattern, subResourceRecords);
// filter the methods according to http method/consumes/produces
filterDispatchMethods(resource, subResourceMethods, context);
// select the best matching method
return (SubResourceInstance) selectBestMatchingMethod(subResourceMethods, context);
}
/**
* Compare two methods in terms of "which is a better match to the request". First a match to
* the input media type is compared, then a match to the Accept header media types is compared
*
* @param record1 the first method
* @param record2 the second method
* @param inputMediaType the input entity media type
* @param acceptableMediaTypes the media types of the Accept header
* @return positive integer if record1 is a better match, negative integer if record2 is a
* better match, 0 if they are equal in matching terms
*/
private int compareMethods(MethodRecord record1, MethodRecord record2,
MediaType inputMediaType, List<MediaType> acceptableMediaTypes)
{
if (record1 == null && record2 == null)
{
return 0;
}
if (record1 != null && record2 == null)
{
return 1;
}
if (record1 == null && record2 != null)
{
return -1;
}
// compare consumes
int res = compareMethodsConsumes(record1, record2, inputMediaType);
if (res != 0)
{
return res;
}
// compare produces
for (MediaType outputMediaType : acceptableMediaTypes)
{
res = compareMethodsProduces(record1, record2, outputMediaType);
if (res != 0)
{
return res;
}
}
// this is just to make the search a bit more deterministic,
// and it should remain undocumented and application implementors
// should not rely on this behavior (i.e. comparing the number of
// parameters)
return compareMethodsParameters(record1, record2);
}
/**
* Compare two methods in terms of matching to the input media type
*
* @param record1 the first method
* @param record2 the second method
* @param inputMediaType the input entity media type
* @return positive integer if record1 is a better match, negative integer if record2 is a
* better match, 0 if they are equal in matching terms
*/
private int compareMethodsConsumes(MethodRecord record1, MethodRecord record2,
MediaType inputMediaType)
{
// get media type of metadata 1 best matching the input media type
MediaType bestMatch1 =
selectBestMatchingMediaType(inputMediaType, record1.getMetadata().getConsumes());
// get media type of metadata 2 best matching the input media type
MediaType bestMatch2 =
selectBestMatchingMediaType(inputMediaType, record2.getMetadata().getConsumes());
if (bestMatch1 == null && bestMatch2 == null)
{
return 0;
}
if (bestMatch1 != null && bestMatch2 == null)
{
return 1;
}
if (bestMatch1 == null && bestMatch2 != null)
{
return -1;
}
int retVal = MediaTypeUtils.compareTo(bestMatch1, bestMatch2);
if (retVal != 0)
{
return retVal;
}
Map<String, String> inputParameters = inputMediaType.getParameters();
Map<String, String> bestMatch1Params = bestMatch1.getParameters();
boolean didMatchAllParamsForBestMatch1 = true;
for (String key : bestMatch1Params.keySet())
{
String inputValue = inputParameters.get(key);
String value1 = bestMatch1Params.get(key);
if (!value1.equals(inputValue))
{
didMatchAllParamsForBestMatch1 = false;
break;
}
}
Map<String, String> bestMatch2Params = bestMatch2.getParameters();
boolean didMatchAllParamsForBestMatch2 = true;
for (String key : bestMatch2Params.keySet())
{
String inputValue = inputParameters.get(key);
String value2 = bestMatch2Params.get(key);
if (!value2.equals(inputValue))
{
didMatchAllParamsForBestMatch2 = false;
break;
}
}
if (didMatchAllParamsForBestMatch1 && !didMatchAllParamsForBestMatch2)
{
return 1;
}
if (!didMatchAllParamsForBestMatch1 && didMatchAllParamsForBestMatch2)
{
return -1;
}
if (didMatchAllParamsForBestMatch1 && didMatchAllParamsForBestMatch2)
{
int size1 = bestMatch1Params.size();
int size2 = bestMatch2Params.size();
if (size1 > size2)
{
return 1;
}
else if (size2 > size1)
{
return -1;
}
}
return 0;
}
/**
* Compares two methods the in terms of the number of parameters
*
* @param record1 the first method
* @param record2 the second method
* @return positive integer if record1 has more parameters, negative integer if record2 has more
* parameters, 0 if they are equal in number of parameters
*/
private int compareMethodsParameters(MethodRecord record1, MethodRecord record2)
{
List<Injectable> params1 = record1.getMetadata().getFormalParameters();
List<Injectable> params2 = record2.getMetadata().getFormalParameters();
return params1.size() - params2.size();
}
/**
* Compare two methods in terms of matching to the Accept header media types
*
* @param record1 the first method
* @param record2 the second method
* @param acceptableMediaTypes the media types of the Accept header
* @return positive integer if record1 is a better match, negative integer if record2 is a
* better match, 0 if they are equal in matching terms
*/
private int compareMethodsProduces(MethodRecord record1, MethodRecord record2,
MediaType outputMediaType)
{
// the acceptMediaTypes list is already sorted according to preference
// of media types,
// so we need to stop with the first media type that has a match
// get media type of metadata 1 best matching the input media type
MediaType bestMatch1 =
selectBestMatchingMediaType(outputMediaType, record1.getMetadata().getProduces());
// get media type of metadata 2 best matching the input media type
MediaType bestMatch2 =
selectBestMatchingMediaType(outputMediaType, record2.getMetadata().getProduces());
if (bestMatch1 == null && bestMatch2 == null)
{
return 0;
}
if (bestMatch1 != null && bestMatch2 == null)
{
return 1;
}
if (bestMatch1 == null && bestMatch2 != null)
{
return -1;
}
int retVal = MediaTypeUtils.compareTo(bestMatch1, bestMatch2);
if (retVal != 0)
{
return retVal;
}
Map<String, String> inputParameters = outputMediaType.getParameters();
Map<String, String> bestMatch1Params = bestMatch1.getParameters();
boolean didMatchAllParamsForBestMatch1 = true;
for (String key : bestMatch1Params.keySet())
{
String inputValue = inputParameters.get(key);
String value1 = bestMatch1Params.get(key);
if (!value1.equals(inputValue))
{
didMatchAllParamsForBestMatch1 = false;
break;
}
}
Map<String, String> bestMatch2Params = bestMatch2.getParameters();
boolean didMatchAllParamsForBestMatch2 = true;
for (String key : bestMatch2Params.keySet())
{
String inputValue = inputParameters.get(key);
String value2 = bestMatch2Params.get(key);
if (!value2.equals(inputValue))
{
didMatchAllParamsForBestMatch2 = false;
break;
}
}
if (didMatchAllParamsForBestMatch1 && !didMatchAllParamsForBestMatch2)
{
return 1;
}
if (!didMatchAllParamsForBestMatch1 && didMatchAllParamsForBestMatch2)
{
return -1;
}
if (didMatchAllParamsForBestMatch1 && didMatchAllParamsForBestMatch2)
{
int size1 = bestMatch1Params.size();
int size2 = bestMatch2Params.size();
if (size1 > size2)
{
return 1;
}
else if (size2 > size1)
{
return -1;
}
}
return 0;
}
/**
* Extract the sub-resource methods from the specified list that have the same path pattern as
* the specified pattern
*
* @param pattern
* @param subResourceRecords
* @return a list of sub-resource methods whose 'path' pattern match the specified pattern
*/
private List<SubResourceInstance> extractSubResourceMethods(String pattern,
List<SubResourceInstance> subResourceRecords)
{
List<SubResourceInstance> subResourceMethods = new LinkedList<SubResourceInstance>();
for (SubResourceInstance instance : subResourceRecords)
{
SubResourceRecord record = instance.getRecord();
String recordPattern = record.getTemplateProcessor().getPatternString();
if (record instanceof SubResourceMethodRecord && recordPattern.equals(pattern))
{
subResourceMethods.add(instance);
}
}
return subResourceMethods;
}
/**
* Checks if the method record matches the media type of the input entity
*
* @param record the method record to check
* @param context the context of the current request
* @return true if the method should be filtered, false otherwise
*/
private boolean filterByConsumes(MethodRecord record, RuntimeContext context)
{
Set<MediaType> consumedMimes = record.getMetadata().getConsumes();
// if not specified, then treat as if consumes */*
if (consumedMimes.size() == 0)
{
return false;
}
MediaType inputMediaType = context.getHttpHeaders().getMediaType();
if (inputMediaType == null)
{
inputMediaType = MediaType.APPLICATION_OCTET_STREAM_TYPE;
}
for (MediaType mediaType : consumedMimes)
{
if (mediaType.isCompatible(inputMediaType))
{
return false;
}
}
return true;
}
/**
* Checks if the method record matches the http method of the request
*
* @param record the method record to check
* @param context the context of the current request
* @return true if the method should be filtered, false otherwise
*/
private boolean filterByHttpMethod(MethodRecord record, RuntimeContext context)
{
String httpMethod = context.getRequest().getMethod();
String recordHttpMethod = record.getMetadata().getHttpMethod();
// non existing http method (with a path on the method),
// then it's a sub-resource locator and it's ok
if (recordHttpMethod == null)
{
return false;
}
// the http method is different than the request http method,
// then the resource method should be filtered
if (!recordHttpMethod.equals(httpMethod))
{
return true;
}
return false;
}
/**
* Checks if the method record matches the Accept header of the request
*
* @param record the method record to check
* @param context the context of the current request
* @return true if the method should be filtered, false otherwise
*/
private boolean filterByProduces(MethodRecord record, RuntimeContext context)
{
Set<MediaType> producedMimes = record.getMetadata().getProduces();
// if not specified, then treat as if produces */*
if (producedMimes.size() == 0)
{
return false;
}
List<MediaType> receivedMediaTypes = context.getHttpHeaders().getAcceptableMediaTypes();
// if no accept media type was specified
if (receivedMediaTypes.size() == 0)
{
return false;
}
List<MediaType> deniableMediaTypes = new ArrayList<MediaType>();
List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
for (MediaType received : receivedMediaTypes)
{
String q = received.getParameters().get("q"); //$NON-NLS-1$
if (q != null && Double.valueOf(q).equals(0.0))
{
deniableMediaTypes.add(received);
}
else
{
acceptableMediaTypes.add(received);
}
}
l1: for (MediaType mediaType : producedMimes)
{
for (MediaType deniable : deniableMediaTypes)
{
if (MediaTypeUtils.isCompatibleNonCommutative(deniable, mediaType))
{
continue l1;
}
}
for (MediaType acceptableMediaType : acceptableMediaTypes)
{
if (mediaType.isCompatible(acceptableMediaType))
{
return false;
}
}
}
return true;
}
/**
* Filter the methods that do not conform to the current request by modifying the input list:
* <ol>
* <li>Filter all methods that do not match the http method of the request. If a method does not
* have an http method, it passes the filter.</li>
* <li>Filter all methods that do not match the media type of the input entity. If a method does
* not have the @Consumes annotation, it passes the filter</li>
* <li>Filter all methods that do not match the Accept header. If a method does not have the @Produces
* annotation, it passes the filter</li>
* </ol>
* The elements in the list remain in the same order
*
* @param resource
* @param methodRecords a list of method records to filter according to the request context
* @param context the context of the current request
* @return
*/
private void filterDispatchMethods(ResourceInstance resource,
List< ? extends MethodRecord> methodRecords, RuntimeContext context)
throws WebApplicationException
{
// filter by http method
ListIterator< ? extends MethodRecord> iterator = methodRecords.listIterator();
while (iterator.hasNext())
{
MethodRecord record = iterator.next();
if (filterByHttpMethod(record, context))
{
iterator.remove();
}
}
if (methodRecords.size() == 0)
{
logger.info(Messages.getMessage("noMethodInClassSupportsHTTPMethod"), resource
.getResourceClass().getName(), context.getRequest().getMethod());
Set<String> httpMethods = getOptions(resource);
ResponseBuilder builder = Response.status(HttpStatus.METHOD_NOT_ALLOWED.getCode());
// add 'Allow' header to the response
String allowHeader = HeaderUtils.buildOptionsHeader(httpMethods);
builder.header(HttpHeadersEx.ALLOW, allowHeader);
throw new WebApplicationException(builder.build());
}
// filter by consumes
iterator = methodRecords.listIterator();
while (iterator.hasNext())
{
MethodRecord record = iterator.next();
if (filterByConsumes(record, context))
{
iterator.remove();
}
}
if (methodRecords.size() == 0)
{
logger.info(Messages.getMessage("noMethodInClassConsumesHTTPMethod", resource //$NON-NLS-1$
.getResourceClass().getName(), context.getHttpHeaders().getMediaType()));
throw new WebApplicationException(Response.Status.UNSUPPORTED_MEDIA_TYPE);
}
// filter by produces
iterator = methodRecords.listIterator();
while (iterator.hasNext())
{
MethodRecord record = iterator.next();
if (filterByProduces(record, context))
{
iterator.remove();
}
}
if (methodRecords.size() == 0)
{
logger.info(Messages.getMessage("noMethodInClassProducesHTTPMethod", resource //$NON-NLS-1$
.getResourceClass().getName(),
context.getHttpHeaders().getRequestHeader(HttpHeaders.ACCEPT)));
throw new WebApplicationException(Response.Status.NOT_ACCEPTABLE);
}
}
/**
* Select the media type from the list of media types that best matches the specified media type
*
* @param mediaType the media type to match against
* @param mediaTypes the list of media types to select the best match from
* @return the best matching media type from the list
*/
private MediaType selectBestMatchingMediaType(MediaType mediaType, Set<MediaType> mediaTypes)
{
MediaType bestMatch = null;
for (MediaType mt : mediaTypes)
{
if (!mt.isCompatible(mediaType))
{
continue;
}
if (bestMatch == null)
{
bestMatch = mt;
continue;
}
int ret = MediaTypeUtils.compareTo(mt, bestMatch);
if (ret > 0)
{
bestMatch = mt;
continue;
}
if (ret == 0)
{
Map<String, String> desiredParameters = mediaType.getParameters();
Map<String, String> currentValueParams = mt.getParameters();
Map<String, String> bestMatchParams = bestMatch.getParameters();
boolean didMatchAllParamsForBestMatch1 = true;
for (String key : currentValueParams.keySet())
{
String inputValue = desiredParameters.get(key);
String value1 = currentValueParams.get(key);
if (!value1.equals(inputValue))
{
didMatchAllParamsForBestMatch1 = false;
break;
}
}
boolean didMatchAllParamsForBestMatch2 = true;
for (String key : bestMatchParams.keySet())
{
String inputValue = desiredParameters.get(key);
String value2 = bestMatchParams.get(key);
if (!value2.equals(inputValue))
{
didMatchAllParamsForBestMatch2 = false;
break;
}
}
if (didMatchAllParamsForBestMatch1 && !didMatchAllParamsForBestMatch2)
{
bestMatch = mt;
continue;
}
if (!didMatchAllParamsForBestMatch1 && didMatchAllParamsForBestMatch2)
{
continue;
}
if (didMatchAllParamsForBestMatch1 && didMatchAllParamsForBestMatch2)
{
int size1 = currentValueParams.size();
int size2 = bestMatchParams.size();
if (size1 > size2)
{
bestMatch = mt;
continue;
}
else if (size2 > size1)
{
continue;
}
}
}
}
if (bestMatch == null)
{
return null;
}
return bestMatch;
}
/**
* Select the method that best matches the details of the request by comparing the media types
* of the input entity and those specified in the Accept header
*
* @param methodRecords the list of methods to select the best match from
* @param context the context of the current request
* @return
*/
private MethodRecord selectBestMatchingMethod(List< ? extends MethodRecord> methodRecords,
RuntimeContext context)
{
HttpHeaders httpHeaders = context.getHttpHeaders();
MediaType inputMediaType = httpHeaders.getMediaType();
List<MediaType> acceptableMediaTypes = httpHeaders.getAcceptableMediaTypes();
MethodRecord bestMatch = null;
for (MethodRecord record : methodRecords)
{
if (compareMethods(record, bestMatch, inputMediaType, acceptableMediaTypes) > 0)
{
bestMatch = record;
}
}
return bestMatch;
}
}