View Javadoc
1   /**
2    * This file Copyright (c) 2012-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.rest;
35  
36  import info.magnolia.cms.beans.config.ServerConfiguration;
37  import info.magnolia.config.registry.DefinitionMetadata;
38  import info.magnolia.config.registry.DefinitionMetadataBuilder;
39  import info.magnolia.config.registry.DefinitionProvider;
40  import info.magnolia.config.registry.DefinitionProviderBuilder;
41  import info.magnolia.event.EventBus;
42  import info.magnolia.event.HandlerRegistration;
43  import info.magnolia.event.SystemEventBus;
44  import info.magnolia.objectfactory.ComponentProvider;
45  import info.magnolia.objectfactory.Components;
46  import info.magnolia.rest.provider.AdditionalProviderDefinition;
47  import info.magnolia.rest.registry.EndpointDefinitionRegistry;
48  import info.magnolia.rest.registry.EndpointDefinitionRegistryEvent;
49  import info.magnolia.rest.registry.EndpointDefinitionRegistryEventHandler;
50  
51  import java.io.File;
52  import java.util.Arrays;
53  import java.util.HashSet;
54  import java.util.Map;
55  import java.util.Objects;
56  import java.util.Set;
57  import java.util.concurrent.ConcurrentHashMap;
58  import java.util.stream.Collectors;
59  
60  import javax.inject.Inject;
61  import javax.inject.Named;
62  import javax.servlet.ServletConfig;
63  import javax.servlet.ServletException;
64  import javax.ws.rs.core.Application;
65  
66  import org.apache.commons.beanutils.BeanUtils;
67  import org.apache.commons.lang3.StringUtils;
68  import org.jboss.resteasy.plugins.server.servlet.ConfigurationBootstrap;
69  import org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher;
70  import org.jboss.resteasy.plugins.server.servlet.ServletBootstrap;
71  import org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher;
72  import org.jboss.resteasy.spi.ResteasyDeployment;
73  import org.slf4j.Logger;
74  import org.slf4j.LoggerFactory;
75  
76  /**
77   * Dispatcher of the rest requests to the dynamically registered endpoints.
78   */
79  public class RestDispatcherServlet extends HttpServletDispatcher implements EndpointDefinitionRegistryEventHandler {
80  
81      private static final Logger log = LoggerFactory.getLogger(RestDispatcherServlet.class);
82  
83      private final RestIntegrationModule restIntegrationModule;
84      private final EndpointDefinitionRegistry endpointRegistry;
85      private final EventBus systemEventBus;
86      private final Map<String, Object> endpoints = new ConcurrentHashMap<>();
87      private final ComponentProvider componentProvider;
88      private final ServerConfiguration serverConfiguration;
89  
90      private Application application = new Application() {
91          @Override
92          public Set<Object> getSingletons() {
93              HashSet<Object> singletons = new HashSet<>();
94              singletons.addAll(endpoints.values());
95              return singletons;
96          }
97      };
98      private ServletConfig servletConfig;
99      private HandlerRegistration registerHandler;
100 
101     @Inject
102     public RestDispatcherServlet(final RestIntegrationModule restIntegrationModule, EndpointDefinitionRegistry endpointRegistry, @Named(SystemEventBus.NAME) EventBus systemEventBus, ComponentProvider componentProvider, ServerConfiguration serverConfiguration) {
103         this.restIntegrationModule = restIntegrationModule;
104         this.endpointRegistry = endpointRegistry;
105         this.systemEventBus = systemEventBus;
106         this.componentProvider = componentProvider;
107         this.serverConfiguration = serverConfiguration;
108     }
109 
110     @Deprecated
111     public RestDispatcherServlet(final RestIntegrationModule restIntegrationModule, EndpointDefinitionRegistry endpointRegistry, @Named(SystemEventBus.NAME) EventBus systemEventBus) {
112         this(restIntegrationModule, endpointRegistry, systemEventBus, Components.getComponentProvider(), Components.getComponent(ServerConfiguration.class));
113     }
114 
115     @Override
116     public void init(ServletConfig servletConfig) throws ServletException {
117         this.servletConfig = servletConfig;
118 
119         // Initialise the ServletContainerDispatcher
120         servletContainerDispatcher = new ServletContainerDispatcher(servletConfig);
121         ConfigurationBootstrap bootstrap = createBootstrap(servletConfig);
122         servletContainerDispatcher.init(servletConfig.getServletContext(), bootstrap, this, this);
123 
124         // Register configured additional providers
125         restIntegrationModule.getAdditionalProviders().stream()
126                 .map(AdditionalProviderDefinition::getProviderClass)
127                 .filter(Objects::nonNull)
128                 .peek(providerClass -> log.debug("Registering additional provider [{}]", providerClass))
129                 .forEach(providerClass -> super.getDispatcher().getProviderFactory().registerProvider(providerClass));
130 
131         // Register all currently registered endpoints
132         for (DefinitionProvider<EndpointDefinition> provider : endpointRegistry.getAllProviders()) {
133             try {
134                 registerEndpoint(provider);
135             } catch (Exception e) {
136                 log.error("Failed to register endpoint [{}]", provider.getMetadata().getReferenceId(), e);
137                 // Others should continue to be registered.
138             }
139         }
140 
141         // Listen for changes to the registry to observe endpoints being added or removed
142         registerHandler = systemEventBus.addHandler(EndpointDefinitionRegistryEvent.class, this);
143     }
144 
145     private void registerPreviewEndpoint(final DefinitionProvider<EndpointDefinition> provider) {
146         final EndpointDefinition deliveryEndpointDefinition = provider.get();
147         final DefinitionMetadata deliveryMetadata = provider.getMetadata();
148         final String endpointReferenceId = deliveryMetadata.getReferenceId().replace("delivery", "preview");
149         final String path = deliveryEndpointDefinition.getEndpointPath();
150         try {
151             final DefinitionMetadata previewEndpointMetadata = DefinitionMetadataBuilder.usingNameAsId()
152                     .name(endpointReferenceId)
153                     .configurationSourceType(deliveryMetadata.getConfigurationSourceType())
154                     .deprecation(deliveryMetadata.getDeprecation().isPresent() ? deliveryMetadata.getDeprecation().get() : null)
155                     .location(deliveryMetadata.getLocation())
156                     .module(deliveryMetadata.getModule())
157                     .relativeLocation(deliveryMetadata.getRelativeLocation())
158                     .type(deliveryMetadata.getType())
159                     .build();
160             final EndpointDefinitionpointDefinition.html#EndpointDefinition">EndpointDefinition previewEndpointDefinition = (EndpointDefinition) BeanUtils.cloneBean(deliveryEndpointDefinition);
161             if (path != null) {
162                 BeanUtils.setProperty(previewEndpointDefinition, "endpointPath", path.replace("delivery", "preview"));
163             }
164             final DefinitionProvider<EndpointDefinition> previewProvider = DefinitionProviderBuilder.<EndpointDefinition>newBuilder()
165                     .metadata(previewEndpointMetadata)
166                     .rawView(provider.getRaw())
167                     .definition(previewEndpointDefinition)
168                     .build();
169             registerEndpoint(previewProvider);
170 
171         } catch (Exception e) {
172             log.warn("Unable register preview endpoint for {}", deliveryMetadata.getReferenceId(), e);
173         }
174     }
175 
176     private boolean isDeliveryEndpoint(final DefinitionProvider<EndpointDefinition> provider) {
177         return getPath(provider).startsWith("delivery");
178     }
179 
180     private String getPath(final DefinitionProvider<EndpointDefinition> provider) {
181         final EndpointDefinition endpointDefinition = provider.get();
182         String endpointReferenceId = provider.getMetadata().getReferenceId();
183         if (StringUtils.isNotEmpty(endpointDefinition.getEndpointPath())) {
184             return truncatePath(endpointDefinition.getEndpointPath());
185         } else {
186             return getBasePath(endpointReferenceId);
187         }
188     }
189 
190     // need to override this because we're not calling init(ServletConfig) on the super class
191     @Override
192     public ServletConfig getServletConfig() {
193         return servletConfig;
194     }
195 
196     @Override
197     public void destroy() {
198         registerHandler.removeHandler();
199         super.destroy();
200         endpoints.clear();
201     }
202 
203     // EVENT HANDLING
204 
205     @Override
206     public void onEndpointRegistered(EndpointDefinitionRegistryEvent event) {
207         DefinitionProvider<EndpointDefinition> provider = event.getEndpointDefinitionProvider();
208         final String referenceId = provider.getMetadata().getReferenceId();
209         if (endpoints.containsKey(referenceId)) {
210             unregisterEndpoint(referenceId);
211         }
212         registerEndpoint(provider);
213     }
214 
215     @Override
216     public void onEndpointReregistered(EndpointDefinitionRegistryEvent event) {
217         DefinitionProvider<EndpointDefinition> provider = event.getEndpointDefinitionProvider();
218         unregisterEndpoint(provider.getMetadata().getReferenceId());
219         registerEndpoint(provider);
220     }
221 
222     @Override
223     public void onEndpointUnregistered(EndpointDefinitionRegistryEvent event) {
224         unregisterEndpoint(event.getEndpointName());
225     }
226 
227     protected Object registerEndpoint(DefinitionProvider<EndpointDefinition> provider) {
228         if (!provider.isValid()) {
229             return null;
230         }
231 
232         EndpointDefinition endpointDefinition = provider.get();
233         String endpointReferenceId = provider.getMetadata().getReferenceId();
234 
235         if (!endpointDefinition.isEnabled()) {
236             log.info("Endpoint {} is disabled, skipping registration", endpointReferenceId);
237             return null;
238         } else if (endpointDefinition.getImplementationClass() == null) {
239             log.warn("Endpoint {} is missing implementationClass, skipping registration", endpointReferenceId);
240             return null;
241         }
242 
243         Object endpoint = instantiateEndpoint(endpointDefinition);
244 
245         if (supportDynamicPath(endpointDefinition.getImplementationClass())) {
246             String path = getPath(provider);
247             super.getDispatcher().getRegistry().addSingletonResource(endpoint, path);
248             log.info("Endpoint {} is registered with base path: [context]/{}", endpointReferenceId, path);
249         } else {
250             super.getDispatcher().getRegistry().addSingletonResource(endpoint);
251         }
252         endpoints.put(endpointReferenceId, endpoint);
253         // Register preview endpoint
254         if (isDeliveryEndpoint(provider) && serverConfiguration.isAdmin()) {
255             registerPreviewEndpoint(provider);
256         }
257 
258         return endpoint;
259     }
260 
261     /**
262      * Allows an endpoint to be registered for the servlet without actually being in the registry.
263      * This is used for REST Tools in particular; otherwise {@link #registerEndpoint(DefinitionProvider)} should be preferred.
264      */
265     protected Object registerEndpoint(EndpointDefinition endpointDefinition) {
266         if (!endpointDefinition.isEnabled()) {
267             return null;
268         }
269         Object endpoint = instantiateEndpoint(endpointDefinition);
270         endpoints.put(endpointDefinition.getName(), endpoint);
271         super.getDispatcher().getRegistry().addSingletonResource(endpoint);
272         return endpoint;
273     }
274 
275     protected void unregisterEndpoint(String endpointReferenceId) {
276         Object endpoint = endpoints.remove(endpointReferenceId);
277         if (endpoint != null) {
278             Class<?> endpointClass = endpoint.getClass();
279             if (supportDynamicPath(endpointClass) && endpoint instanceof AbstractEndpoint) {
280                 String configuredPath = ((AbstractEndpoint) endpoint).getEndpointDefinition().getEndpointPath();
281                 String path = StringUtils.isEmpty(configuredPath) ? getBasePath(endpointReferenceId) : truncatePath(configuredPath);
282                 super.getDispatcher().getRegistry().removeRegistrations(endpointClass, path);
283                 log.debug("Unregister endpoint {} with base path {} from registry.", endpointReferenceId, path);
284             } else {
285                 super.getDispatcher().getRegistry().removeRegistrations(endpointClass);
286                 log.debug("Unregister endpoint which has reference id: {} from registry.", endpointReferenceId);
287             }
288 
289         }
290         // Unregister preview endpoint
291         if (endpointReferenceId.startsWith("delivery")) {
292             unregisterEndpoint(endpointReferenceId.replace("delivery", "preview"));
293         }
294     }
295 
296     protected Object instantiateEndpoint(EndpointDefinition endpointDefinition) {
297         return componentProvider.newInstance(endpointDefinition.getImplementationClass(), endpointDefinition);
298     }
299 
300     // DEPLOYMENT
301 
302     protected Application getApplication() {
303         return application;
304     }
305 
306     protected ConfigurationBootstrap createBootstrap(ServletConfig servletConfig) {
307         return new ServletBootstrap(servletConfig) {
308             @Override
309             public ResteasyDeployment createDeployment() {
310                 return configureDeployment(super.createDeployment());
311             }
312         };
313     }
314 
315     protected ResteasyDeployment configureDeployment(ResteasyDeployment deployment) {
316         deployment.setApplicationClass(null);
317         deployment.setApplication(getApplication());
318         return deployment;
319     }
320 
321     /**
322      * Extract base path from preference id and definition path.
323      * Only applicable for {@linkplain DynamicPath} annotated endpoint class.
324      * Convention:
325      * /module_name/restEndpoints/p1/p2/p3/def_v1.yaml -> {context}/p1/p2/p3/def/v1
326      * /module_name/restEndpoints/p1/p2/p3/defv1.yaml -> {context}/p1/p2/p3/defv1
327      * /module_name/restEndpoints/def_v1.yaml -> {context}/def/v1
328      * /module_name/restEndpoints/defv1.yaml -> {context}/defv1
329      *
330      * @return base path
331      */
332     protected String getBasePath(String endpointReferenceId) {
333         final String[] endpointPaths = StringUtils.split(endpointReferenceId, File.separator);
334         endpointPaths[endpointPaths.length - 1] = endpointPaths[endpointPaths.length - 1].replace('_', '/');
335         return Arrays.stream(endpointPaths).collect(Collectors.joining("/"));
336     }
337 
338     private boolean supportDynamicPath(final Class<?> implementationClass) {
339         return implementationClass != null && implementationClass.getAnnotation(DynamicPath.class) != null;
340     }
341 
342     private String truncatePath(String configuredPath) {
343         String path = StringUtils.removeEnd(configuredPath, "/");
344         path = StringUtils.removeStart(path, "/");
345         return path;
346     }
347 }