001/*
002 * Copyright 2012-2016 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.autoconfigure.jersey;
018
019import java.util.Arrays;
020import java.util.EnumSet;
021import java.util.List;
022import java.util.Map.Entry;
023
024import javax.annotation.PostConstruct;
025import javax.servlet.DispatcherType;
026import javax.servlet.ServletContext;
027import javax.servlet.ServletException;
028import javax.servlet.ServletRegistration;
029import javax.ws.rs.ApplicationPath;
030import javax.ws.rs.ext.ContextResolver;
031
032import com.fasterxml.jackson.databind.AnnotationIntrospector;
033import com.fasterxml.jackson.databind.ObjectMapper;
034import com.fasterxml.jackson.databind.cfg.MapperConfig;
035import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
036import org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038import org.glassfish.jersey.CommonProperties;
039import org.glassfish.jersey.jackson.JacksonFeature;
040import org.glassfish.jersey.server.ResourceConfig;
041import org.glassfish.jersey.servlet.ServletContainer;
042import org.glassfish.jersey.servlet.ServletProperties;
043
044import org.springframework.beans.factory.ObjectProvider;
045import org.springframework.boot.autoconfigure.AutoConfigureAfter;
046import org.springframework.boot.autoconfigure.AutoConfigureBefore;
047import org.springframework.boot.autoconfigure.AutoConfigureOrder;
048import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
049import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
050import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
051import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
052import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
053import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
054import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
055import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
056import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration;
057import org.springframework.boot.context.properties.EnableConfigurationProperties;
058import org.springframework.boot.web.servlet.FilterRegistrationBean;
059import org.springframework.boot.web.servlet.RegistrationBean;
060import org.springframework.boot.web.servlet.ServletRegistrationBean;
061import org.springframework.context.annotation.Bean;
062import org.springframework.context.annotation.Configuration;
063import org.springframework.core.Ordered;
064import org.springframework.core.annotation.AnnotationAwareOrderComparator;
065import org.springframework.core.annotation.AnnotationUtils;
066import org.springframework.core.annotation.Order;
067import org.springframework.util.ClassUtils;
068import org.springframework.util.StringUtils;
069import org.springframework.web.WebApplicationInitializer;
070import org.springframework.web.context.ServletContextAware;
071import org.springframework.web.filter.RequestContextFilter;
072
073/**
074 * {@link EnableAutoConfiguration Auto-configuration} for Jersey.
075 *
076 * @author Dave Syer
077 * @author Andy Wilkinson
078 * @author Eddú Meléndez
079 * @author Stephane Nicoll
080 */
081@Configuration
082@ConditionalOnClass(name = { "org.glassfish.jersey.server.spring.SpringComponentProvider",
083                "javax.servlet.ServletRegistration" })
084@ConditionalOnBean(type = "org.glassfish.jersey.server.ResourceConfig")
085@ConditionalOnWebApplication
086@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
087@AutoConfigureBefore(DispatcherServletAutoConfiguration.class)
088@AutoConfigureAfter(JacksonAutoConfiguration.class)
089@EnableConfigurationProperties(JerseyProperties.class)
090public class JerseyAutoConfiguration implements ServletContextAware {
091
092        private static final Log logger = LogFactory.getLog(JerseyAutoConfiguration.class);
093
094        private final JerseyProperties jersey;
095
096        private final ResourceConfig config;
097
098        private final List<ResourceConfigCustomizer> customizers;
099
100        private String path;
101
102        public JerseyAutoConfiguration(JerseyProperties jersey, ResourceConfig config,
103                        ObjectProvider<List<ResourceConfigCustomizer>> customizers) {
104                this.jersey = jersey;
105                this.config = config;
106                this.customizers = customizers.getIfAvailable();
107        }
108
109        @PostConstruct
110        public void path() {
111                resolveApplicationPath();
112                customize();
113        }
114
115        private void resolveApplicationPath() {
116                if (StringUtils.hasLength(this.jersey.getApplicationPath())) {
117                        this.path = parseApplicationPath(this.jersey.getApplicationPath());
118                }
119                else {
120                        this.path = findApplicationPath(AnnotationUtils
121                                        .findAnnotation(this.config.getClass(), ApplicationPath.class));
122                }
123        }
124
125        private void customize() {
126                if (this.customizers != null) {
127                        AnnotationAwareOrderComparator.sort(this.customizers);
128                        for (ResourceConfigCustomizer customizer : this.customizers) {
129                                customizer.customize(this.config);
130                        }
131                }
132        }
133
134        @Bean
135        @ConditionalOnMissingBean
136        public FilterRegistrationBean requestContextFilter() {
137                FilterRegistrationBean registration = new FilterRegistrationBean();
138                registration.setFilter(new RequestContextFilter());
139                registration.setOrder(this.jersey.getFilter().getOrder() - 1);
140                registration.setName("requestContextFilter");
141                return registration;
142        }
143
144        @Bean
145        @ConditionalOnMissingBean(name = "jerseyFilterRegistration")
146        @ConditionalOnProperty(prefix = "spring.jersey", name = "type", havingValue = "filter")
147        public FilterRegistrationBean jerseyFilterRegistration() {
148                FilterRegistrationBean registration = new FilterRegistrationBean();
149                registration.setFilter(new ServletContainer(this.config));
150                registration.setUrlPatterns(Arrays.asList(this.path));
151                registration.setOrder(this.jersey.getFilter().getOrder());
152                registration.addInitParameter(ServletProperties.FILTER_CONTEXT_PATH,
153                                stripPattern(this.path));
154                addInitParameters(registration);
155                registration.setName("jerseyFilter");
156                registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
157                return registration;
158        }
159
160        private String stripPattern(String path) {
161                if (path.endsWith("/*")) {
162                        path = path.substring(0, path.lastIndexOf("/*"));
163                }
164                return path;
165        }
166
167        @Bean
168        @ConditionalOnMissingBean(name = "jerseyServletRegistration")
169        @ConditionalOnProperty(prefix = "spring.jersey", name = "type", havingValue = "servlet", matchIfMissing = true)
170        public ServletRegistrationBean jerseyServletRegistration() {
171                ServletRegistrationBean registration = new ServletRegistrationBean(
172                                new ServletContainer(this.config), this.path);
173                addInitParameters(registration);
174                registration.setName(getServletRegistrationName());
175                registration.setLoadOnStartup(this.jersey.getServlet().getLoadOnStartup());
176                return registration;
177        }
178
179        private String getServletRegistrationName() {
180                return ClassUtils.getUserClass(this.config.getClass()).getName();
181        }
182
183        private void addInitParameters(RegistrationBean registration) {
184                for (Entry<String, String> entry : this.jersey.getInit().entrySet()) {
185                        registration.addInitParameter(entry.getKey(), entry.getValue());
186                }
187        }
188
189        private static String findApplicationPath(ApplicationPath annotation) {
190                // Jersey doesn't like to be the default servlet, so map to /* as a fallback
191                if (annotation == null) {
192                        return "/*";
193                }
194                return parseApplicationPath(annotation.value());
195        }
196
197        private static String parseApplicationPath(String applicationPath) {
198                if (!applicationPath.startsWith("/")) {
199                        applicationPath = "/" + applicationPath;
200                }
201                return applicationPath.equals("/") ? "/*" : applicationPath + "/*";
202        }
203
204        @Override
205        public void setServletContext(ServletContext servletContext) {
206                String servletRegistrationName = getServletRegistrationName();
207                ServletRegistration registration = servletContext
208                                .getServletRegistration(servletRegistrationName);
209                if (registration != null) {
210                        if (logger.isInfoEnabled()) {
211                                logger.info("Configuring existing registration for Jersey servlet '"
212                                                + servletRegistrationName + "'");
213                        }
214                        registration.setInitParameters(this.jersey.getInit());
215                        registration.setInitParameter(
216                                        CommonProperties.METAINF_SERVICES_LOOKUP_DISABLE,
217                                        Boolean.TRUE.toString());
218                }
219        }
220
221        @Order(Ordered.HIGHEST_PRECEDENCE)
222        public static final class JerseyWebApplicationInitializer
223                        implements WebApplicationInitializer {
224
225                @Override
226                public void onStartup(ServletContext servletContext) throws ServletException {
227                        // We need to switch *off* the Jersey WebApplicationInitializer because it
228                        // will try and register a ContextLoaderListener which we don't need
229                        servletContext.setInitParameter("contextConfigLocation", "<NONE>");
230                }
231
232        }
233
234        @ConditionalOnClass(JacksonFeature.class)
235        @ConditionalOnSingleCandidate(ObjectMapper.class)
236        @Configuration
237        static class JacksonResourceConfigCustomizer {
238
239                private static final String JAXB_ANNOTATION_INTROSPECTOR_CLASS_NAME = "com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector";
240
241                @Bean
242                public ResourceConfigCustomizer resourceConfigCustomizer(
243                                final ObjectMapper objectMapper) {
244                        addJaxbAnnotationIntrospectorIfPresent(objectMapper);
245                        return new ResourceConfigCustomizer() {
246                                @Override
247                                public void customize(ResourceConfig config) {
248                                        config.register(JacksonFeature.class);
249                                        config.register(new ObjectMapperContextResolver(objectMapper),
250                                                        ContextResolver.class);
251                                }
252                        };
253                }
254
255                private void addJaxbAnnotationIntrospectorIfPresent(ObjectMapper objectMapper) {
256                        if (ClassUtils.isPresent(JAXB_ANNOTATION_INTROSPECTOR_CLASS_NAME,
257                                        getClass().getClassLoader())) {
258                                new ObjectMapperCustomizer().addJaxbAnnotationIntrospector(objectMapper);
259                        }
260                }
261
262                private static final class ObjectMapperCustomizer {
263
264                        private void addJaxbAnnotationIntrospector(ObjectMapper objectMapper) {
265                                JaxbAnnotationIntrospector jaxbAnnotationIntrospector = new JaxbAnnotationIntrospector(
266                                                objectMapper.getTypeFactory());
267                                objectMapper.setAnnotationIntrospectors(
268                                                createPair(objectMapper.getSerializationConfig(),
269                                                                jaxbAnnotationIntrospector),
270                                                createPair(objectMapper.getDeserializationConfig(),
271                                                                jaxbAnnotationIntrospector));
272                        }
273
274                        private AnnotationIntrospector createPair(MapperConfig<?> config,
275                                        JaxbAnnotationIntrospector jaxbAnnotationIntrospector) {
276                                return AnnotationIntrospector.pair(config.getAnnotationIntrospector(),
277                                                jaxbAnnotationIntrospector);
278                        }
279
280                }
281
282                private static final class ObjectMapperContextResolver
283                                implements ContextResolver<ObjectMapper> {
284
285                        private final ObjectMapper objectMapper;
286
287                        private ObjectMapperContextResolver(ObjectMapper objectMapper) {
288                                this.objectMapper = objectMapper;
289                        }
290
291                        @Override
292                        public ObjectMapper getContext(Class<?> type) {
293                                return this.objectMapper;
294                        }
295
296                }
297
298        }
299
300}