001/*
002 * Copyright 2012-2018 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.Collections;
020import java.util.EnumSet;
021
022import javax.annotation.PostConstruct;
023import javax.servlet.DispatcherType;
024import javax.servlet.ServletContext;
025import javax.servlet.ServletException;
026import javax.servlet.ServletRegistration;
027import javax.ws.rs.ApplicationPath;
028import javax.ws.rs.ext.ContextResolver;
029
030import com.fasterxml.jackson.databind.AnnotationIntrospector;
031import com.fasterxml.jackson.databind.ObjectMapper;
032import com.fasterxml.jackson.databind.cfg.MapperConfig;
033import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.glassfish.jersey.jackson.JacksonFeature;
037import org.glassfish.jersey.server.ResourceConfig;
038import org.glassfish.jersey.servlet.ServletContainer;
039import org.glassfish.jersey.servlet.ServletProperties;
040
041import org.springframework.beans.factory.ObjectProvider;
042import org.springframework.boot.autoconfigure.AutoConfigureAfter;
043import org.springframework.boot.autoconfigure.AutoConfigureBefore;
044import org.springframework.boot.autoconfigure.AutoConfigureOrder;
045import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
046import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
047import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
048import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
049import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
050import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
051import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
052import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
053import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
054import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
055import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
056import org.springframework.boot.context.properties.EnableConfigurationProperties;
057import org.springframework.boot.web.servlet.DynamicRegistrationBean;
058import org.springframework.boot.web.servlet.FilterRegistrationBean;
059import org.springframework.boot.web.servlet.ServletRegistrationBean;
060import org.springframework.context.annotation.Bean;
061import org.springframework.context.annotation.Configuration;
062import org.springframework.core.Ordered;
063import org.springframework.core.annotation.AnnotationUtils;
064import org.springframework.core.annotation.Order;
065import org.springframework.util.ClassUtils;
066import org.springframework.util.StringUtils;
067import org.springframework.web.WebApplicationInitializer;
068import org.springframework.web.context.ServletContextAware;
069
070/**
071 * {@link EnableAutoConfiguration Auto-configuration} for Jersey.
072 *
073 * @author Dave Syer
074 * @author Andy Wilkinson
075 * @author EddĂș MelĂ©ndez
076 * @author Stephane Nicoll
077 */
078@Configuration
079@ConditionalOnClass(name = { "org.glassfish.jersey.server.spring.SpringComponentProvider",
080                "javax.servlet.ServletRegistration" })
081@ConditionalOnBean(type = "org.glassfish.jersey.server.ResourceConfig")
082@ConditionalOnWebApplication(type = Type.SERVLET)
083@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
084@AutoConfigureBefore(DispatcherServletAutoConfiguration.class)
085@AutoConfigureAfter(JacksonAutoConfiguration.class)
086@EnableConfigurationProperties(JerseyProperties.class)
087public class JerseyAutoConfiguration implements ServletContextAware {
088
089        private static final Log logger = LogFactory.getLog(JerseyAutoConfiguration.class);
090
091        private final JerseyProperties jersey;
092
093        private final ResourceConfig config;
094
095        private final ObjectProvider<ResourceConfigCustomizer> customizers;
096
097        public JerseyAutoConfiguration(JerseyProperties jersey, ResourceConfig config,
098                        ObjectProvider<ResourceConfigCustomizer> customizers) {
099                this.jersey = jersey;
100                this.config = config;
101                this.customizers = customizers;
102        }
103
104        @PostConstruct
105        public void path() {
106                customize();
107        }
108
109        private void customize() {
110                this.customizers.orderedStream()
111                                .forEach((customizer) -> customizer.customize(this.config));
112        }
113
114        @Bean
115        @ConditionalOnMissingBean
116        public JerseyApplicationPath jerseyApplicationPath() {
117                return this::resolveApplicationPath;
118        }
119
120        private String resolveApplicationPath() {
121                if (StringUtils.hasLength(this.jersey.getApplicationPath())) {
122                        return this.jersey.getApplicationPath();
123                }
124                return findApplicationPath(AnnotationUtils.findAnnotation(
125                                this.config.getApplication().getClass(), ApplicationPath.class));
126        }
127
128        @Bean
129        @ConditionalOnMissingBean(name = "jerseyFilterRegistration")
130        @ConditionalOnProperty(prefix = "spring.jersey", name = "type", havingValue = "filter")
131        public FilterRegistrationBean<ServletContainer> jerseyFilterRegistration(
132                        JerseyApplicationPath applicationPath) {
133                FilterRegistrationBean<ServletContainer> registration = new FilterRegistrationBean<>();
134                registration.setFilter(new ServletContainer(this.config));
135                registration.setUrlPatterns(
136                                Collections.singletonList(applicationPath.getUrlMapping()));
137                registration.setOrder(this.jersey.getFilter().getOrder());
138                registration.addInitParameter(ServletProperties.FILTER_CONTEXT_PATH,
139                                stripPattern(applicationPath.getPath()));
140                addInitParameters(registration);
141                registration.setName("jerseyFilter");
142                registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
143                return registration;
144        }
145
146        private String stripPattern(String path) {
147                if (path.endsWith("/*")) {
148                        path = path.substring(0, path.lastIndexOf("/*"));
149                }
150                return path;
151        }
152
153        @Bean
154        @ConditionalOnMissingBean(name = "jerseyServletRegistration")
155        @ConditionalOnProperty(prefix = "spring.jersey", name = "type", havingValue = "servlet", matchIfMissing = true)
156        public ServletRegistrationBean<ServletContainer> jerseyServletRegistration(
157                        JerseyApplicationPath applicationPath) {
158                ServletRegistrationBean<ServletContainer> registration = new ServletRegistrationBean<>(
159                                new ServletContainer(this.config), applicationPath.getUrlMapping());
160                addInitParameters(registration);
161                registration.setName(getServletRegistrationName());
162                registration.setLoadOnStartup(this.jersey.getServlet().getLoadOnStartup());
163                return registration;
164        }
165
166        private String getServletRegistrationName() {
167                return ClassUtils.getUserClass(this.config.getClass()).getName();
168        }
169
170        private void addInitParameters(DynamicRegistrationBean<?> registration) {
171                this.jersey.getInit().forEach(registration::addInitParameter);
172        }
173
174        private static String findApplicationPath(ApplicationPath annotation) {
175                // Jersey doesn't like to be the default servlet, so map to /* as a fallback
176                if (annotation == null) {
177                        return "/*";
178                }
179                return annotation.value();
180        }
181
182        @Override
183        public void setServletContext(ServletContext servletContext) {
184                String servletRegistrationName = getServletRegistrationName();
185                ServletRegistration registration = servletContext
186                                .getServletRegistration(servletRegistrationName);
187                if (registration != null) {
188                        if (logger.isInfoEnabled()) {
189                                logger.info("Configuring existing registration for Jersey servlet '"
190                                                + servletRegistrationName + "'");
191                        }
192                        registration.setInitParameters(this.jersey.getInit());
193                }
194        }
195
196        @Order(Ordered.HIGHEST_PRECEDENCE)
197        public static final class JerseyWebApplicationInitializer
198                        implements WebApplicationInitializer {
199
200                @Override
201                public void onStartup(ServletContext servletContext) throws ServletException {
202                        // We need to switch *off* the Jersey WebApplicationInitializer because it
203                        // will try and register a ContextLoaderListener which we don't need
204                        servletContext.setInitParameter("contextConfigLocation", "<NONE>");
205                }
206
207        }
208
209        @ConditionalOnClass(JacksonFeature.class)
210        @ConditionalOnSingleCandidate(ObjectMapper.class)
211        @Configuration
212        static class JacksonResourceConfigCustomizer {
213
214                private static final String JAXB_ANNOTATION_INTROSPECTOR_CLASS_NAME = "com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector";
215
216                @Bean
217                public ResourceConfigCustomizer resourceConfigCustomizer(
218                                final ObjectMapper objectMapper) {
219                        addJaxbAnnotationIntrospectorIfPresent(objectMapper);
220                        return (ResourceConfig config) -> {
221                                config.register(JacksonFeature.class);
222                                config.register(new ObjectMapperContextResolver(objectMapper),
223                                                ContextResolver.class);
224                        };
225                }
226
227                private void addJaxbAnnotationIntrospectorIfPresent(ObjectMapper objectMapper) {
228                        if (ClassUtils.isPresent(JAXB_ANNOTATION_INTROSPECTOR_CLASS_NAME,
229                                        getClass().getClassLoader())) {
230                                new ObjectMapperCustomizer().addJaxbAnnotationIntrospector(objectMapper);
231                        }
232                }
233
234                private static final class ObjectMapperCustomizer {
235
236                        private void addJaxbAnnotationIntrospector(ObjectMapper objectMapper) {
237                                JaxbAnnotationIntrospector jaxbAnnotationIntrospector = new JaxbAnnotationIntrospector(
238                                                objectMapper.getTypeFactory());
239                                objectMapper.setAnnotationIntrospectors(
240                                                createPair(objectMapper.getSerializationConfig(),
241                                                                jaxbAnnotationIntrospector),
242                                                createPair(objectMapper.getDeserializationConfig(),
243                                                                jaxbAnnotationIntrospector));
244                        }
245
246                        private AnnotationIntrospector createPair(MapperConfig<?> config,
247                                        JaxbAnnotationIntrospector jaxbAnnotationIntrospector) {
248                                return AnnotationIntrospector.pair(config.getAnnotationIntrospector(),
249                                                jaxbAnnotationIntrospector);
250                        }
251
252                }
253
254                private static final class ObjectMapperContextResolver
255                                implements ContextResolver<ObjectMapper> {
256
257                        private final ObjectMapper objectMapper;
258
259                        private ObjectMapperContextResolver(ObjectMapper objectMapper) {
260                                this.objectMapper = objectMapper;
261                        }
262
263                        @Override
264                        public ObjectMapper getContext(Class<?> type) {
265                                return this.objectMapper;
266                        }
267
268                }
269
270        }
271
272}