/* * 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; } }