/* (c) 2017 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.rest; import freemarker.core.ParseException; import freemarker.template.*; import org.geoserver.config.util.XStreamPersister; import org.geoserver.rest.converters.FreemarkerHTMLMessageConverter; import org.geoserver.rest.converters.XStreamMessageConverter; import org.geoserver.rest.wrapper.RestHttpInputWrapper; import org.geoserver.rest.wrapper.RestListWrapper; import org.geoserver.rest.wrapper.RestWrapper; import org.geoserver.rest.wrapper.RestWrapperAdapter; import org.geotools.util.logging.Logging; import org.springframework.core.MethodParameter; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice; import java.io.IOException; import java.lang.reflect.Type; import java.util.Collection; import java.util.logging.Level; import java.util.logging.Logger; /** * Base class for all rest controllers * * Extending classes should be annotated with {@link org.springframework.web.bind.annotation.RestController} so that * they are automatically instantiated as a Controller bean. * * Custom configuration can be added to XStreamPersister by overriding {@link #configurePersister(XStreamPersister, XStreamMessageConverter)} * Custom configuration can be added to Freemarker by calling {@link #configureFreemarker(FreemarkerHTMLMessageConverter, Template)} * * Any extending classes which override {@link #configurePersister(XStreamPersister, XStreamMessageConverter)}, and * require this configuration for reading objects from incoming requests must also be annotated with * {@link org.springframework.web.bind.annotation.ControllerAdvice} and override the {@link #supports(MethodParameter, Type, Class)} * method. * * Any response objects that should be encoded using either {@link XStreamMessageConverter} or {@link FreemarkerHTMLMessageConverter} * should be wrapped in a {@link RestWrapper} by calling {@link #wrapObject(Object, Class)}. * Any response objects that should be encoded using {@link org.geoserver.rest.converters.XStreamCatalogListConverter} * should be wrapped by calling {@link #wrapList(Collection, Class)} */ public abstract class RestBaseController implements RequestBodyAdvice { private static final Logger LOGGER = Logging.getLogger("org.geoserver.rest"); /** * Root path of the rest api */ public static final String ROOT_PATH = "/rest"; /** * Default encoding for the freemarker {@link Configuration} */ protected String encoding = "UTF-8"; /** * Name of the folder containing freemarker templates */ protected String pathPrefix = "ftl-templates"; /** * Constructs the freemarker {@link Configuration} * * @param clazz Class of the object being wrapped * @return */ protected <T> Configuration createConfiguration(Class<T> clazz) { Configuration cfg = new Configuration( ); cfg.setObjectWrapper(createObjectWrapper(clazz)); cfg.setClassForTemplateLoading(getClass(),pathPrefix); if (encoding != null) { cfg.setDefaultEncoding(encoding); } return cfg; } /** * Constructs the freemarker {@link ObjectWrapper} * * @param clazz Class of the object being wrapped * @return */ protected <T> ObjectWrapper createObjectWrapper(Class<T> clazz) { return new ObjectToMapWrapper<>(clazz); } /** * Finds a freemarker {@link Template} based on the object and {@link Configuration} * @param o Object being serialized * @param clazz Class of the object * @return Freemarker template */ protected Template getTemplate(Object o, Class clazz) { Template template = null; Configuration configuration = createConfiguration(clazz); //first try finding a name directly String templateName = getTemplateName( o ); if ( templateName != null ) { template = tryLoadTemplate(configuration, templateName); if(template == null) template = tryLoadTemplate(configuration, templateName + ".ftl"); } final RequestInfo requestInfo = RequestInfo.get(); //next look up by the resource being requested if ( template == null && requestInfo != null ) { //could not find a template bound to the class directly, search by the resource // being requested String pagePath = requestInfo.getPagePath(); String r = pagePath.substring(pagePath.lastIndexOf('/')+1); //trim trailing slash if(r.equals("")) { pagePath = pagePath.substring(0, pagePath.length() - 1); r = pagePath.substring(pagePath.lastIndexOf('/')+1); } int i = r.lastIndexOf( "." ); if ( i != -1 ) { r = r.substring( 0, i ); } template = tryLoadTemplate(configuration, r + ".ftl"); } //finally try to find by class while( template == null && clazz != null ) { template = tryLoadTemplate(configuration, clazz.getSimpleName() + ".ftl"); if (template == null) { template = tryLoadTemplate(configuration, clazz.getSimpleName().toLowerCase() + ".ftl"); } if(template == null) { for (Class<?> interfaze : clazz.getInterfaces()) { template = tryLoadTemplate(configuration, interfaze.getSimpleName() + ".ftl" ); if(template != null) break; } } //move up the class hierarchy to continue to look for a matching template if ( clazz.getSuperclass() == Object.class ) { break; } clazz = clazz.getSuperclass(); } if ( template != null ) { templateName = template.getName(); } else { //use a fallback templateName = "Object.ftl"; } return tryLoadTemplate(configuration, templateName); } /** * Tries to load a template, will return null if it's not found. If the template exists * but it contains syntax errors an exception will be thrown instead * * @param configuration The template configuration. * @param templateName The name of the template to load. */ protected Template tryLoadTemplate(Configuration configuration, String templateName) { try { return configuration.getTemplate(templateName); } catch(ParseException e) { throw new RuntimeException(e); } catch(IOException io) { LOGGER.log(Level.FINE, "Failed to lookup template " + templateName, io); return null; } } /** * Template method to get a custom template name * * @param object The object being serialized. */ protected String getTemplateName(Object object) { return null; } /** * Wraps the passed collection in a {@link RestListWrapper} * * @param list The collection to wrap * @param clazz The advertised class to use for the collection contents * @return */ protected <T> RestWrapper<T> wrapList(Collection<T> list, Class<T> clazz) { return new RestListWrapper<>(list, clazz, this, getTemplate(list, clazz)); } /** * Wraps the passed object in a {@link RestWrapperAdapter} * * @param object The object to wrap * @param clazz The advertised class to use for the collection contents * @return */ protected <T> RestWrapper<T> wrapObject(T object, Class<T> clazz) { return new RestWrapperAdapter<>(object, clazz, this, getTemplate(object, clazz)); } /** * Wraps the passed object in a {@link RestWrapperAdapter} * * @param object The object to wrap * @param clazz The advertised class to use for the collection contents * @param errorMessage The error message to return if the object is null. * @param quietOnNotFound The value of the quietOnNotFound parameter * @return */ protected <T> RestWrapper<T> wrapObject(T object, Class<T> clazz, String errorMessage, Boolean quietOnNotFound) { errorMessage = quietOnNotFound != null && quietOnNotFound ? "" : errorMessage; if (object == null){ throw new RestException(errorMessage, HttpStatus.NOT_FOUND); } return new RestWrapperAdapter<>(object, clazz, this, getTemplate(object, clazz)); } @Override /** * Any subclass that implements {@link #configurePersister(XStreamPersister, XStreamMessageConverter)} and require * this configuration for reading objects from incoming requests should override this method to return true when * called from the appropriate controller, and should also be annotated with * {@link org.springframework.web.bind.annotation.ControllerAdvice} */ public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return false; } @Override public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return body; } @Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { if (!(inputMessage instanceof RestHttpInputWrapper)) { return new RestHttpInputWrapper(inputMessage, this); } return inputMessage; } @Override public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return body; } /** * Default (empty) implementation of configurePersister. This will be called by the default implementations of * {@link RestWrapper#configurePersister(XStreamPersister, XStreamMessageConverter)} and * {@link RestHttpInputWrapper#configurePersister(XStreamPersister, XStreamMessageConverter)} constructed by * {@link #wrapObject(Object, Class)}, {@link #wrapList(Collection, Class)}, and * {@link #beforeBodyRead(HttpInputMessage, MethodParameter, Type, Class)} * * Subclasses should override this to implement custom functionality * * @param persister */ public void configurePersister(XStreamPersister persister, XStreamMessageConverter converter) { } /** * Default (empty) implementation of configurePersister. This will be called by the default implementation of * {@link RestWrapper#configurePersister(XStreamPersister, XStreamMessageConverter)}, constructed by * {@link #wrapObject(Object, Class)}, and {@link #wrapList(Collection, Class)} * * Subclasses should override this to implement custom functionality * * @param converter */ public void configureFreemarker(FreemarkerHTMLMessageConverter converter, Template template) { } }