/*
* Copyright 2015 herd contributors
*
* 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 org.finra.herd.rest.config;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.bind.Marshaller;
import org.eclipse.persistence.jaxb.MarshallerProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.http.converter.xml.MarshallingHttpMessageConverter;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.util.PathMatcher;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.resource.ResourceUrlProvider;
import org.springframework.web.util.UrlPathHelper;
import org.finra.herd.core.helper.ConfigurationHelper;
import org.finra.herd.dao.helper.HerdCharacterEscapeHandler;
import org.finra.herd.model.dto.ConfigurationValue;
/**
* REST Spring module configuration. This configuration doesn't use the @EnableWebMvc annotation and instead extends WebMvcConfigurationSupport so we have the
* ability to override configuration methods that we need. One example is that we need to configure our own message converter that can perform XSD validation.
* Note that to override non-bean defined base class methods, we need to add the "@Bean" configuration that calls those methods in this configuration for them
* to get used. In this case, just define the bean and call it's super method to invoke the default processing.
*/
@Configuration
// Component scan all packages, but exclude the configuration ones since they are explicitly specified.
@ComponentScan(value = "org.finra.herd.rest",
excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = "org\\.finra\\.herd\\.rest\\.config\\..*"))
@Import(RestAopSpringModuleConfig.class)
public class RestSpringModuleConfig extends WebMvcConfigurationSupport
{
@Autowired
private ResourcePatternResolver resourceResolver;
@Autowired
private HerdCharacterEscapeHandler herdCharacterEscapeHandler;
@Autowired
private ConfigurationHelper configurationHelper;
/**
* We need to override the base method so this "@Bean" will get invoked and ultimately call configureMessageConverters. Otherwise, it doesn't get called.
* This implementation doesn't do anything except call the super method.
*
* @return the RequestMappingHandlerAdapter.
*/
@Bean
@Override
public HandlerExceptionResolver handlerExceptionResolver()
{
return super.handlerExceptionResolver();
}
/**
* We need to override the base method so this "@Bean" will get invoked and ultimately call configureMessageConverters. Otherwise, it doesn't get called.
* This implementation doesn't do anything except call the super method.
*
* @return the RequestMappingHandlerAdapter.
*/
@Bean
@Override
public RequestMappingHandlerAdapter requestMappingHandlerAdapter()
{
return super.requestMappingHandlerAdapter();
}
/**
* This is called from requestMappingHandlerAdapter to configure the message converters. We override it to configure our own converter in addition to the
* default converters.
*
* @param converters the converter list we configure.
*/
@Override
@SuppressWarnings("rawtypes")
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters)
{
// Add in our custom converter first.
converters.add(marshallingMessageConverter());
// Add in the default converters (e.g. standard JAXB, Jackson, etc.).
addDefaultHttpMessageConverters(converters);
// Remove the Jackson2Xml converter since we want to use JAXB instead when we encounter "application/xml". Otherwise, the XSD auto-generated
// classes with JAXB annotations won't get used.
// Set jackson mapper to include only properties with non-null values.
for (HttpMessageConverter httpMessageConverter : converters)
{
if (httpMessageConverter instanceof MappingJackson2XmlHttpMessageConverter)
{
converters.remove(httpMessageConverter);
break;
}
}
}
/**
* Gets a new marshalling HTTP message converter that is aware of our custom JAXB marshaller.
*
* @return the newly created message converter.
*/
@Bean
public MarshallingHttpMessageConverter marshallingMessageConverter()
{
// Return a new marshalling HTTP message converter with our custom JAXB marshaller.
return new MarshallingHttpMessageConverter(jaxb2Marshaller(), jaxb2Marshaller());
}
/**
* Gets a new JAXB marshaller that is aware of our XSD and can perform schema validation. It is also aware of all our auto-generated classes that are in the
* org.finra.herd.model.api.xml package. Note that REST endpoints that use Java objects which are not in this package will not use this marshaller and will
* not get schema validated which is good since they don't have an XSD.
*
* @return the newly created JAXB marshaller.
*/
@Bean
public Jaxb2Marshaller jaxb2Marshaller()
{
try
{
// Create the marshaller that is aware of our Java XSD and it's auto-generated classes.
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setPackagesToScan("org.finra.herd.model.api.xml");
marshaller.setSchemas(resourceResolver.getResources("classpath:herd.xsd"));
// Get the JAXB XML headers from the environment.
String xmlHeaders = configurationHelper.getProperty(ConfigurationValue.JAXB_XML_HEADERS);
// We need to set marshaller properties to reconfigure the XML header.
Map<String, Object> marshallerProperties = new HashMap<>();
marshaller.setMarshallerProperties(marshallerProperties);
// Remove the header that JAXB will generate.
marshallerProperties.put(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
// Specify the new XML headers.
marshallerProperties.put(ConfigurationValue.JAXB_XML_HEADERS.getKey(), xmlHeaders);
// Specify a custom character escape handler to escape XML 1.1 restricted characters.
marshallerProperties.put(MarshallerProperties.CHARACTER_ESCAPE_HANDLER, herdCharacterEscapeHandler);
// Return the marshaller.
return marshaller;
}
catch (Exception ex)
{
// Throw a runtime exception instead of a checked IOException since the XSD file should be contained within our application.
throw new IllegalArgumentException("Unable to create marshaller.", ex);
}
}
@Bean
@Override
public RequestMappingHandlerMapping requestMappingHandlerMapping()
{
return super.requestMappingHandlerMapping();
}
@Bean
@Override
public ResourceUrlProvider mvcResourceUrlProvider()
{
return super.mvcResourceUrlProvider();
}
@Bean
@Override
public PathMatcher mvcPathMatcher()
{
return super.mvcPathMatcher();
}
@Bean
@Override
public UrlPathHelper mvcUrlPathHelper()
{
return super.mvcUrlPathHelper();
}
/**
* Configure the path match by disabling suffix pattern matching.
*
* @param configurer the path match configurer.
*/
@Override
public void configurePathMatch(PathMatchConfigurer configurer)
{
// Turn off suffix pattern matching which will ensure REST URL's that end with periods and some other text get matched in full and not without
// the period and the following text suffix. This is due to Spring's extension suffix matching logic that we don't need and don't want
// (e.g. .txt could be parsed by a specific handler).
configurer.setUseSuffixPatternMatch(false);
}
}