/* * 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.constants.ModelAndViewInterface; import edu.mayo.cts2.framework.core.constants.URIHelperInterface; import edu.mayo.cts2.framework.core.util.EncodingUtils; import edu.mayo.cts2.framework.model.command.Page; import edu.mayo.cts2.framework.model.core.ScopedEntityName; import edu.mayo.cts2.framework.model.core.URIAndEntityName; import edu.mayo.cts2.framework.model.exception.ExceptionFactory; import edu.mayo.cts2.framework.model.exception.UnspecifiedCts2Exception; import edu.mayo.cts2.framework.model.service.core.types.LoggingLevel; import edu.mayo.cts2.framework.model.service.exception.CTS2Exception; import edu.mayo.cts2.framework.webapp.rest.command.RestFilter; import edu.mayo.cts2.framework.webapp.rest.command.RestFilters; import edu.mayo.cts2.framework.webapp.rest.config.RestConfig; import edu.mayo.cts2.framework.webapp.rest.exception.Cts2RestExceptionCodeMapper; import edu.mayo.cts2.framework.webapp.rest.exception.StatusSettingCts2RestException; import edu.mayo.cts2.framework.webapp.service.AbstractServiceAwareBean; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.ConversionNotSupportedException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.util.UrlPathHelper; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.Date; import java.util.HashSet; import java.util.Set; /** * The base abstract Spring MVC Controller. * * @author <a href="mailto:kevin.peterson@mayo.edu">Kevin Peterson</a> */ public abstract class AbstractController extends AbstractServiceAwareBean implements URIHelperInterface, ModelAndViewInterface { protected Log log = LogFactory.getLog(getClass()); protected static final String DEFAULT_REDIRECT = "true"; @Resource private RestConfig restConfig; @Resource private Cts2RestExceptionCodeMapper cts2RestExceptionCodeMapper; private UrlPathHelper urlPathHelper = new UrlPathHelper(); /** * Decode uri. * * @param uri the uri * @return the string */ protected String decodeUri(String uri){ try { return URLDecoder.decode(uri, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new IllegalStateException(e); } } /** * Handle exception. * * @param response the response * @param ex the ex * @return the model and view */ @ExceptionHandler(CTS2Exception.class) @ResponseBody public CTS2Exception handleException(HttpServletRequest request, HttpServletResponse response, CTS2Exception ex) { int status = this.cts2RestExceptionCodeMapper.getErrorCode(ex); try { //Horrible hack for Firefox - https://github.com/cts2/cts2-framework/issues/30 - https://bugzilla.mozilla.org/show_bug.cgi?id=907800 if (status == 408 && request.getHeader("User-Agent").indexOf("Firefox") > 0) { status = 508; } } catch (Exception e) { // noop - just want to ensure this hack doesn't break other things unexpectedly } if(ex.getSeverity() == null){ ex.setSeverity(LoggingLevel.ERROR); } response.setStatus(status); return ex; } @ExceptionHandler(StatusSettingCts2RestException.class) @ResponseBody public CTS2Exception handleException(HttpServletResponse response, StatusSettingCts2RestException ex) { response.setStatus(ex.getStatusCode()); return ex; } /** * Handle exception. * * @param response the response * @param request the request * @param ex the ex * @return the model and view */ @ExceptionHandler(Throwable.class) @ResponseBody public CTS2Exception handleException( HttpServletResponse response, HttpServletRequest request, RuntimeException ex) { String errorId = Long.toString(new Date().getTime()); log.error("Unexpected Error: " + errorId, ex); int status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; response.setStatus(status); boolean showStackTrace = this.getRestConfig().getShowStackTraceOnError(); if(showStackTrace){ return ExceptionFactory.createUnknownException( ex, getUrlString(request), getParameterString(request), true); } else { return ExceptionFactory.createUnknownException( this.getErrorSupportMessage(errorId), getUrlString(request), getParameterString(request)); } } protected String getErrorSupportMessage(String errorId){ String preamble = "An Unexpected error has occurred."; String supportEmail = this.getRestConfig().getSupportEmail(); if(StringUtils.isNotBlank(supportEmail)){ preamble = preamble + "\nPlease contact " + supportEmail + " and reference Log ID: " + errorId; } return preamble; } /** * Handle exception. * * @param response the response * @param request the request * @param ex the ex * @return the model and view */ @ExceptionHandler(UnsupportedOperationException.class) @ResponseBody public CTS2Exception handleException( HttpServletResponse response, HttpServletRequest request, UnsupportedOperationException ex) { int status = HttpServletResponse.SC_NOT_IMPLEMENTED; response.setStatus(status); return ExceptionFactory.createUnknownException( "Method not implemented. " + ex.getMessage() != null ? ex.getMessage() : "", getUrlString(request), getParameterString(request)); } /** * Gets the url string. * * @param request the request * @return the url string */ private String getUrlString(HttpServletRequest request){ return request.getContextPath(); } /** * Gets the parameter string. * * @param request the request * @return the parameter string */ private String getParameterString(HttpServletRequest request){ return request.getQueryString(); } /** * Sets the count. * * @param count the count * @param httpServletResponse the http servlet response */ protected void setCount(int count, HttpServletResponse httpServletResponse) { httpServletResponse.setHeader(HEADER_COUNT, Integer.toString(count)); } protected boolean isPartialRedirect(HttpServletRequest request, String urlTemplatePath){ String adjustedTemplate = StringUtils.removeEnd(urlTemplatePath, ALL_WILDCARD); String contextPath = this.getUrlPathHelper().getContextPath(request); String requestUri = StringUtils.removeStart(request.getRequestURI(),contextPath); return ! (StringUtils.removeStart(StringUtils.removeEnd(requestUri, "/"), "/").equals( StringUtils.removeStart( StringUtils.removeEnd(adjustedTemplate, "/"),"/"))); } @ExceptionHandler(ConversionNotSupportedException.class) @ResponseBody public UnspecifiedCts2Exception handleException( HttpServletResponse response, HttpServletRequest request, ConversionNotSupportedException ex) { int status = HttpServletResponse.SC_BAD_REQUEST; response.setStatus(status); Class<?> requiredType = ex.getRequiredType(); String typeName = requiredType.getSimpleName(); String value = ex.getValue().toString(); String possibleValues = ""; if(requiredType.isEnum()){ StringBuilder sb = new StringBuilder(); sb.append(" Possible values include: "); Object[] values = requiredType.getEnumConstants(); sb.append(StringUtils.join(values, ", ")); possibleValues = sb.toString(); } return ExceptionFactory.createUnknownException( "Cannot convert value " + value + " to the type " + typeName + "." + possibleValues, getUrlString(request), getParameterString(request)); } /** * Gets the end. * * @param page the page * @return the end */ protected int getEnd(Page page){ return ( page.getPage() + 1 ) * page.getMaxToReturn(); } /** * Checks if is last page. * * @param page the page * @param pageSize the page size * @return true, if is last page */ protected boolean isLastPage(Page page, int pageSize){ return pageSize < page.getMaxToReturn(); } /** * Gets the start. * * @param page the page * @return the start */ protected int getStart(Page page){ return page.getPage() * page.getMaxToReturn(); } @InitBinder public void initPageBinder( WebDataBinder binder, @RequestParam(value=PARAM_MAXTORETURN, required=false) Integer maxToReturn, @RequestParam(value=PARAM_PAGE, required=false) Integer pageNumber) { if(binder.getTarget() instanceof Page){ Page page = (Page) binder.getTarget(); if(maxToReturn != null){ page.setMaxToReturn(maxToReturn); } if(pageNumber != null){ page.setPage(pageNumber); } } } @InitBinder public void initEntityDescriptionRestrictionBinder( WebDataBinder binder, @RequestParam(value=PARAM_FILTERCOMPONENT, required=false) String filterComponent, @RequestParam(value=PARAM_MATCHALGORITHM, required=false) String matchAlgorithm, @RequestParam(value=PARAM_MATCHVALUE, required=false) String matchValue) { if(binder.getTarget() instanceof RestFilters){ RestFilters filters = (RestFilters) binder.getTarget(); if(StringUtils.isNotBlank(matchValue)){ RestFilter filter = new RestFilter(); if(StringUtils.isNotBlank(filterComponent)){ filter.setFiltercomponent(filterComponent); } if(StringUtils.isNotBlank(matchAlgorithm)){ filter.setMatchalgorithm(matchAlgorithm); } filter.setMatchvalue(matchValue); filters.getRestFilters().add(filter); } } } /** * Gets the scoped entity name. * * @param entityName the entity name * @param codeSystemName the code system name * @return the scoped entity name */ protected ScopedEntityName getScopedEntityName(String entityName, String codeSystemName){ ScopedEntityName scopedName = new ScopedEntityName(); String[] nameParts = entityName.split(":"); if(nameParts.length == 2){ scopedName.setNamespace(nameParts[0]); scopedName.setName(nameParts[1]); } else if(nameParts.length == 1){ scopedName.setNamespace(codeSystemName); scopedName.setName(nameParts[0]); } else { throw new IllegalStateException(); } return scopedName; } protected <T> Set<T> createSet(T element){ Set<T> set = new HashSet<T>(); set.add(element); return set; } protected ScopedEntityName getScopedEntityName(String encodedEntityName){ return EncodingUtils.decodeEntityName(encodedEntityName); } protected String getScopedEntityName(ScopedEntityName scopedEntityName){ return EncodingUtils.encodeScopedEntityName(scopedEntityName); } protected String getScopedEntityName(URIAndEntityName uriAndEntityName){ return EncodingUtils.encodeScopedEntityName(uriAndEntityName); } protected UrlPathHelper getUrlPathHelper() { return urlPathHelper; } protected RestConfig getRestConfig() { return restConfig; } protected void setRestConfig(RestConfig restConfig) { this.restConfig = restConfig; } }