/* (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 org.geoserver.catalog.Catalog;
import org.geoserver.catalog.SLDHandler;
import org.geoserver.catalog.StyleHandler;
import org.geoserver.catalog.Styles;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.rest.converters.*;
import org.geotools.util.Version;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.util.UrlPathHelper;
import org.xml.sax.EntityResolver;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
/**
* Configure various aspects of Spring MVC, in particular message converters
*/
@Configuration
public class RestConfiguration extends WebMvcConfigurationSupport {
private ContentNegotiationManager contentNegotiationManager;
@Autowired
private ApplicationContext applicationContext;
/**
* Return a {@link ContentNegotiationManager} instance to use to determine
* requested {@linkplain MediaType media types} in a given request.
*/
@Override
@Bean
public ContentNegotiationManager mvcContentNegotiationManager() {
if (this.contentNegotiationManager == null) {
this.contentNegotiationManager = super.mvcContentNegotiationManager();
this.contentNegotiationManager.getStrategies().add(0, new DelegatingContentNegotiationStrategy());
}
return this.contentNegotiationManager;
}
/**
* Allows extension point configuration of {@link ContentNegotiationStrategy}s
*/
private static class DelegatingContentNegotiationStrategy implements ContentNegotiationStrategy {
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
List<ContentNegotiationStrategy> strategies = GeoServerExtensions.extensions(ContentNegotiationStrategy.class);
List<MediaType> mediaTypes;
for (ContentNegotiationStrategy strategy : strategies) {
if (!(strategy instanceof ContentNegotiationManager || strategy instanceof DelegatingContentNegotiationStrategy)) {
mediaTypes = strategy.resolveMediaTypes(webRequest);
if (mediaTypes.size() > 0) {
return mediaTypes;
}
}
}
return new ArrayList<>();
}
}
@Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Catalog catalog = (Catalog) applicationContext.getBean("catalog");
List<BaseMessageConverter> gsConverters = GeoServerExtensions.extensions(BaseMessageConverter.class);
//Add default converters
gsConverters.add(new FreemarkerHTMLMessageConverter("UTF-8"));
gsConverters.add(new XStreamXMLMessageConverter());
gsConverters.add(new XStreamJSONMessageConverter());
gsConverters.add(new XStreamCatalogListConverter.XMLXStreamListConverter());
gsConverters.add(new XStreamCatalogListConverter.JSONXStreamListConverter());
gsConverters.add(new InputStreamConverter());
//Deal with the various Style handler
EntityResolver entityResolver = catalog.getResourcePool().getEntityResolver();
for (StyleHandler sh : Styles.handlers()) {
for (Version ver : sh.getVersions()) {
gsConverters.add(new StyleReaderConverter(sh.mimeType(ver), ver, sh, entityResolver));
gsConverters.add(new StyleWriterConverter(sh.mimeType(ver), ver, sh));
}
}
//Sort the converters based on ExtensionPriority
gsConverters.sort(Comparator.comparingInt(BaseMessageConverter::getPriority));
for (BaseMessageConverter converter : gsConverters) {
converters.add(converter);
}
//use the default ones as lowest priority
super.addDefaultHttpMessageConverters(converters);
}
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RestInterceptor());
registry.addInterceptor(new CallbackInterceptor());
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// scan and register media types for style handlers
List<StyleHandler> styleHandlers = GeoServerExtensions.extensions(StyleHandler.class);
for (StyleHandler handler : styleHandlers) {
if(handler.getVersions() != null && handler.getVersions().size() > 0) {
// Spring configuration allows associating a single mime to extensions, pick the latest
List<Version> versions = handler.getVersions();
final Version firstVersion = versions.get(versions.size() - 1);
configurer.mediaType(handler.getFormat(), MediaType.valueOf(handler.mimeType(firstVersion)));
}
}
// manually force SLD to v10 for backwards compatibility
configurer.mediaType("sld", MediaType.valueOf(SLDHandler.MIMETYPE_10));
// other common media types
configurer.mediaType("html", MediaType.TEXT_HTML);
configurer.mediaType("xml", MediaType.APPLICATION_XML);
configurer.mediaType("json", MediaType.APPLICATION_JSON);
configurer.mediaType("xslt", MediaType.valueOf("application/xslt+xml"));
configurer.mediaType("ftl", MediaType.TEXT_PLAIN);
configurer.mediaType("xml", MediaType.APPLICATION_XML);
configurer.favorParameter(true);
// allow extension point configuration of media types
List<MediaTypeCallback> callbacks = GeoServerExtensions.extensions(MediaTypeCallback.class);
for (MediaTypeCallback callback : callbacks) {
callback.configure(configurer);
}
// configurer.favorPathExtension(true);
//todo properties files are only supported for test cases. should try to find a way to
//support them without polluting prod code with handling
// configurer.mediaType("properties", MediaType.valueOf("application/prs.gs.psl"));
}
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
//Force MVC to use /restng endpoint. If we need something more advanced, we should make a custom PathHelper
configurer.setUrlPathHelper(new GeoServerUrlPathHelper());
configurer.getUrlPathHelper().setAlwaysUseFullPath(true);
}
static class GeoServerUrlPathHelper extends UrlPathHelper {
public GeoServerUrlPathHelper() {
setAlwaysUseFullPath(true);
setDefaultEncoding("UTF-8");
}
@Override
public String decodeRequestString(HttpServletRequest request, String source) {
// compatibility with old Restlet based config, it also decodes "+" into space
try {
return URLDecoder.decode(source, "UTF-8");
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
}