/*
* Copyright: (c) 2004-2011 Mayo Foundation for Medical Education and
* Research (MFMER). All rights reserved. MAYO, MAYO CLINIC, and the
* triple-shield Mayo logo are trademarks and service marks of MFMER.
*
* Except as contained in the copyright notice above, or as used to identify
* MFMER as the author of this software, the trade names, trademarks, service
* marks, or product names of the copyright holder shall not be used in
* advertising, promotion or otherwise in connection with this software without
* prior written authorization of the copyright holder.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mayo.cts2.framework.webapp.rest.controller;
import edu.mayo.cts2.framework.core.config.ServerContext;
import edu.mayo.cts2.framework.core.constants.URIHelperInterface;
import edu.mayo.cts2.framework.model.command.Page;
import edu.mayo.cts2.framework.model.command.ResolvedReadContext;
import edu.mayo.cts2.framework.model.core.*;
import edu.mayo.cts2.framework.model.core.types.CompleteDirectory;
import edu.mayo.cts2.framework.model.core.types.SortDirection;
import edu.mayo.cts2.framework.model.directory.DirectoryResult;
import edu.mayo.cts2.framework.model.exception.ExceptionFactory;
import edu.mayo.cts2.framework.model.service.exception.UnknownResourceReference;
import edu.mayo.cts2.framework.service.profile.*;
import edu.mayo.cts2.framework.webapp.rest.command.QueryControl;
import edu.mayo.cts2.framework.webapp.rest.command.RestReadContext;
import edu.mayo.cts2.framework.webapp.rest.exception.StatusSettingCts2RestException;
import edu.mayo.cts2.framework.webapp.rest.resolver.FilterResolver;
import edu.mayo.cts2.framework.webapp.rest.resolver.ReadContextResolver;
import edu.mayo.cts2.framework.webapp.rest.util.ControllerUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Field;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.*;
import java.util.Map.Entry;
/**
* An Abstract Spring MVC Controller to handle various common CTS2 functionality such as
* reading individual resources and constructing directories.
*
* @author <a href="mailto:kevin.peterson@mayo.edu">Kevin Peterson</a>
*/
public abstract class AbstractMessageWrappingController extends
AbstractController {
@Resource
private UrlTemplateBindingCreator urlTemplateBindingCreator;
@Resource
private CreateHandler createHandler;
@Resource
private UpdateHandler updateHandler;
@Resource
private DeleteHandler deleteHandler;
@Resource
private FilterResolver filterResolver;
@Resource
private ServerContext serverContext;
@Resource
private ReadContextResolver readContextResolver;
private static String BEANS_VIEW = "beans";
private static String BEANS_MODEL_OBJECT = "bean";
private static String IS_DIRECTORY_MODEL_OBJECT = "isDirectory";
private static String URLBASE_MODEL_OBJECT = "urlBase";
/*
* (non-Javadoc)
*
* @see
* org.cts2.web.rest.controller.AbstractMessageWrappingController.MethodWrapper
* #wrapMessage(java.lang.Object, javax.servlet.http.HttpServletRequest)
*/
/**
* Wrap message.
*
* @param <T>
* the generic type
* @param message
* the message
* @param httpServletRequest
* the http servlet request
* @return the t
*/
protected <T extends Message> T wrapMessage(T message,
HttpServletRequest httpServletRequest) {
RESTResource heading = this
.getHeadingForNameRequest(httpServletRequest);
message.setHeading(heading);
return message;
}
protected <T extends Message, R> T wrapMessage(T message,
String urlTemplate,
UrlTemplateBinder<R> binder,
R resource,
HttpServletRequest httpServletRequest) {
String resourceUrl = this.urlTemplateBindingCreator.bindResourceToUrlTemplate(binder, resource, urlTemplate);
RESTResource heading = this
.getHeadingWithKnownUrlRequest(httpServletRequest, resourceUrl);
message.setHeading(heading);
return message;
}
private void setDirectoryEntries(Directory directory, List<?> entries){
try {
final Field field = ReflectionUtils.findField(directory.getClass(),
"_entryList");
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
field.setAccessible(true);
return null;
}
});
ReflectionUtils.setField(field, directory, entries);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Populate directory.
*
* @param <T>
* the generic type
* @param result
* the result
* @param page
* the page
* @param httpServletRequest
* the http servlet request
* @param directoryClazz
* the directory clazz
* @return the t
*/
@SuppressWarnings("unchecked")
protected <T extends Directory> T populateDirectory(
DirectoryResult<?> result,
Page page,
HttpServletRequest httpServletRequest,
Class<T> directoryClazz) {
T directory;
try {
directory = directoryClazz.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
if(result == null || result.getEntries() == null){
result = new DirectoryResult<Void>(new ArrayList<Void>(), true);
}
this.setDirectoryEntries(directory, result.getEntries());
boolean atEnd = result.isAtEnd();
boolean isComplete = atEnd && ( page.getPage() == 0 );
String urlRoot = this.serverContext.getServerRootWithAppName();
if (!urlRoot.endsWith("/")) {
urlRoot = urlRoot + "/";
}
String pathInfo = httpServletRequest.getServletPath();
String url = urlRoot
+ StringUtils.removeStart(pathInfo,
"/");
if (isComplete) {
directory.setComplete(CompleteDirectory.COMPLETE);
} else {
directory.setComplete(CompleteDirectory.PARTIAL);
if (!result.isAtEnd()) {
directory.setNext(url
+ getParametersString(
httpServletRequest.getParameterMap(),
page.getPage() + 1, page.getMaxToReturn()));
}
if (page.getPage() > 0) {
directory.setPrev(url
+ getParametersString(
httpServletRequest.getParameterMap(),
page.getPage() - 1, page.getMaxToReturn()));
}
}
directory.setNumEntries((long) result.getEntries().size());
return this.wrapMessage(directory, httpServletRequest);
}
@SuppressWarnings("unchecked")
protected RESTResource getHeadingForNameRequest(HttpServletRequest request) {
return this.getHeading(request.getParameterMap(),
this.getUrlPathHelper().getServletPath(request));
}
@SuppressWarnings("unchecked")
protected RESTResource getHeadingWithKnownUrlRequest(HttpServletRequest request,
String resourceUrl) {
return this.getHeading(request.getParameterMap(), resourceUrl);
}
protected <R> ModelAndView forward(
HttpServletRequest httpServletRequest,
Message message,
R resource,
UrlTemplateBinder<R> urlBinder,
String urlTemplate,
boolean redirect){
ModelAndView mav;
if(!redirect){
mav = this.buildUriForwardingModelAndView(message);
} else {
@SuppressWarnings("unchecked")
Map<String,Object> parameters =
new HashMap<String,Object>(httpServletRequest.getParameterMap());
parameters.remove(PARAM_REDIRECT);
parameters.remove(PARAM_URI);
String queryString = this.mapToQueryString(parameters);
mav = new ModelAndView(
"redirect:" + this.urlTemplateBindingCreator.bindResourceToUrlTemplate(urlBinder, resource, urlTemplate) + queryString);
}
return mav;
}
protected ModelAndView buildUriForwardingModelAndView(Object payload){
return new ModelAndView(
"forward:"+ UriResolutionController.FORWARDING_URL,
UriResolutionController.ATTRIBUTE_NAME,
payload);
}
protected <R,I> Object doRead(
HttpServletRequest httpServletRequest,
MessageFactory<R> messageFactory,
ReadService<R,I> readService,
RestReadContext restReadContext,
Class<? extends UnknownResourceReference > exceptionClazz,
I id) {
ResolvedReadContext resolvedContext = this.resolveRestReadContext(restReadContext);
R resource = readService.read(id, resolvedContext);
if(resource == null){
throw ExceptionFactory.createUnknownResourceException(id.toString(), exceptionClazz);
}
Message msg = messageFactory.createMessage(resource);
msg = this.wrapMessage(msg, httpServletRequest);
return this.buildResponse(httpServletRequest, msg);
}
protected Object buildResponse(HttpServletRequest request, Object bean){
String acceptHeader = request.getHeader("Accept");
List<MediaType> types = MediaType.parseMediaTypes(acceptHeader);
if(CollectionUtils.isEmpty(types)){
return new ResponseEntity<Object>(bean, HttpStatus.OK);
}
MediaType.sortByQualityValue(types);
MediaType type = types.get(0);
if(this.getRestConfig().getAllowHtmlRendering() &&
type.isCompatibleWith(MediaType.TEXT_HTML)){
ModelAndView mav = new ModelAndView(BEANS_VIEW);
mav.addObject(BEANS_MODEL_OBJECT, bean);
mav.addObject(URLBASE_MODEL_OBJECT, this.serverContext.getServerRootWithAppName());
mav.addObject(IS_DIRECTORY_MODEL_OBJECT, bean instanceof Directory);
return mav;
} else {
return new ResponseEntity<Object>(bean, this.getHeaders(request), HttpStatus.OK);
}
}
@SuppressWarnings("unchecked")
protected HttpHeaders getHeaders(HttpServletRequest request){
HttpHeaders httpHeaders = new HttpHeaders();
Enumeration<String> headers = request.getHeaderNames();
while(headers.hasMoreElements()){
String headerName = headers.nextElement();
httpHeaders.put(headerName, Collections.list(request.getHeaders(headerName)));
}
return httpHeaders;
}
protected <R,S,Q extends ResourceQuery> Object doQuery(
HttpServletRequest httpServletRequest,
boolean isList,
QueryService<R,S,Q> queryService,
Q query,
Page page,
QueryControl queryControl,
Class<? extends Directory> summaryDirectory,
Class<? extends Directory> listDirectory) {
DirectoryResult<?> result;
Class<? extends Directory> directoryClass;
SortCriteria sortCriteria = this.resolveSort(queryControl, queryService);
if(isList){
result =
queryService.getResourceList(query, sortCriteria, page);
directoryClass = listDirectory;
} else {
result =
queryService.getResourceSummaries(query, sortCriteria, page);
directoryClass = summaryDirectory;
}
Directory dir =
this.populateDirectory(result, page, httpServletRequest, directoryClass);
return this.buildResponse(httpServletRequest, dir);
}
protected <I> Object doDelete(
HttpServletResponse response,
I identifier,
String changeSetUri,
BaseMaintenanceService<?,?,I> service) {
this.deleteHandler.delete(identifier, changeSetUri, service);
response.setStatus(HttpStatus.NO_CONTENT.value());
//TODO: Add a ModelAndView return type
return null;
}
protected <T extends IsChangeable,R extends IsChangeable,I> Object doUpdate(
HttpServletResponse response,
T resource,
String changeSetUri,
I identifier,
BaseMaintenanceService<T,R,I> service) {
this.updateHandler.update(
resource,
changeSetUri,
identifier,
service);
response.setStatus(HttpStatus.NO_CONTENT.value());
//TODO: Add a ModelAndView return type
return null;
}
protected <T extends IsChangeable,R extends IsChangeable> Object doCreate(
HttpServletResponse response,
R resource,
String changeSetUri,
String urlTemplate,
UrlTemplateBinder<T> template,
BaseMaintenanceService<T,R,?> service){
T returnedResource = this.createHandler.create(
resource,
changeSetUri,
urlTemplate,
template,
service);
String location = this.urlTemplateBindingCreator.bindResourceToUrlTemplate(
template,
returnedResource,
urlTemplate);
if(StringUtils.isNotBlank(changeSetUri)){
location = location + ("?" + URIHelperInterface.PARAM_CHANGESETCONTEXT + "=" + changeSetUri);
}
this.setLocation(response, location);
//TODO: Add a ModelAndView return type
return null;
}
protected void setLocation(HttpServletResponse response, String location){
location = StringUtils.removeStart(location, "/");
response.setHeader("Location", location);
}
protected SortCriteria resolveSort(QueryControl sort, BaseQueryService queryService) {
if(sort == null || StringUtils.isBlank(sort.getSort())){
return null;
}
Set<? extends ComponentReference> predicates = queryService.getSupportedSortReferences();
ComponentReference ref =
ControllerUtils.getComponentReference(sort.getSort(), predicates);
SortCriterion sortCriterion = new SortCriterion();
sortCriterion.setSortDirection(this.getSortDirection(sort.getSortdirection()));
sortCriterion.setSortElement(ref);
SortCriteria sortCriteria = new SortCriteria();
sortCriteria.addEntry(sortCriterion);
return sortCriteria;
}
private SortDirection getSortDirection(String direction){
if(StringUtils.isBlank(direction) || StringUtils.equals(direction, "descending")){
return SortDirection.DESCENDING;
}
if(StringUtils.equals(direction, "ascending")){
return SortDirection.ASCENDING;
}
throw new StatusSettingCts2RestException(
"Invalid 'sortdirection' parameter.",
400);
}
protected ResolvedReadContext resolveRestReadContext(RestReadContext context){
if(context == null){
return null;
}
ResolvedReadContext resolvedContext = new ResolvedReadContext();
resolvedContext.setChangeSetContextUri(context.getChangesetcontext());
//TODO: Finish this method
return resolvedContext;
}
protected <I> void doExists(
HttpServletResponse httpServletResponse,
ReadService<?,I> readService,
Class<? extends UnknownResourceReference > exceptionClazz,
I id) {
//TODO: ReadContext
boolean exists = readService.exists(id, null);
this.handleExists(id.toString(), exceptionClazz, httpServletResponse, exists);
}
/**
* Handle exists.
*
* @param resourceIdAsString the resource name
* @param exceptionClass the exception class
* @param httpServletResponse the http servlet response
* @param exists the exists
*/
private void handleExists(String resourceIdAsString,
Class<? extends UnknownResourceReference> exceptionClass,
HttpServletResponse httpServletResponse,
boolean exists){
if(exists){
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
} else {
throw ExceptionFactory.createUnknownResourceException(
resourceIdAsString,
exceptionClass);
}
}
protected <R extends IsChangeable, I> ModelAndView doReadByUri(
HttpServletRequest httpServletRequest,
MessageFactory<R> messageFactory,
String byUriTemplate,
String byNameTemaplate,
UrlTemplateBinder<R> urlBinder,
ReadService<R,I> readService,
RestReadContext restReadContext,
Class<? extends UnknownResourceReference > exceptionClazz,
I identifier,
boolean redirect) {
ResolvedReadContext resolvedContext = this.resolveRestReadContext(restReadContext);
R resource =
readService.read(identifier, resolvedContext);
return this.doForward(
resource,
identifier.toString(),
httpServletRequest,
messageFactory,
byUriTemplate,
byNameTemaplate,
urlBinder,
restReadContext,
exceptionClazz,
redirect);
}
protected <R> ModelAndView doForward(
R resource,
String identifier,
HttpServletRequest httpServletRequest,
MessageFactory<R> messageFactory,
String byUriTemplate,
String byNameTemaplate,
UrlTemplateBinder<R> urlBinder,
RestReadContext restReadContext,
Class<? extends UnknownResourceReference > exceptionClazz,
boolean redirect) {
if(resource == null){
throw ExceptionFactory.createUnknownResourceException(
identifier,
exceptionClazz);
}
if(! this.isPartialRedirect(httpServletRequest, byUriTemplate)){
Message msg = messageFactory.createMessage(resource);
msg = this.wrapMessage(msg, byNameTemaplate, urlBinder, resource, httpServletRequest);
return this.forward(httpServletRequest, msg, resource, urlBinder, byNameTemaplate, redirect);
} else {
return this.forward(httpServletRequest, urlBinder, byNameTemaplate, resource, byUriTemplate, redirect);
}
}
@SuppressWarnings("unchecked")
protected <R> ModelAndView forward(
HttpServletRequest httpServletRequest,
UrlTemplateBinder<R> urlBinder,
String urlTemplate,
R resource,
String byUriTemplate,
boolean redirect) {
String url = this.urlTemplateBindingCreator.bindResourceToUrlTemplate(urlBinder, resource, urlTemplate);
String extraUrlPath = StringUtils.substringAfter(httpServletRequest.getRequestURI(), StringUtils.removeEnd(byUriTemplate, ALL_WILDCARD));
if(StringUtils.isNotBlank(extraUrlPath)){
url = url + "/" + extraUrlPath;
}
ModelAndView mav;
if(redirect){
Map<String,Object> parameters =
new HashMap<String,Object>(httpServletRequest.getParameterMap());
parameters.remove(PARAM_REDIRECT);
parameters.remove(PARAM_URI);
mav = new ModelAndView("redirect:" + url + this.mapToQueryString(parameters));
} else {
mav = new ModelAndView("forward:" + url);
}
return mav;
}
private RESTResource getHeading(Map<Object, Object> parameterMap,
String resourceUrl) {
RESTResource resource = new RESTResource();
for (Entry<Object, Object> param : parameterMap.entrySet()) {
Parameter headingParam = new Parameter();
headingParam.setArg(param.getKey().toString());
headingParam.setVal(getParamValue(param.getValue()));
resource.addParameter(headingParam);
}
resource.setAccessDate(new Date());
String urlRoot = this.serverContext.getServerRootWithAppName();
if (!urlRoot.endsWith("/")) {
urlRoot = urlRoot + "/";
}
resource.setResourceRoot(urlRoot);
String resourceRelativeURI = StringUtils.removeStart(resourceUrl, "/");
resource.setResourceURI(resourceRelativeURI);
return resource;
}
/**
* Gets the parameters string.
*
* @param parameters
* the parameters
* @param page
* the page
* @param pageSize
* the page size
* @return the parameters string
*/
protected String getParametersString(Map<String, Object> parameters,
int page, int pageSize) {
parameters = new HashMap<String, Object>(parameters);
parameters.put(URIHelperInterface.PARAM_PAGE, Integer.toString(page));
parameters.put(URIHelperInterface.PARAM_MAXTORETURN,
Integer.toString(pageSize));
return this.mapToQueryString(parameters);
}
protected String mapToQueryString(Map<String, Object> parameters){
if(MapUtils.isNotEmpty(parameters)){
StringBuffer sb = new StringBuffer();
sb.append("?");
for (Entry<String, Object> entry : parameters.entrySet()) {
if (entry.getValue().getClass().isArray()) {
for (Object val : (Object[]) entry.getValue()) {
sb.append(entry.getKey() + "=" + val);
sb.append("&");
}
} else {
sb.append(entry.getKey() + "=" + entry.getValue());
sb.append("&");
}
}
return StringUtils.removeEnd(sb.toString(), "&");
} else {
return "";
}
}
/**
* Parameter value to string.
*
* @param param
* the param
* @return the string
*/
private String parameterValueToString(Object param) {
String paramString;
if (param.getClass().isArray()) {
paramString = ArrayUtils.toString(param);
} else {
paramString = param.toString().trim();
}
if (paramString.startsWith("{")) {
paramString = paramString.substring(1);
}
if (paramString.endsWith("}")) {
paramString = paramString.substring(0, paramString.length() - 1);
}
return paramString;
}
/**
* Gets the param value.
*
* @param value
* the value
* @return the param value
*/
private String getParamValue(Object value) {
if (value == null) {
return null;
}
return parameterValueToString(value);
}
protected FilterResolver getFilterResolver() {
return filterResolver;
}
protected void setFilterResolver(FilterResolver filterResolver) {
this.filterResolver = filterResolver;
}
protected ReadContextResolver getReadContextResolver() {
return readContextResolver;
}
protected void setReadContextResolver(ReadContextResolver readContextResolver) {
this.readContextResolver = readContextResolver;
}
protected ServerContext getServerContext() {
return serverContext;
}
protected void setServerContext(ServerContext serverContext) {
this.serverContext = serverContext;
}
protected UrlTemplateBindingCreator getUrlTemplateBindingCreator() {
return urlTemplateBindingCreator;
}
}