Merged HEAD-QA to HEAD (4.2) (including moving test classes into separate folders)

51903 to 54309 


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@54310 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Samuel Langlois
2013-08-20 17:17:31 +00:00
parent a91f6e2535
commit 788d3c9c89
777 changed files with 77820 additions and 23746 deletions

View File

@@ -0,0 +1,211 @@
package org.alfresco.rest.framework.webscripts;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.alfresco.repo.web.scripts.content.ContentStreamer;
import org.alfresco.rest.framework.Api;
import org.alfresco.rest.framework.core.HttpMethodSupport;
import org.alfresco.rest.framework.core.ResourceLocator;
import org.alfresco.rest.framework.core.ResourceWithMetadata;
import org.alfresco.rest.framework.core.exceptions.ApiException;
import org.alfresco.rest.framework.jacksonextensions.JacksonHelper;
import org.alfresco.rest.framework.resource.actions.ActionExecutor;
import org.alfresco.rest.framework.resource.content.BinaryResource;
import org.alfresco.rest.framework.resource.content.ContentInfo;
import org.alfresco.rest.framework.resource.content.FileBinaryResource;
import org.alfresco.rest.framework.resource.content.NodeBinaryResource;
import org.alfresco.rest.framework.resource.parameters.Params;
import org.alfresco.service.transaction.TransactionService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;
import org.springframework.http.HttpMethod;
/**
* Webscript that handles the request for and execution of a Resource
*
* 1) Finds a resource
* 2) Extracts params
* 3) Executes params on a resource
* 4) Post processes the response to add embeds or projected relationship
* 5) Renders the response
*
* @author Gethin James
*/
// TODO for requests that pass in input streams e.g. binary content for workflow, this is going to need a way to re-read the input stream a la
// code in RepositoryContainer due to retrying transaction logic
public abstract class AbstractResourceWebScript extends ApiWebScript implements HttpMethodSupport, ActionExecutor
{
private static Log logger = LogFactory.getLog(AbstractResourceWebScript.class);
protected ResourceLocator locator;
private HttpMethod httpMethod;
private ParamsExtractor paramsExtractor;
private ContentStreamer streamer;
protected ResourceWebScriptHelper helper;
protected TransactionService transactionService;
@SuppressWarnings("rawtypes")
@Override
public void execute(final Api api, final WebScriptRequest req, final WebScriptResponse res) throws IOException
{
try
{
final Map<String, Object> respons = new HashMap<String, Object>();
final Map<String, String> templateVars = req.getServiceMatch().getTemplateVars();
final ResourceWithMetadata resource = locator.locateResource(api,templateVars, httpMethod);
final Params params = paramsExtractor.extractParams(resource.getMetaData(),req);
final ActionExecutor executor = findExecutor(httpMethod, params, resource, req.getContentType());
//This execution usually takes place in a Retrying Transaction (see subclasses)
executor.execute(resource, params, new ExecutionCallback()
{
@Override
public void onSuccess(Object result, ContentInfo contentInfo)
{
respons.put("toSerialize", result);
respons.put("contentInfo", contentInfo);
setSuccessResponseStatus(res);
}
});
//Outside the transaction.
Object toSerialize = respons.get("toSerialize");
ContentInfo contentInfo = (ContentInfo) respons.get("contentInfo");
setContentInfoOnResponse(res, contentInfo);
if (toSerialize != null)
{
if (toSerialize instanceof BinaryResource)
{
streamResponse(req, res, (BinaryResource) toSerialize);
}
else
{
renderJsonResponse(res, toSerialize);
}
}
}
catch (ApiException apiException)
{
renderErrorResponse(resolveException(apiException), res);
}
catch (WebScriptException webException)
{
renderErrorResponse(resolveException(webException), res);
}
catch (RuntimeException runtimeException)
{
renderErrorResponse(resolveException(runtimeException), res);
}
}
protected void streamResponse(final WebScriptRequest req, final WebScriptResponse res, BinaryResource resource) throws IOException
{
if (resource instanceof FileBinaryResource)
{
FileBinaryResource fileResource = (FileBinaryResource) resource;
streamer.streamContent(req, res, fileResource.getFile(), null, false, null, null);
}
else if (resource instanceof NodeBinaryResource)
{
NodeBinaryResource nodeResource = (NodeBinaryResource) resource;
streamer.streamContent(req, res, nodeResource.getNodeRef(), nodeResource.getPropertyQName(), false, null, null);
}
}
/**
* Renders the result of an execution.
*
* @param res WebScriptResponse
* @param respons result of an execution
* @throws IOException
*/
protected void renderJsonResponse(final WebScriptResponse res, final Object toSerialize)
throws IOException
{
jsonHelper.withWriter(res.getOutputStream(), new JacksonHelper.Writer()
{
@Override
public void writeContents(JsonGenerator generator, ObjectMapper objectMapper)
throws JsonGenerationException, JsonMappingException, IOException
{
objectMapper.writeValue(generator, toSerialize);
}
});
}
/**
* The response status must be set before the response is written by Jackson (which will by default close and commit the response).
* In a r/w txn, web script buffered responses ensure that it doesn't really matter but for r/o txns this is important.
* @param res
*/
protected void setSuccessResponseStatus(final WebScriptResponse res)
{
// default for GET, HEAD, OPTIONS, PUT, TRACE
res.setStatus(Status.STATUS_OK);
}
/**
* Finds the action executor to execute actions on.
* @param httpMethod - the http method
* @param params Params
* @param resource
* @param contentType Request content type
* @return ActionExecutor the action executor
*/
public ActionExecutor findExecutor(HttpMethod httpMethod, Params params, ResourceWithMetadata resource, String contentType)
{
//Ignore all params and return this
return this;
}
public void setTransactionService(TransactionService transactionService)
{
this.transactionService = transactionService;
}
public void setLocator(ResourceLocator locator)
{
this.locator = locator;
}
public void setHttpMethod(HttpMethod httpMethod)
{
this.httpMethod = httpMethod;
}
public void setParamsExtractor(ParamsExtractor paramsExtractor)
{
this.paramsExtractor = paramsExtractor;
}
public void setHelper(ResourceWebScriptHelper helper)
{
this.helper = helper;
}
public HttpMethod getHttpMethod()
{
return this.httpMethod;
}
public void setStreamer(ContentStreamer streamer)
{
this.streamer = streamer;
}
}

View File

@@ -0,0 +1,161 @@
package org.alfresco.rest.framework.webscripts;
import java.io.IOException;
import java.util.Map;
import org.alfresco.rest.framework.Api;
import org.alfresco.rest.framework.core.exceptions.DefaultExceptionResolver;
import org.alfresco.rest.framework.core.exceptions.ErrorResponse;
import org.alfresco.rest.framework.core.exceptions.ExceptionResolver;
import org.alfresco.rest.framework.jacksonextensions.JacksonHelper;
import org.alfresco.rest.framework.jacksonextensions.JacksonHelper.Writer;
import org.alfresco.rest.framework.resource.content.ContentInfo;
import org.alfresco.rest.framework.resource.content.ContentInfoImpl;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.json.simple.JSONObject;
import org.springframework.extensions.webscripts.AbstractWebScript;
import org.springframework.extensions.webscripts.Cache;
import org.springframework.extensions.webscripts.Description.RequiredCache;
import org.springframework.extensions.webscripts.Format;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;
import org.springframework.extensions.webscripts.servlet.WebScriptServletResponse;
/**
* Entry point for API webscript. Supports version/scope as well
* as discovery.
*
* @author Gethin James
*/
public abstract class ApiWebScript extends AbstractWebScript
{
protected JacksonHelper jsonHelper;
ExceptionResolver<Exception> defaultResolver = new DefaultExceptionResolver();
ExceptionResolver<Exception> resolver;
public final static String UTF8 = "UTF-8";
public final static Cache CACHE_NEVER = new Cache(new RequiredCache()
{
@Override
public boolean getNeverCache()
{
return true;
}
@Override
public boolean getIsPublic()
{
return false;
}
@Override
public boolean getMustRevalidate()
{
return true;
}
});
final static ContentInfo DEFAULT_JSON_CONTENT = new ContentInfoImpl(Format.JSON.mimetype(),UTF8, 0, null);
@Override
public void execute(final WebScriptRequest req, final WebScriptResponse res) throws IOException
{
Map<String, String> templateVars = req.getServiceMatch().getTemplateVars();
Api api = determineApi(templateVars);
execute(api, req, res);
}
private Api determineApi(Map<String, String> templateVars)
{
String apiScope = templateVars.get("apiScope");
String apiVersion = templateVars.get("apiVersion");
String apiName = templateVars.get("apiName");
return Api.valueOf(apiName,apiScope,apiVersion);
}
protected ErrorResponse resolveException(Exception ex)
{
ErrorResponse error = resolver.resolveException(ex);
if (error == null)
{
error = defaultResolver.resolveException(ex);
}
return error;
}
public abstract void execute(final Api api, WebScriptRequest req, WebScriptResponse res) throws IOException;
/**
* Renders a JSON error response
* @param errorResponse The error
* @param res web script response
* @throws IOException
*/
public void renderErrorResponse(final ErrorResponse errorResponse, final WebScriptResponse res) throws IOException {
errorResponse.setDescriptionURL("http://developer.alfresco.com/ErrorsExplained.html#"+errorResponse.getErrorKey());
setContentInfoOnResponse(res, DEFAULT_JSON_CONTENT);
// Status must be set before the response is written by Jackson (which will by default close and commit the response).
// In a r/w txn, web script buffered responses ensure that it doesn't really matter but for r/o txns this is important.
res.setStatus(errorResponse.getStatusCode());
jsonHelper.withWriter(res.getOutputStream(), new Writer()
{
@SuppressWarnings("unchecked")
@Override
public void writeContents(JsonGenerator generator, ObjectMapper objectMapper)
throws JsonGenerationException, JsonMappingException, IOException
{
JSONObject obj = new JSONObject();
obj.put("error", errorResponse);
objectMapper.writeValue(generator, obj);
}
});
}
/**
* Sets the response headers with any information we know about the content
* @param res WebScriptResponse
* @param contentInfo Content Information
*/
protected void setContentInfoOnResponse(WebScriptResponse res, ContentInfo contentInfo)
{
if (contentInfo != null)
{
//Set content info on the response
res.setContentType(contentInfo.getMimeType());
res.setContentEncoding(contentInfo.getEncoding());
if (res instanceof WebScriptServletResponse)
{
WebScriptServletResponse servletResponse = (WebScriptServletResponse) res;
if (contentInfo.getLength() > 0)
{
if (contentInfo.getLength()>0 && contentInfo.getLength() < Integer.MAX_VALUE)
{
servletResponse.getHttpServletResponse().setContentLength((int)contentInfo.getLength());
}
}
if (contentInfo.getLocale() != null)
{
servletResponse.getHttpServletResponse().setLocale(contentInfo.getLocale());
}
}
}
}
public void setResolver(ExceptionResolver<Exception> resolver)
{
this.resolver = resolver;
}
public void setJsonHelper(JacksonHelper jsonHelper)
{
this.jsonHelper = jsonHelper;
}
}

View File

@@ -0,0 +1,15 @@
package org.alfresco.rest.framework.webscripts;
import org.alfresco.rest.framework.core.HttpMethodSupport;
import org.alfresco.rest.framework.core.ResourceMetadata;
import org.alfresco.rest.framework.resource.parameters.Params;
import org.springframework.extensions.webscripts.WebScriptRequest;
/*
* Extracts parameters from the HTTP request.
*
*/
public interface ParamsExtractor extends HttpMethodSupport
{
public Params extractParams(ResourceMetadata resourceMeta,WebScriptRequest req);
}

View File

@@ -0,0 +1,138 @@
package org.alfresco.rest.framework.webscripts;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.rest.framework.core.ResourceLocator;
import org.alfresco.rest.framework.core.ResourceMetadata;
import org.alfresco.rest.framework.core.ResourceWithMetadata;
import org.alfresco.rest.framework.core.exceptions.DeletedResourceException;
import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException;
import org.alfresco.rest.framework.resource.actions.interfaces.BinaryResourceAction;
import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction;
import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
import org.alfresco.rest.framework.resource.parameters.Params;
import org.apache.commons.lang.StringUtils;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;
import org.springframework.http.HttpMethod;
/**
* Handles the HTTP DELETE for a Resource
*
* @author Gethin James
*/
public class ResourceWebScriptDelete extends AbstractResourceWebScript implements ParamsExtractor
{
public ResourceWebScriptDelete()
{
super();
setHttpMethod(HttpMethod.DELETE);
setParamsExtractor(this);
}
@Override
public Params extractParams(ResourceMetadata resourceMeta, WebScriptRequest req)
{
String entityId = req.getServiceMatch().getTemplateVars().get(ResourceLocator.ENTITY_ID);
String relationshipId = req.getServiceMatch().getTemplateVars().get(ResourceLocator.RELATIONSHIP_ID);
switch (resourceMeta.getType())
{
case ENTITY:
if (StringUtils.isBlank(entityId))
{
throw new UnsupportedResourceOperationException("DELETE is executed against the instance URL");
}
else
{
return Params.valueOf(entityId, relationshipId);
}
case RELATIONSHIP:
if (StringUtils.isBlank(relationshipId))
{
throw new UnsupportedResourceOperationException("DELETE is executed against the instance URL");
}
else
{
return Params.valueOf(entityId, relationshipId);
}
case PROPERTY:
final String resourceName = req.getServiceMatch().getTemplateVars().get(ResourceLocator.RELATIONSHIP_RESOURCE);
if (StringUtils.isNotBlank(entityId) && StringUtils.isNotBlank(resourceName))
{
return Params.valueOf(entityId, null, null, null, resourceName, null, null);
}
//Fall through to unsupported.
default:
throw new UnsupportedResourceOperationException("DELETE not supported for Actions");
}
}
/**
* Executes the action on the resource
* @param resource
* @param params parameters to use
* @return anObject the result of the execute
*/
private Object executeInternal(ResourceWithMetadata resource, Params params)
{
switch (resource.getMetaData().getType())
{
case ENTITY:
if (resource.getMetaData().isDeleted(EntityResourceAction.Delete.class))
{
throw new DeletedResourceException("(DELETE) "+resource.getMetaData().getUniqueId());
}
EntityResourceAction.Delete entityDeleter = (EntityResourceAction.Delete) resource.getResource();
entityDeleter.delete(params.getEntityId(), params);
//Don't pass anything to the callback - its just successful
return null;
case RELATIONSHIP:
if (resource.getMetaData().isDeleted(RelationshipResourceAction.Delete.class))
{
throw new DeletedResourceException("(DELETE) "+resource.getMetaData().getUniqueId());
}
RelationshipResourceAction.Delete relationDeleter = (RelationshipResourceAction.Delete) resource.getResource();
relationDeleter.delete(params.getEntityId(), params.getRelationshipId(), params);
//Don't pass anything to the callback - its just successful
return null;
case PROPERTY:
if (resource.getMetaData().isDeleted(BinaryResourceAction.Delete.class))
{
throw new DeletedResourceException("(DELETE) "+resource.getMetaData().getUniqueId());
}
BinaryResourceAction.Delete binDeleter = (BinaryResourceAction.Delete) resource.getResource();
binDeleter.deleteProperty(params.getEntityId(), params);
//Don't pass anything to the callback - its just successful
return null;
default:
throw new UnsupportedResourceOperationException("DELETE not supported for Actions");
}
}
@Override
public void execute(final ResourceWithMetadata resource, final Params params, final ExecutionCallback executionCallback)
{
transactionService.getRetryingTransactionHelper().doInTransaction(
new RetryingTransactionCallback<Void>()
{
@Override
public Void execute() throws Throwable
{
executeInternal(resource, params); //ignore return result
executionCallback.onSuccess(null, DEFAULT_JSON_CONTENT);
return null;
}
}, false, true);
}
@Override
protected void setSuccessResponseStatus(WebScriptResponse res)
{
res.setStatus(Status.STATUS_NO_CONTENT);
}
}

View File

@@ -0,0 +1,209 @@
package org.alfresco.rest.framework.webscripts;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.rest.framework.core.ResourceInspector;
import org.alfresco.rest.framework.core.ResourceLocator;
import org.alfresco.rest.framework.core.ResourceMetadata;
import org.alfresco.rest.framework.core.ResourceWithMetadata;
import org.alfresco.rest.framework.core.exceptions.DeletedResourceException;
import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException;
import org.alfresco.rest.framework.resource.actions.interfaces.BinaryResourceAction;
import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction;
import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction.Read;
import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction.ReadById;
import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
import org.alfresco.rest.framework.resource.content.BinaryResource;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Params;
import org.alfresco.rest.framework.resource.parameters.Params.RecognizedParams;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.http.HttpMethod;
/**
* Handles the HTTP Get for a Resource
*
* @author Gethin James
*/
public class ResourceWebScriptGet extends AbstractResourceWebScript implements ParamsExtractor
{
private static Log logger = LogFactory.getLog(ResourceWebScriptGet.class);
public ResourceWebScriptGet()
{
super();
setHttpMethod(HttpMethod.GET);
setParamsExtractor(this);
}
@Override
public Params extractParams(ResourceMetadata resourceMeta, WebScriptRequest req)
{
final String entityId = req.getServiceMatch().getTemplateVars().get(ResourceLocator.ENTITY_ID);
final String relationshipId = req.getServiceMatch().getTemplateVars().get(ResourceLocator.RELATIONSHIP_ID);
final RecognizedParams params = ResourceWebScriptHelper.getRecognizedParams(req);
switch (resourceMeta.getType())
{
case ENTITY:
if (StringUtils.isNotBlank(entityId))
{
return Params.valueOf(params, entityId, null);
}
else
{
return Params.valueOf(params, null, null);// collection resource
}
case RELATIONSHIP:
if (StringUtils.isNotBlank(relationshipId))
{
return Params.valueOf(params, entityId, relationshipId);
}
else
{
return Params.valueOf(params, entityId, null); //relationship collection resource
}
case PROPERTY:
final String resourceName = req.getServiceMatch().getTemplateVars().get(ResourceLocator.RELATIONSHIP_RESOURCE);
if (StringUtils.isNotBlank(entityId) && StringUtils.isNotBlank(resourceName))
{
return Params.valueOf(entityId, null, null, null, resourceName, params, null);
}
//Fall through to unsupported.
default:
throw new UnsupportedResourceOperationException("GET not supported for Actions");
}
}
/**
* Executes the action on the resource
* @param resource
* @param params parameters to use
* @return anObject the result of the execute
*/
private Object executeInternal(ResourceWithMetadata resource, Params params)
{
switch (resource.getMetaData().getType())
{
case ENTITY:
if (StringUtils.isBlank(params.getEntityId()))
{
//Get the collection
if (EntityResourceAction.Read.class.isAssignableFrom(resource.getResource().getClass()))
{
if (resource.getMetaData().isDeleted(EntityResourceAction.Read.class))
{
throw new DeletedResourceException("(GET) "+resource.getMetaData().getUniqueId());
}
EntityResourceAction.Read<?> getter = (Read<?>) resource.getResource();
CollectionWithPagingInfo<?> resources = getter.readAll(params);
return resources;
}
else
{
throw new UnsupportedResourceOperationException();
}
}
else
{
if (EntityResourceAction.ReadById.class.isAssignableFrom(resource.getResource().getClass()))
{
if (resource.getMetaData().isDeleted(EntityResourceAction.ReadById.class))
{
throw new DeletedResourceException("(GET by id) "+resource.getMetaData().getUniqueId());
}
EntityResourceAction.ReadById<?> entityGetter = (ReadById<?>) resource.getResource();
Object result = entityGetter.readById(params.getEntityId(), params);
return result;
}
else
{
throw new UnsupportedResourceOperationException();
}
}
case RELATIONSHIP:
if(StringUtils.isNotBlank(params.getRelationshipId()))
{
if (RelationshipResourceAction.ReadById.class.isAssignableFrom(resource.getResource().getClass()))
{
if (resource.getMetaData().isDeleted(RelationshipResourceAction.ReadById.class))
{
throw new DeletedResourceException("(GET by id) "+resource.getMetaData().getUniqueId());
}
RelationshipResourceAction.ReadById<?> relationGetter = (RelationshipResourceAction.ReadById<?>) resource.getResource();
Object result = relationGetter.readById(params.getEntityId(), params.getRelationshipId(), params);
return result;
}
else
{
throw new UnsupportedResourceOperationException();
}
}
else
{
if (resource.getMetaData().isDeleted(RelationshipResourceAction.Read.class))
{
throw new DeletedResourceException("(GET) "+resource.getMetaData().getUniqueId());
}
RelationshipResourceAction.Read<?> relationGetter = (RelationshipResourceAction.Read<?>) resource.getResource();
CollectionWithPagingInfo<?> relations = relationGetter.readAll(params.getEntityId(),params);
return relations;
}
case PROPERTY:
if (StringUtils.isNotBlank(params.getEntityId()))
{
if (BinaryResourceAction.Read.class.isAssignableFrom(resource.getResource().getClass()))
{
if (resource.getMetaData().isDeleted(BinaryResourceAction.Read.class))
{
throw new DeletedResourceException("(GET) "+resource.getMetaData().getUniqueId());
}
BinaryResourceAction.Read getter = (BinaryResourceAction.Read) resource.getResource();
BinaryResource prop = getter.readProperty(params.getEntityId(), params);
return prop;
}
else
{
throw new UnsupportedResourceOperationException();
}
}
else
{
throw new UnsupportedResourceOperationException();
}
default:
throw new UnsupportedResourceOperationException("GET not supported for Actions");
}
}
@Override
public void execute(final ResourceWithMetadata resource, final Params params, final ExecutionCallback executionCallback)
{
final String entityCollectionName = ResourceInspector.findEntityCollectionNameName(resource.getMetaData());
transactionService.getRetryingTransactionHelper().doInTransaction(
new RetryingTransactionCallback<Void>()
{
@Override
public Void execute() throws Throwable
{
Object result = executeInternal(resource, params);
if (result instanceof BinaryResource)
{
executionCallback.onSuccess(result, null);
}
else
{
executionCallback.onSuccess(helper.postProcessResponse(resource.getMetaData().getApi(), entityCollectionName, params, result), DEFAULT_JSON_CONTENT);
}
return null;
}
}, true, true); //Read only
}
}

View File

@@ -0,0 +1,661 @@
package org.alfresco.rest.framework.webscripts;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringTokenizer;
import org.alfresco.rest.framework.Api;
import org.alfresco.rest.framework.core.ResourceInspector;
import org.alfresco.rest.framework.core.ResourceInspectorUtil;
import org.alfresco.rest.framework.core.ResourceLocator;
import org.alfresco.rest.framework.core.ResourceWithMetadata;
import org.alfresco.rest.framework.core.exceptions.ApiException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.NotFoundException;
import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
import org.alfresco.rest.framework.jacksonextensions.BeanPropertiesFilter;
import org.alfresco.rest.framework.jacksonextensions.ExecutionResult;
import org.alfresco.rest.framework.jacksonextensions.JacksonHelper;
import org.alfresco.rest.framework.resource.actions.ActionExecutor;
import org.alfresco.rest.framework.resource.actions.ActionExecutor.ExecutionCallback;
import org.alfresco.rest.framework.resource.content.ContentInfo;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.InvalidSelectException;
import org.alfresco.rest.framework.resource.parameters.Paging;
import org.alfresco.rest.framework.resource.parameters.Params;
import org.alfresco.rest.framework.resource.parameters.Params.RecognizedParams;
import org.alfresco.rest.framework.resource.parameters.SortColumn;
import org.alfresco.rest.framework.resource.parameters.where.InvalidQueryException;
import org.alfresco.rest.framework.resource.parameters.where.Query;
import org.alfresco.rest.framework.resource.parameters.where.QueryImpl;
import org.alfresco.rest.framework.resource.parameters.where.WhereCompiler;
import org.alfresco.util.Pair;
import org.alfresco.util.PropertyCheck;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.tree.CommonErrorNode;
import org.antlr.runtime.tree.CommonTree;
import org.antlr.runtime.tree.RewriteCardinalityException;
import org.antlr.runtime.tree.Tree;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.jackson.map.JsonMappingException;
import org.springframework.beans.BeanUtils;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.http.HttpMethod;
/**
* Helps a Webscript with various tasks
*
* @author Gethin James
*/
public class ResourceWebScriptHelper
{
private static Log logger = LogFactory.getLog(ResourceWebScriptHelper.class);
public static final String PARAM_RELATIONS = "relations";
public static final String PARAM_FILTER_PROPS = "properties";
public static final String PARAM_PAGING_SKIP = "skipCount";
public static final String PARAM_PAGING_MAX = "maxItems";
public static final String PARAM_SORT = "sort";
public static final String PARAM_WHERE = "where";
public static final String PARAM_SELECT = "select";
public static final List<String> KNOWN_PARAMS = Arrays.asList(PARAM_RELATIONS,PARAM_FILTER_PROPS,PARAM_PAGING_SKIP,PARAM_PAGING_MAX, PARAM_SORT, PARAM_WHERE, PARAM_SELECT);
private ResourceLocator locator;
private ActionExecutor executor;
/**
* Takes the web request and looks for a "filter" parameter Parses the
* parameter and produces a list of bean properties to use as a filter A
* SimpleBeanPropertyFilter it returned that uses the properties If no
* filter param is set then a default BeanFilter is returned that will never
* filter properties (ie. Returns all bean properties).
*
* @param req
* @return BeanPropertyFilter - if no parameter then returns a new
* ReturnAllBeanProperties class
*/
public static BeanPropertiesFilter getFilter(String filterParams)
{
if (filterParams != null)
{
StringTokenizer st = new StringTokenizer(filterParams, ",");
Set<String> filteredProperties = new HashSet<String>(st.countTokens());
while (st.hasMoreTokens())
{
filteredProperties.add(st.nextToken());
}
logger.debug("Filtering using the following properties: " + filteredProperties);
BeanPropertiesFilter filter = new BeanPropertiesFilter(filteredProperties);
return filter;
}
return BeanPropertiesFilter.ALLOW_ALL;
}
/**
* Takes the web request and looks for a "relations" parameter Parses the
* parameter and produces a list of bean properties to use as a filter A
* SimpleBeanPropertiesFilter it returned that uses the properties If no
* filter param is set then a default BeanFilter is returned that will never
* filter properties (ie. Returns all bean properties).
*
* @param req
* @return BeanPropertiesFilter - if no parameter then returns a new
* ReturnAllBeanProperties class
*/
public static Map<String, BeanPropertiesFilter> getRelationFilter(String filterParams)
{
if (filterParams != null)
{
// Split by a comma when not in a bracket
String[] relations = filterParams.split(",(?![^()]*+\\))");
Map<String, BeanPropertiesFilter> filterMap = new HashMap<String, BeanPropertiesFilter>(relations.length);
for (String relation : relations)
{
int bracketLocation = relation.indexOf("(");
if (bracketLocation != -1)
{
// We have properties
String relationKey = relation.substring(0, bracketLocation);
String props = relation.substring(bracketLocation + 1, relation.length() - 1);
filterMap.put(relationKey, getFilter(props));
}
else
{
// no properties so just get the String
filterMap.put(relation, getFilter(null));
}
}
return filterMap;
}
return Collections.emptyMap();
}
/**
* Takes the "select" parameter and turns it into a List<String> property names
* @param selectParam
* @return List<String> bean property names potentially using JSON Pointer syntax
*/
public static List<String> getSelectClause(String selectParam) throws InvalidArgumentException
{
if (selectParam == null) return Collections.emptyList();
try {
CommonTree selectedPropsTree = WhereCompiler.compileSelectClause(selectParam);
if (selectedPropsTree instanceof CommonErrorNode)
{
logger.debug("Error parsing the SELECT clause "+selectedPropsTree);
throw new InvalidSelectException(selectedPropsTree);
}
if (selectedPropsTree.getChildCount() == 0 && !selectedPropsTree.getText().isEmpty())
{
return Arrays.asList(selectedPropsTree.getText());
}
List<Tree> children = selectedPropsTree.getChildren();
if (children!= null && !children.isEmpty())
{
List<String> properties = new ArrayList<String>(children.size());
for (Tree child : children) {
properties.add(child.getText());
}
return properties;
}
} catch (RewriteCardinalityException re) { //Catch any error so it doesn't get thrown up the stack
logger.debug("Unhandled Error parsing the SELECT clause: "+re);
} catch (RecognitionException e) {
logger.debug("Error parsing the SELECT clause: "+selectParam);
}
//Default to throw out an invalid query
throw new InvalidSelectException(selectParam);
}
/**
* Takes the "where" parameter and turns it into a Java Object that can be used for querying
* @param whereParam
* @return Query a parsed version of the where clause, represented in Java
*/
public static Query getWhereClause(String whereParam) throws InvalidQueryException
{
if (whereParam == null) return QueryImpl.EMPTY;
try {
CommonTree whereTree = WhereCompiler.compileWhereClause(whereParam);
if (whereTree instanceof CommonErrorNode)
{
logger.debug("Error parsing the WHERE clause "+whereTree);
throw new InvalidQueryException(whereTree);
}
return new QueryImpl(whereTree);
} catch (RewriteCardinalityException re) { //Catch any error so it doesn't get thrown up the stack
logger.info("Unhandled Error parsing the WHERE clause: "+re);
} catch (RecognitionException e) {
whereParam += ", "+WhereCompiler.resolveMessage(e);
logger.info("Error parsing the WHERE clause: "+whereParam);
}
//Default to throw out an invalid query
throw new InvalidQueryException(whereParam);
}
/**
* Takes the Sort parameter as a String and parses it into a List of SortColumn objects.
* The format is a comma seperated list of "columnName sortDirection",
* e.g. "name DESC, age ASC". It is not case sensitive and the sort direction is optional
* It default to sort ASCENDING.
* @param sortParams - String passed in on the request
* @return List<SortColumn> - the sort columns or an empty list if the params were invalid.
*/
public static List<SortColumn> getSort(String sortParams)
{
if (sortParams != null)
{
StringTokenizer st = new StringTokenizer(sortParams, ",");
List<SortColumn> sortedColumns = new ArrayList<SortColumn>(st.countTokens());
while (st.hasMoreTokens())
{
String token = st.nextToken();
StringTokenizer columnDesc = new StringTokenizer(token, " ");
if (columnDesc.countTokens() <= 2)
{
String columnName = columnDesc.nextToken();
String sortOrder = SortColumn.ASCENDING;
if (columnDesc.hasMoreTokens())
{
String sortDef = columnDesc.nextToken().toUpperCase();
if (SortColumn.ASCENDING.equals(sortDef) || SortColumn.DESCENDING.equals(sortDef))
{
sortOrder = sortDef;
}
else
{
logger.debug("Invalid sort order definition ("+sortDef+"). Valid values are "+SortColumn.ASCENDING+" or "+SortColumn.DESCENDING+".");
}
}
sortedColumns.add(new SortColumn(columnName, SortColumn.ASCENDING.equals(sortOrder)));
}
// filteredProperties.add();
}
// logger.debug("Filtering using the following properties: " + filteredProperties);
// BeanPropertiesFilter filter = new BeanPropertiesFilter(filteredProperties);
return sortedColumns;
}
return Collections.emptyList();
}
/**
* Extracts the body contents from the request
*
* @param req the request
* @param jsonHelper Jackson Helper
* @param requiredType the type to return
* @return the Object in the required type
*/
public static <T> T extractJsonContent(WebScriptRequest req, JacksonHelper jsonHelper, Class<T> requiredType)
{
Reader reader;
try
{
reader = req.getContent().getReader();
return jsonHelper.construct(reader, requiredType);
}
catch (JsonMappingException e)
{
logger.warn("Could not read content from HTTP request body.", e);
throw new InvalidArgumentException("Could not read content from HTTP request body.");
}
catch (IOException e)
{
throw new ApiException("Could not read content from HTTP request body.", e.getCause());
}
}
/**
* Extracts the body contents from the request as a List, the JSON can be an array or just a single value without the [] symbols
*
* @param req the request
* @param jsonHelper Jackson Helper
* @param requiredType the type to return (without the List param)
* @return A List of "Object" as the required type
*/
public static <T> List<T> extractJsonContentAsList(WebScriptRequest req, JacksonHelper jsonHelper, Class<T> requiredType)
{
Reader reader;
try
{
reader = req.getContent().getReader();
return jsonHelper.constructList(reader, requiredType);
}
catch (IOException e)
{
throw new ApiException("Could not read content from HTTP request body.", e.getCause());
}
}
/**
* Set the id of theObj to the uniqueId. Attempts to find a set method and
* invoke it. If it fails it just swallows the exceptions and doesn't throw
* them further.
*
* @param theObj
* @param uniqueId
*/
public static void setUniqueId(Object theObj, String uniqueId)
{
Method annotatedMethod = ResourceInspector.findUniqueIdMethod(theObj.getClass());
if (annotatedMethod != null)
{
PropertyDescriptor pDesc = BeanUtils.findPropertyForMethod(annotatedMethod);
if (pDesc != null)
{
Method writeMethod = pDesc.getWriteMethod();
if (writeMethod != null)
{
try
{
writeMethod.invoke(theObj, uniqueId);
if (logger.isDebugEnabled())
{
logger.debug("Unique id set for property: " + pDesc.getName());
}
}
catch (IllegalArgumentException error)
{
logger.warn("Invocation error", error);
}
catch (IllegalAccessException error)
{
logger.warn("IllegalAccessException", error);
}
catch (InvocationTargetException error)
{
logger.warn("InvocationTargetException", error);
}
}
else
{
logger.warn("No setter method for property: " + pDesc.getName());
}
}
}
}
// /**
// * Renders the response result
// *
// * @param response
// * @param result
// */
// public static void renderResponseDep(Map<String, Object> response, Object result)
// {
//
// if (result == null) { return; }
//
// if (result instanceof Collection)
// {
// response.put("list", result);
// }
// else if (result instanceof CollectionWithPagingInfo)
// {
// CollectionWithPagingInfo<?> col = (CollectionWithPagingInfo<?>) result;
// if (col.getCollection() !=null && !col.getCollection().isEmpty())
// {
// response.put("list", col);
// }
// }
// else if (result instanceof Pair<?,?>)
// {
// Pair<?,?> aPair = (Pair<?, ?>) result;
// response.put("entry", aPair.getFirst());
// response.put("relations", aPair.getSecond());
// }
// else
// {
// response.put("entry", result);
// }
// }
/**
* Looks at the object passed in and recursively expands any @EmbeddedEntityResource annotations or related relationship.
* @EmbeddedEntityResource is expanded by calling the ReadById method for this entity.
*
* Either returns a ExecutionResult object or a CollectionWithPagingInfo containing a collection of ExecutionResult objects.
*
* @param objectToWrap
* @param result
* @return Object - Either ExecutionResult or CollectionWithPagingInfo<ExecutionResult>
*/
public Object postProcessResponse(Api api, String entityCollectionName, Params params, Object objectToWrap)
{
PropertyCheck.mandatory(this, null, params);
if (objectToWrap == null ) return null;
if (objectToWrap instanceof CollectionWithPagingInfo<?>)
{
CollectionWithPagingInfo<?> collectionToWrap = (CollectionWithPagingInfo<?>) objectToWrap;
if (!collectionToWrap.getCollection().isEmpty())
{
Collection<Object> resultCollection = new ArrayList(collectionToWrap.getCollection().size());
for (Object obj : collectionToWrap.getCollection())
{
resultCollection.add(postProcessResponse(api,entityCollectionName,params,obj));
}
return CollectionWithPagingInfo.asPaged(collectionToWrap.getPaging(), resultCollection, collectionToWrap.hasMoreItems(), collectionToWrap.getTotalItems());
}
else
{ //It is empty so just return it for rendering.
return objectToWrap;
}
}
else
{
if (BeanUtils.isSimpleProperty(objectToWrap.getClass()) || objectToWrap instanceof Collection)
{
//Simple property or Collection that can't be embedded so just return it.
return objectToWrap;
}
final ExecutionResult execRes = new ExecutionResult(objectToWrap, params.getFilter());
Map<String,Pair<String,Method>> embeddded = ResourceInspector.findEmbeddedResources(objectToWrap.getClass());
if (embeddded != null && !embeddded.isEmpty())
{
Map<String, Object> results = executeEmbeddedResources(api, params,objectToWrap, embeddded);
execRes.addEmbedded(results);
}
if (params.getRelationsFilter() != null && !params.getRelationsFilter().isEmpty())
{
Map<String, ResourceWithMetadata> relationshipResources = locator.locateRelationResource(api,entityCollectionName, params.getRelationsFilter().keySet(), HttpMethod.GET);
String uniqueEntityId = ResourceInspector.findUniqueId(objectToWrap);
Map<String,Object> relatedResources = executeRelatedResources(api,params.getRelationsFilter(), relationshipResources, uniqueEntityId);
execRes.addRelated(relatedResources);
}
return execRes;
}
}
/**
* Loops through the embedded Resources and executes them. The results are added to list of embedded results used by
* the ExecutionResult object.
*
* @param relatedResources
* @param execRes
* @param uniqueEntityId
*/
private Map<String, Object> executeEmbeddedResources(Api api, Params params, Object objectToWrap, Map<String, Pair<String, Method>> embeddded)
{
final Map<String,Object> results = new HashMap<String,Object>(embeddded.size());
for (Entry<String, Pair<String,Method>> embeddedEntry : embeddded.entrySet())
{
ResourceWithMetadata res = locator.locateEntityResource(api,embeddedEntry.getValue().getFirst(), HttpMethod.GET);
if (res != null)
{
Object id = ResourceInspectorUtil.invokeMethod(embeddedEntry.getValue().getSecond(), objectToWrap);
if (id != null)
{
Object execEmbeddedResult = executeRelatedResource(api, params.getRelationsFilter(), String.valueOf(id), embeddedEntry.getKey(), res);
if (execEmbeddedResult != null)
{
if (execEmbeddedResult instanceof ExecutionResult)
{
((ExecutionResult) execEmbeddedResult).setAnEmbeddedEntity(true);
}
results.put(embeddedEntry.getKey(), execEmbeddedResult);
}
}
else
{
//Call to embedded id for null value,
logger.warn("Cannot embed resource with path "+embeddedEntry.getKey()+". No unique id because the method annotated with @EmbeddedEntityResource returned null.");
}
}
}
return results;
}
/**
* Loops through the related Resources and executed them. The results are added to list of embedded results used by
* the ExecutionResult object.
*
* @param relatedResources
* @param execRes
* @param uniqueEntityId
*/
private Map<String,Object> executeRelatedResources(final Api api, Map<String, BeanPropertiesFilter> filters,
Map<String, ResourceWithMetadata> relatedResources,
String uniqueEntityId)
{
final Map<String,Object> results = new HashMap<String,Object>(relatedResources.size());
for (final Entry<String, ResourceWithMetadata> relation : relatedResources.entrySet())
{
Object execResult = executeRelatedResource(api, filters, uniqueEntityId, relation.getKey(), relation.getValue());
if (execResult != null)
{
results.put(relation.getKey(), execResult);
}
}
return results;
}
/**
* Executes a single related Resource. The results are added to list of embedded results used by
* the ExecutionResult object.
*
* @param relatedResources
* @param uniqueEntityId
*/
private Object executeRelatedResource(final Api api, final Map<String, BeanPropertiesFilter> filters,
final String uniqueEntityId, final String resourceKey, final ResourceWithMetadata resource)
{
try
{
BeanPropertiesFilter paramFilter = null;
final Object[] resultOfExecution = new Object[1];
if (filters!=null)
{
paramFilter = filters.get(resourceKey);
}
final Params executionParams = Params.valueOf(paramFilter, uniqueEntityId);
executor.execute(resource, executionParams, new ExecutionCallback()
{
@Override
public void onSuccess(Object result, ContentInfo contentInfo)
{
resultOfExecution[0] = result;
}
});
return resultOfExecution[0];
}
catch(NotFoundException e)
{
// ignore, cannot access the object so don't embed it
if (logger.isDebugEnabled())
{
logger.debug("Ignored error, cannot access the object so can't embed it ", e);
}
}
catch(PermissionDeniedException e)
{
// ignore, cannot access the object so don't embed it
if (logger.isDebugEnabled())
{
logger.debug("Ignored error, cannot access the object so can't embed it ", e);
}
}
return null; //default
}
/**
* Finds all request parameters that aren't already know about (eg. not paging or filter params)
* and returns them for use.
*
* @param req - the WebScriptRequest object
* @return Map<String, String[]> the request parameters
*/
public static Map<String, String[]> getRequestParameters(WebScriptRequest req)
{
if (req!= null)
{
String[] paramNames = req.getParameterNames();
if (paramNames!= null)
{
Map<String, String[]> requestParameteters = new HashMap<String, String[]>(paramNames.length);
for (int i = 0; i < paramNames.length; i++)
{
String paramName = paramNames[i];
if (!KNOWN_PARAMS.contains(paramName))
{
String[] vals = req.getParameterValues(paramName);
requestParameteters.put(paramName, vals);
}
}
return requestParameteters;
}
}
return Collections.emptyMap();
}
/**
* Finds the formal set of params that any rest service could potentially have passed in as request params
* @param req WebScriptRequest
* @return RecognizedParams a POJO containing the params for use with the Params objects
*/
public static RecognizedParams getRecognizedParams(WebScriptRequest req)
{
Paging paging = findPaging(req);
List<SortColumn> sorting = getSort(req.getParameter(ResourceWebScriptHelper.PARAM_SORT));
Map<String, BeanPropertiesFilter> relationFilter = getRelationFilter(req.getParameter(ResourceWebScriptHelper.PARAM_RELATIONS));
BeanPropertiesFilter filter = getFilter(req.getParameter(ResourceWebScriptHelper.PARAM_FILTER_PROPS));
Query whereQuery = getWhereClause(req.getParameter(ResourceWebScriptHelper.PARAM_WHERE));
Map<String, String[]> requestParams = getRequestParameters(req);
List<String> theSelect = getSelectClause(req.getParameter(ResourceWebScriptHelper.PARAM_SELECT));
return new RecognizedParams(requestParams, paging, filter, relationFilter, theSelect, whereQuery, sorting);
}
/**
* Find paging setings based on the request parameters.
*
* @param req
* @return Paging
*/
public static Paging findPaging(WebScriptRequest req)
{
int skipped = Paging.DEFAULT_SKIP_COUNT;
int max = Paging.DEFAULT_MAX_ITEMS;
String skip = req.getParameter(PARAM_PAGING_SKIP);
String maxItems = req.getParameter(PARAM_PAGING_MAX);
try
{
if (skip != null) { skipped = Integer.parseInt(skip);}
if (maxItems != null) { max = Integer.parseInt(maxItems); }
if (max < 0 || skipped < 0)
{
throw new InvalidArgumentException("Negative values not supported.");
}
}
catch (NumberFormatException error)
{
if (logger.isDebugEnabled())
{
logger.debug("Invalid paging params skip: " + skip + ",maxItems:" + maxItems);
}
throw new InvalidArgumentException();
}
return Paging.valueOf(skipped, max);
}
public void setLocator(ResourceLocator locator)
{
this.locator = locator;
}
public void setExecutor(ActionExecutor executor)
{
this.executor = executor;
}
}

View File

@@ -0,0 +1,203 @@
package org.alfresco.rest.framework.webscripts;
import java.util.Arrays;
import java.util.List;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.rest.framework.core.ResourceInspector;
import org.alfresco.rest.framework.core.ResourceLocator;
import org.alfresco.rest.framework.core.ResourceMetadata;
import org.alfresco.rest.framework.core.ResourceParameter;
import org.alfresco.rest.framework.core.ResourceWithMetadata;
import org.alfresco.rest.framework.core.exceptions.DeletedResourceException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException;
import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction;
import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Params;
import org.alfresco.rest.framework.resource.parameters.Params.RecognizedParams;
import org.apache.commons.lang.StringUtils;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;
import org.springframework.http.HttpMethod;
/**
* Handles the HTTP POST for a Resource, equivalent to CRUD Create
*
* @author Gethin James
*/
public class ResourceWebScriptPost extends AbstractResourceWebScript implements ParamsExtractor
{
public ResourceWebScriptPost()
{
super();
setHttpMethod(HttpMethod.POST);
setParamsExtractor(this);
}
@Override
public Params extractParams(ResourceMetadata resourceMeta, WebScriptRequest req)
{
final RecognizedParams params = ResourceWebScriptHelper.getRecognizedParams(req);
switch (resourceMeta.getType())
{
case ENTITY:
String entityIdCheck = req.getServiceMatch().getTemplateVars().get(ResourceLocator.ENTITY_ID);
if (StringUtils.isNotBlank(entityIdCheck))
{
throw new UnsupportedResourceOperationException("POST is executed against the collection URL");
}
else
{
Object postedObj = extractObjFromJson(resourceMeta, req);
return Params.valueOf(null, params, postedObj);
}
case RELATIONSHIP:
String entityId = req.getServiceMatch().getTemplateVars().get(ResourceLocator.ENTITY_ID);
String relationshipId = req.getServiceMatch().getTemplateVars().get(ResourceLocator.RELATIONSHIP_ID);
if (StringUtils.isNotBlank(relationshipId))
{
throw new UnsupportedResourceOperationException("POST is executed against the collection URL");
}
else
{
Object postedRel = extractObjFromJson(resourceMeta, req);
return Params.valueOf(entityId,params,postedRel);
}
default:
throw new UnsupportedResourceOperationException("POST not supported for Actions");
}
}
/**
* If the @WebApiParam has been used and set allowMultiple to false then this will get a single entry. It
* should error if an array is passed in.
* @param resourceMeta
* @param req
* @return Either an object
*/
private Object extractObjFromJson(ResourceMetadata resourceMeta, WebScriptRequest req)
{
List<ResourceParameter> params = resourceMeta.getParameters(HttpMethod.POST);
Class<?> objType = resourceMeta.getObjectType(HttpMethod.POST);
if (!params.isEmpty())
{
for (ResourceParameter resourceParameter : params)
{
if (ResourceParameter.KIND.HTTP_BODY_OBJECT.equals(resourceParameter.getParamType()) && !resourceParameter.isAllowMultiple())
{
// Only allow 1 value.
try
{
Object content = ResourceWebScriptHelper.extractJsonContent(req,jsonHelper, objType);
return Arrays.asList(content);
}
catch (InvalidArgumentException iae)
{
if (iae.getMessage().contains("START_ARRAY") && iae.getMessage().contains("line: 1, column: 1"))
{
throw new UnsupportedResourceOperationException("Only 1 entity is supported in the HTTP request body");
}
else
{
throw iae;
}
}
}
}
}
return ResourceWebScriptHelper.extractJsonContentAsList(req, jsonHelper, objType);
}
/**
* Executes the action on the resource
* @param resource
* @param params parameters to use
* @return anObject the result of the execute
*/
@SuppressWarnings("unchecked")
private Object executeInternal(ResourceWithMetadata resource, Params params)
{
switch (resource.getMetaData().getType())
{
case ENTITY:
if (resource.getMetaData().isDeleted(EntityResourceAction.Create.class))
{
throw new DeletedResourceException("(DELETE) "+resource.getMetaData().getUniqueId());
}
EntityResourceAction.Create<Object> creator = (EntityResourceAction.Create<Object>) resource.getResource();
List<Object> created = creator.create((List<Object>) params.getPassedIn(), params);
if (created !=null && created.size() == 1)
{
//return just one object instead of an array
return created.get(0);
}
else
{
return wrapWithCollectionWithPaging(created);
}
case RELATIONSHIP:
if (resource.getMetaData().isDeleted(RelationshipResourceAction.Create.class))
{
throw new DeletedResourceException("(DELETE) "+resource.getMetaData().getUniqueId());
}
RelationshipResourceAction.Create<Object> createRelation = (RelationshipResourceAction.Create) resource.getResource();
List<Object> createdRel = createRelation.create(params.getEntityId(), (List<Object>) params.getPassedIn(), params);
if (createdRel !=null && createdRel.size() == 1)
{
//return just one object instead of an array
return createdRel.get(0);
}
else
{
return wrapWithCollectionWithPaging(createdRel);
}
default:
throw new UnsupportedResourceOperationException("POST not supported for Actions");
}
}
private Object wrapWithCollectionWithPaging(List<Object> created)
{
if (created !=null && created.size() > 1)
{
return CollectionWithPagingInfo.asPagedCollection(created.toArray());
}
else
{
return created;
}
}
@Override
public void execute(final ResourceWithMetadata resource, final Params params, final ExecutionCallback executionCallback)
{
final String entityCollectionName = ResourceInspector.findEntityCollectionNameName(resource.getMetaData());
transactionService.getRetryingTransactionHelper().doInTransaction(
new RetryingTransactionCallback<Void>()
{
@SuppressWarnings("unchecked")
@Override
public Void execute() throws Throwable
{
Object result = executeInternal(resource, params);
executionCallback.onSuccess(helper.postProcessResponse(resource.getMetaData().getApi(), entityCollectionName, params, result), DEFAULT_JSON_CONTENT);
return null;
}
}, false, true);
}
@Override
protected void setSuccessResponseStatus(WebScriptResponse res)
{
res.setStatus(Status.STATUS_CREATED);
}
}

View File

@@ -0,0 +1,193 @@
package org.alfresco.rest.framework.webscripts;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.rest.framework.core.ResourceInspector;
import org.alfresco.rest.framework.core.ResourceLocator;
import org.alfresco.rest.framework.core.ResourceMetadata;
import org.alfresco.rest.framework.core.ResourceWithMetadata;
import org.alfresco.rest.framework.core.exceptions.DeletedResourceException;
import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException;
import org.alfresco.rest.framework.resource.actions.interfaces.BinaryResourceAction;
import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction;
import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction;
import org.alfresco.rest.framework.resource.content.BasicContentInfo;
import org.alfresco.rest.framework.resource.content.ContentInfoImpl;
import org.alfresco.rest.framework.resource.parameters.Params;
import org.alfresco.rest.framework.resource.parameters.Params.RecognizedParams;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.servlet.WebScriptServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
/**
* Handles the HTTP PUT for a Resource, equivalent to CRUD Update
*
* @author Gethin James
*/
public class ResourceWebScriptPut extends AbstractResourceWebScript implements ParamsExtractor
{
private static Log logger = LogFactory.getLog(ResourceWebScriptPut.class);
public ResourceWebScriptPut()
{
super();
setHttpMethod(HttpMethod.PUT);
setParamsExtractor(this);
}
@Override
public Params extractParams(ResourceMetadata resourceMeta, WebScriptRequest req)
{
final String relationshipId = req.getServiceMatch().getTemplateVars().get(ResourceLocator.RELATIONSHIP_ID);
final String entityId = req.getServiceMatch().getTemplateVars().get(ResourceLocator.ENTITY_ID);
final RecognizedParams params = ResourceWebScriptHelper.getRecognizedParams(req);
switch (resourceMeta.getType())
{
case ENTITY:
if (StringUtils.isBlank(entityId))
{
throw new UnsupportedResourceOperationException("PUT is executed against the instance URL");
} else
{
Object putEnt = ResourceWebScriptHelper.extractJsonContent(req, jsonHelper, resourceMeta.getObjectType(HttpMethod.PUT));
return Params.valueOf(entityId,params,putEnt);
}
case RELATIONSHIP:
if (StringUtils.isBlank(relationshipId))
{
throw new UnsupportedResourceOperationException("PUT is executed against the instance URL");
} else
{
Object putRel = ResourceWebScriptHelper.extractJsonContent(req, jsonHelper, resourceMeta.getObjectType(HttpMethod.PUT));
ResourceWebScriptHelper.setUniqueId(putRel,relationshipId);
return Params.valueOf(entityId, params, putRel);
}
case PROPERTY:
final String resourceName = req.getServiceMatch().getTemplateVars().get(ResourceLocator.RELATIONSHIP_RESOURCE);
if (StringUtils.isNotBlank(entityId) && StringUtils.isNotBlank(resourceName))
{
return Params.valueOf(entityId, null, null, getStream(req), resourceName, params, getContentInfo(req));
}
//Fall through to unsupported.
default:
throw new UnsupportedResourceOperationException("PUT not supported for this request.");
}
}
/**
* Returns the basic content info from the request.
* @param req
* @return BasicContentInfo
* @throws IOException
*/
private BasicContentInfo getContentInfo(WebScriptRequest req) {
String encoding = "UTF-8";
String contentType = MimetypeMap.MIMETYPE_BINARY;
if (StringUtils.isNotEmpty(req.getContentType()))
{
MediaType media = MediaType.parseMediaType(req.getContentType());
contentType = media.getType()+'/'+media.getSubtype();
if (media.getCharSet() != null)
{
encoding = media.getCharSet().toString();
}
}
return new ContentInfoImpl(contentType, encoding, -1, Locale.getDefault());
}
/**
* Returns the input stream for the request
* @param req
* @return
* @throws IOException
*/
private InputStream getStream(WebScriptRequest req)
{
if (req instanceof WebScriptServletRequest)
{
WebScriptServletRequest servletRequest = (WebScriptServletRequest) req;
try
{
return servletRequest.getHttpServletRequest().getInputStream();
}
catch (IOException error)
{
logger.warn("Failed to get the input stream.", error);
}
}
return null;
}
/**
* Executes the action on the resource
* @param resource
* @param params parameters to use
* @return anObject the result of the execute
*/
@SuppressWarnings("unchecked")
private Object executeInternal(ResourceWithMetadata resource, Params params)
{
switch (resource.getMetaData().getType())
{
case ENTITY:
if (resource.getMetaData().isDeleted(EntityResourceAction.Update.class))
{
throw new DeletedResourceException("(UPDATE) "+resource.getMetaData().getUniqueId());
}
EntityResourceAction.Update<Object> updateEnt = (EntityResourceAction.Update<Object>) resource.getResource();
Object result = updateEnt.update(params.getEntityId(), params.getPassedIn(), params);
return result;
case RELATIONSHIP:
if (resource.getMetaData().isDeleted(RelationshipResourceAction.Update.class))
{
throw new DeletedResourceException("(UPDATE) "+resource.getMetaData().getUniqueId());
}
RelationshipResourceAction.Update<Object> relationUpdater = (RelationshipResourceAction.Update<Object>) resource.getResource();
Object relResult = relationUpdater.update(params.getEntityId(), params.getPassedIn(), params);
return relResult;
case PROPERTY:
if (resource.getMetaData().isDeleted(BinaryResourceAction.Update.class))
{
throw new DeletedResourceException("(UPDATE) "+resource.getMetaData().getUniqueId());
}
BinaryResourceAction.Update binUpdater = (BinaryResourceAction.Update) resource.getResource();
binUpdater.update(params.getEntityId(),params.getContentInfo(), params.getStream(), params);
//Don't pass anything to the callback - its just successful
return null;
default:
throw new UnsupportedResourceOperationException("PUT not supported for Actions");
}
}
@Override
public void execute(final ResourceWithMetadata resource, final Params params, final ExecutionCallback executionCallback)
{
final String entityCollectionName = ResourceInspector.findEntityCollectionNameName(resource.getMetaData());
transactionService.getRetryingTransactionHelper().doInTransaction(
new RetryingTransactionCallback<Void>()
{
@Override
public Void execute() throws Throwable
{
Object result = executeInternal(resource, params);
executionCallback.onSuccess(helper.postProcessResponse(resource.getMetaData().getApi(), entityCollectionName, params, result), DEFAULT_JSON_CONTENT);
return null;
}
}, false, true);
}
}

View File

@@ -0,0 +1,80 @@
package org.alfresco.rest.framework.webscripts.metadata;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import org.alfresco.rest.framework.Api;
import org.alfresco.rest.framework.core.ResourceDictionary;
import org.alfresco.rest.framework.core.ResourceLookupDictionary;
import org.alfresco.rest.framework.core.ResourceMetadata;
import org.alfresco.rest.framework.core.ResourceWithMetadata;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.jacksonextensions.ExecutionResult;
import org.alfresco.rest.framework.jacksonextensions.JacksonHelper.Writer;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Paging;
import org.alfresco.rest.framework.webscripts.ApiWebScript;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;
/**
* Provides general information about the Api calls and methods.
*
* @author Gethin James
*/
public class InfoWebScriptGet extends ApiWebScript
{
private ResourceLookupDictionary lookupDictionary;
public void setLookupDictionary(ResourceLookupDictionary lookupDictionary)
{
this.lookupDictionary = lookupDictionary;
}
@Override
public void execute(final Api api, WebScriptRequest req, WebScriptResponse res) throws IOException
{
ResourceDictionary resourceDic = lookupDictionary.getDictionary();
final Map<String, ResourceWithMetadata> apiResources = resourceDic.getAllResources().get(api);
if (apiResources == null)
{
throw new InvalidArgumentException(InvalidArgumentException.DEFAULT_INVALID_API);
}
jsonHelper.withWriter(res.getOutputStream(), new Writer()
{
@Override
public void writeContents(JsonGenerator generator, ObjectMapper objectMapper)
throws JsonGenerationException, JsonMappingException, IOException
{
List<ExecutionResult> entities = new ArrayList<ExecutionResult>();
for (ResourceWithMetadata resource : apiResources.values())
{
entities.add(new ExecutionResult(resource.getMetaData(), null));
}
Collections.sort(entities, new Comparator<ExecutionResult>()
{
public int compare(ExecutionResult r1, ExecutionResult r2)
{
return ((ResourceMetadata) r1.getRoot()).getUniqueId().compareTo(((ResourceMetadata) r2.getRoot()).getUniqueId());
}
});
objectMapper.writeValue(generator, CollectionWithPagingInfo.asPaged(Paging.DEFAULT,entities));
}
});
}
}

View File

@@ -0,0 +1,151 @@
package org.alfresco.rest.framework.webscripts.metadata;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.alfresco.rest.framework.Api;
import org.alfresco.rest.framework.core.ResourceDictionary;
import org.alfresco.rest.framework.core.ResourceLocator;
import org.alfresco.rest.framework.core.ResourceLookupDictionary;
import org.alfresco.rest.framework.core.ResourceMetadata.RESOURCE_TYPE;
import org.alfresco.rest.framework.core.ResourceWithMetadata;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException;
import org.alfresco.rest.framework.jacksonextensions.ExecutionResult;
import org.alfresco.rest.framework.jacksonextensions.JacksonHelper.Writer;
import org.alfresco.rest.framework.metadata.ResourceMetaDataWriter;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Paging;
import org.alfresco.rest.framework.webscripts.ApiWebScript;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;
/**
* Provides general information about an Api call and its methods.
*
* @author Gethin James
*/
public class WebScriptOptionsMetaData extends ApiWebScript implements ResourceMetaDataWriter
{
private static Log logger = LogFactory.getLog(WebScriptOptionsMetaData.class);
private ResourceLookupDictionary lookupDictionary;
private Map<String, ResourceMetaDataWriter> writers;
public void setLookupDictionary(ResourceLookupDictionary lookupDictionary)
{
this.lookupDictionary = lookupDictionary;
}
@Override
public void execute(final Api api, WebScriptRequest req, WebScriptResponse res) throws IOException
{
final Map<String, String> templateVars = req.getServiceMatch().getTemplateVars();
ResourceDictionary resourceDic = lookupDictionary.getDictionary();
Map<String, ResourceWithMetadata> apiResources = resourceDic.getAllResources().get(api);
if (apiResources == null)
{
throw new InvalidArgumentException(InvalidArgumentException.DEFAULT_INVALID_API);
}
String collectionName = templateVars.get(ResourceLocator.COLLECTION_RESOURCE);
String resourceName = templateVars.get(ResourceLocator.RELATIONSHIP_RESOURCE);
String resourceKey = ResourceDictionary.resourceKey(collectionName, resourceName);
if (logger.isDebugEnabled())
{
logger.debug("Locating resource :" + resourceKey);
}
ResourceWithMetadata resource = apiResources.get(resourceKey);
if (resource == null)
{
//Get entity resource and check if we are referencing a property on it.
resourceKey = ResourceDictionary.propertyResourceKey(collectionName, resourceName);
resource = apiResources.get(resourceKey);
}
ResourceMetaDataWriter writer = chooseWriter(req);
writer.writeMetaData(res.getOutputStream(), resource, apiResources);
}
/**
* Chooses the correct writer to use based on the supplied "format" param
* @param req - the WebScriptRequest
* @return ResourceMetaDataWriter - a matching writer - DEFAULT is this class.
*/
protected ResourceMetaDataWriter chooseWriter(WebScriptRequest req)
{
if (writers != null)
{
ResourceMetaDataWriter theWriter = writers.get(req.getParameter("format"));
if (theWriter != null) return theWriter;
}
return this;
}
/**
* Processes the resulting resource and returns the data to be displayed
* @param resource
* @param apiResources
* @return Either a ExecutionResult or a CollectionWithPagingInfo
*/
public static Object processResult(ResourceWithMetadata resource, Map<String, ResourceWithMetadata> apiResources)
{
List<ExecutionResult> results = new ArrayList();
if (RESOURCE_TYPE.ENTITY.equals(resource.getMetaData().getType()))
{
results.add(new ExecutionResult(resource, null));
for (ResourceWithMetadata aResource : apiResources.values())
{
if (resource.getMetaData().getUniqueId().equals(aResource.getMetaData().getParentResource()))
{
results.add(new ExecutionResult(aResource, null));
}
}
}
if (results.isEmpty())
{
return new ExecutionResult(resource, null);
}
else
{
return CollectionWithPagingInfo.asPaged(Paging.DEFAULT, results);
}
}
@Override
public void writeMetaData(OutputStream out, ResourceWithMetadata resource, Map<String, ResourceWithMetadata> allApiResources) throws IOException
{
final Object result = processResult(resource,allApiResources);
jsonHelper.withWriter(out, new Writer()
{
@Override
public void writeContents(JsonGenerator generator, ObjectMapper objectMapper)
throws JsonGenerationException, JsonMappingException, IOException
{
objectMapper.writeValue(generator, result);
}
});
}
public void setWriters(Map<String, ResourceMetaDataWriter> writers)
{
this.writers = writers;
}
}