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.config.registry.DefinitionProvider;
37  import info.magnolia.event.EventBus;
38  import info.magnolia.event.HandlerRegistration;
39  import info.magnolia.event.SystemEventBus;
40  import info.magnolia.objectfactory.ComponentProvider;
41  import info.magnolia.objectfactory.Components;
42  import info.magnolia.rest.provider.AdditionalProviderDefinition;
43  import info.magnolia.rest.registry.EndpointDefinitionRegistry;
44  import info.magnolia.rest.registry.EndpointDefinitionRegistryEvent;
45  import info.magnolia.rest.registry.EndpointDefinitionRegistryEventHandler;
46  
47  import java.io.File;
48  import java.util.Arrays;
49  import java.util.HashSet;
50  import java.util.Map;
51  import java.util.Set;
52  import java.util.concurrent.ConcurrentHashMap;
53  import java.util.stream.Collectors;
54  
55  import javax.inject.Inject;
56  import javax.inject.Named;
57  import javax.servlet.ServletConfig;
58  import javax.servlet.ServletException;
59  import javax.ws.rs.core.Application;
60  
61  import org.apache.commons.lang3.StringUtils;
62  import org.jboss.resteasy.plugins.server.servlet.ConfigurationBootstrap;
63  import org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher;
64  import org.jboss.resteasy.plugins.server.servlet.ServletBootstrap;
65  import org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher;
66  import org.jboss.resteasy.spi.ResteasyDeployment;
67  import org.slf4j.Logger;
68  import org.slf4j.LoggerFactory;
69  
70  /**
71   * Dispatcher of the rest requests to the dynamically registered endpoints.
72   */
73  public class RestDispatcherServlet extends HttpServletDispatcher implements EndpointDefinitionRegistryEventHandler {
74  
75      private static final Logger log = LoggerFactory.getLogger(RestDispatcherServlet.class);
76  
77      private final RestIntegrationModule restIntegrationModule;
78      private final EndpointDefinitionRegistry endpointRegistry;
79      private final EventBus systemEventBus;
80      private final Map<String, Object> endpoints = new ConcurrentHashMap<>();
81      private final ComponentProvider componentProvider;
82  
83      private Application application = new Application() {
84          @Override
85          public Set<Object> getSingletons() {
86              HashSet<Object> singletons = new HashSet<>();
87              singletons.addAll(endpoints.values());
88              return singletons;
89          }
90      };
91      private ServletConfig servletConfig;
92      private HandlerRegistration registerHandler;
93  
94      @Inject
95      public RestDispatcherServlet(final RestIntegrationModule restIntegrationModule, EndpointDefinitionRegistry endpointRegistry, @Named(SystemEventBus.NAME) EventBus systemEventBus, ComponentProvider componentProvider) {
96          this.restIntegrationModule = restIntegrationModule;
97          this.endpointRegistry = endpointRegistry;
98          this.systemEventBus = systemEventBus;
99          this.componentProvider = componentProvider;
100     }
101 
102     @Deprecated
103     public RestDispatcherServlet(final RestIntegrationModule restIntegrationModule, EndpointDefinitionRegistry endpointRegistry, @Named(SystemEventBus.NAME) EventBus systemEventBus) {
104         this(restIntegrationModule, endpointRegistry, systemEventBus, Components.getComponentProvider());
105     }
106 
107     @Override
108     public void init(ServletConfig servletConfig) throws ServletException {
109         this.servletConfig = servletConfig;
110 
111         // Initialise the ServletContainerDispatcher
112         servletContainerDispatcher = new ServletContainerDispatcher();
113         ConfigurationBootstrap bootstrap = createBootstrap(servletConfig);
114         servletContainerDispatcher.init(servletConfig.getServletContext(), bootstrap, this, this);
115         servletContainerDispatcher.getDispatcher().getDefaultContextObjects().put(ServletConfig.class, servletConfig);
116 
117         // Register additional providers that have been configured
118         for (AdditionalProviderDefinition provider : restIntegrationModule.getAdditionalProviders()) {
119             log.debug("Registering additional provider [{}]", provider.getProviderClass());
120             super.getDispatcher().getProviderFactory().registerProvider(provider.getProviderClass());
121         }
122 
123         // Register all currently registered endpoints
124         for (DefinitionProvider<EndpointDefinition> provider : endpointRegistry.getAllProviders()) {
125             try {
126                 registerEndpoint(provider);
127             } catch (Exception e) {
128                 log.error("Failed to register endpoint [{}]", provider.getMetadata().getReferenceId(), e);
129                 // Others should continue to be registered.
130             }
131         }
132 
133         // Listen for changes to the registry to observe endpoints being added or removed
134         registerHandler = systemEventBus.addHandler(EndpointDefinitionRegistryEvent.class, this);
135     }
136 
137     @Override
138     public void destroy() {
139         registerHandler.removeHandler();
140         super.destroy();
141         endpoints.clear();
142     }
143 
144     // EVENT HANDLING
145 
146     @Override
147     public void onEndpointRegistered(EndpointDefinitionRegistryEvent event) {
148         DefinitionProvider<EndpointDefinition> provider = event.getEndpointDefinitionProvider();
149         final String referenceId = provider.getMetadata().getReferenceId();
150         if (endpoints.containsKey(referenceId)) {
151             unregisterEndpoint(referenceId);
152         }
153         registerEndpoint(provider);
154     }
155 
156     @Override
157     public void onEndpointReregistered(EndpointDefinitionRegistryEvent event) {
158         DefinitionProvider<EndpointDefinition> provider = event.getEndpointDefinitionProvider();
159         unregisterEndpoint(provider.getMetadata().getReferenceId());
160         registerEndpoint(provider);
161     }
162 
163     @Override
164     public void onEndpointUnregistered(EndpointDefinitionRegistryEvent event) {
165         unregisterEndpoint(event.getEndpointName());
166     }
167 
168     // need to override this because we're not calling init(ServletConfig) on the super class
169     @Override
170     public ServletConfig getServletConfig() {
171         return servletConfig;
172     }
173 
174     /**
175      * Allows an endpoint to be registered for the servlet without actually being in the registry.
176      * This is used for REST Tools in particular; otherwise {@link #registerEndpoint(DefinitionProvider)} should be preferred.
177      */
178     protected Object registerEndpoint(EndpointDefinition endpointDefinition) {
179         if (!endpointDefinition.isEnabled()) {
180             return null;
181         }
182         Object endpoint = instantiateEndpoint(endpointDefinition);
183         endpoints.put(endpointDefinition.getName(), endpoint);
184         super.getDispatcher().getRegistry().addSingletonResource(endpoint);
185         return endpoint;
186     }
187 
188     protected Object registerEndpoint(DefinitionProvider<EndpointDefinition> provider) {
189         if (!provider.isValid()) {
190             return null;
191         }
192 
193         EndpointDefinition endpointDefinition = provider.get();
194         String endpointReferenceId = provider.getMetadata().getReferenceId();
195 
196         if (!endpointDefinition.isEnabled()) {
197             log.info("Endpoint %s is disabled, skipping registration", endpointReferenceId);
198             return null;
199         }
200 
201         Object endpoint = instantiateEndpoint(endpointDefinition);
202 
203         if (supportDynamicPath(endpointDefinition.getImplementationClass())) {
204             String path;
205             if (StringUtils.isNotEmpty(endpointDefinition.getEndpointPath())) {
206                 path = truncatePath(endpointDefinition.getEndpointPath());
207             } else {
208                 path = getBasePath(endpointReferenceId);
209             }
210             super.getDispatcher().getRegistry().addSingletonResource(endpoint, path);
211             log.info("Endpoint {} is registered with base path: [context]/{}", endpointReferenceId, path);
212             endpoints.put(endpointReferenceId, endpoint);
213         } else {
214             super.getDispatcher().getRegistry().addSingletonResource(endpoint);
215             endpoints.put(endpointReferenceId, endpoint);
216         }
217 
218         return endpoint;
219     }
220 
221     protected void unregisterEndpoint(String endpointReferenceId) {
222         Object endpoint = endpoints.remove(endpointReferenceId);
223         if (endpoint != null) {
224             Class<?> endpointClass = endpoint.getClass();
225             if (supportDynamicPath(endpointClass) && endpoint instanceof AbstractEndpoint) {
226                 String configuredPath = ((AbstractEndpoint) endpoint).getEndpointDefinition().getEndpointPath();
227                 String path = StringUtils.isEmpty(configuredPath) ? getBasePath(endpointReferenceId) : truncatePath(configuredPath);
228                 super.getDispatcher().getRegistry().removeRegistrations(endpointClass, path);
229                 log.debug("Unregister endpoint {} with base path {} from registry.", endpointReferenceId, path);
230             } else {
231                 super.getDispatcher().getRegistry().removeRegistrations(endpointClass);
232                 log.debug("Unregister endpoint which has reference id: {} from registry.", endpointReferenceId);
233             }
234 
235         }
236     }
237 
238     protected Object instantiateEndpoint(EndpointDefinition endpointDefinition) {
239         return componentProvider.newInstance(endpointDefinition.getImplementationClass(), endpointDefinition);
240     }
241 
242     // DEPLOYMENT
243 
244     protected Application getApplication() {
245         return application;
246     }
247 
248     protected ConfigurationBootstrap createBootstrap(ServletConfig servletConfig) {
249         return new ServletBootstrap(servletConfig) {
250             @Override
251             public ResteasyDeployment createDeployment() {
252                 return configureDeployment(super.createDeployment());
253             }
254         };
255     }
256 
257     protected ResteasyDeployment configureDeployment(ResteasyDeployment deployment) {
258         deployment.setApplicationClass(null);
259         deployment.setApplication(getApplication());
260         return deployment;
261     }
262 
263     /**
264      * Extract base path from preference id and definition path.
265      * Only applicable for {@linkplain DynamicPath} annotated endpoint class.
266      * Convention:
267      * /module_name/restEndpoints/p1/p2/p3/def_v1.yaml -> {context}/p1/p2/p3/def/v1
268      * /module_name/restEndpoints/p1/p2/p3/defv1.yaml -> {context}/p1/p2/p3/defv1
269      * /module_name/restEndpoints/def_v1.yaml -> {context}/def/v1
270      * /module_name/restEndpoints/defv1.yaml -> {context}/defv1
271      *
272      * @return base path
273      */
274     protected String getBasePath(String endpointReferenceId) {
275         final String[] endpointPaths = StringUtils.split(endpointReferenceId, File.separator);
276         endpointPaths[endpointPaths.length - 1] = endpointPaths[endpointPaths.length - 1].replace('_', '/');
277         return Arrays.stream(endpointPaths).collect(Collectors.joining("/"));
278     }
279 
280     private boolean supportDynamicPath(final Class<?> implementationClass) {
281         return implementationClass != null && implementationClass.getAnnotation(DynamicPath.class) != null;
282     }
283 
284     private String truncatePath(String configuredPath) {
285         String path = StringUtils.removeEnd(configuredPath, "/");
286         path = StringUtils.removeStart(path, "/");
287         return path;
288     }
289 }