View Javadoc
1   /**
2    * This file Copyright (c) 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.rest.registry.EndpointDefinitionRegistry;
42  import info.magnolia.rest.registry.EndpointDefinitionRegistryEvent;
43  import info.magnolia.rest.registry.EndpointDefinitionRegistryEventHandler;
44  
45  import java.io.File;
46  import java.lang.reflect.Method;
47  import java.util.Arrays;
48  import java.util.Map;
49  import java.util.Set;
50  import java.util.concurrent.ConcurrentHashMap;
51  import java.util.stream.Collectors;
52  
53  import javax.inject.Inject;
54  import javax.inject.Named;
55  import javax.servlet.ServletException;
56  import javax.ws.rs.Path;
57  import javax.ws.rs.ext.Provider;
58  
59  import org.apache.commons.collections4.CollectionUtils;
60  import org.apache.commons.lang3.StringUtils;
61  import org.glassfish.jersey.server.ResourceConfig;
62  import org.glassfish.jersey.server.model.Resource;
63  import org.glassfish.jersey.server.model.ResourceMethod;
64  import org.glassfish.jersey.server.model.ResourceModel;
65  import org.glassfish.jersey.servlet.ServletContainer;
66  import org.glassfish.jersey.servlet.WebConfig;
67  import org.reflections.Reflections;
68  import org.slf4j.Logger;
69  import org.slf4j.LoggerFactory;
70  
71  /**
72   * Custom Jersey Dispatcher which handles endpoint registrations.
73   */
74  public class RestJerseyDispatcherServlet extends ServletContainer implements EndpointDefinitionRegistryEventHandler {
75  
76      private static final Logger log = LoggerFactory.getLogger(RestJerseyDispatcherServlet.class);
77  
78      private final RestIntegrationModule restIntegrationModule;
79      private final EndpointDefinitionRegistry endpointRegistry;
80      private final EventBus systemEventBus;
81      private final Map<String, Object> endpoints = new ConcurrentHashMap<>();
82      private final ComponentProvider componentProvider;
83  
84      private HandlerRegistration registerHandler;
85  
86      @Inject
87      public RestJerseyDispatcherServlet(final RestIntegrationModule restIntegrationModule, EndpointDefinitionRegistry endpointRegistry, @Named(SystemEventBus.NAME) EventBus systemEventBus, ComponentProvider componentProvider) {
88          super(new ResourceConfig());
89  
90          this.restIntegrationModule = restIntegrationModule;
91          this.endpointRegistry = endpointRegistry;
92          this.systemEventBus = systemEventBus;
93          this.componentProvider = componentProvider;
94      }
95  
96      @Override
97      public void init(WebConfig webConfig) throws ServletException {
98          super.init(webConfig);
99  
100         ResourceConfig config = new ResourceConfig();
101 
102         Reflections reflection = new Reflections("info.magnolia.rest");
103         final Set<Class<?>> providers = reflection.getTypesAnnotatedWith(Provider.class);
104         config.registerClasses(providers);
105 
106         log.info("Registered scanned Provider classes: {}", providers.stream().map(c -> c.getName()).collect(Collectors.joining(", ")));
107 
108         // Register all currently registered endpoints
109         for (DefinitionProvider<EndpointDefinition> provider : endpointRegistry.getAllProviders()) {
110             try {
111                 registerEndpoint(provider, config);
112             } catch (Exception e) {
113                 log.error("Failed to register endpoint [{}]", provider.getMetadata().getReferenceId(), e);
114             }
115         }
116 
117         reload(config);
118 
119         // Listen for changes to the registry to observe endpoints being added or removed
120         registerHandler = systemEventBus.addHandler(EndpointDefinitionRegistryEvent.class, this);
121     }
122 
123     @Override
124     public void destroy() {
125         super.destroy();
126         registerHandler.removeHandler();
127         endpoints.clear();
128         log.info("RestJerseyDispatcherServlet disposed.");
129     }
130 
131     // EVENT HANDLING
132 
133     @Override
134     public void onEndpointRegistered(EndpointDefinitionRegistryEvent event) {
135         DefinitionProvider<EndpointDefinition> provider = event.getEndpointDefinitionProvider();
136         final String referenceId = provider.getMetadata().getReferenceId();
137         if (endpoints.containsKey(referenceId)) {
138             unregisterEndpoint(referenceId);
139         }
140         registerEndpoint(provider, null);
141     }
142 
143     @Override
144     public void onEndpointReregistered(EndpointDefinitionRegistryEvent event) {
145         DefinitionProvider<EndpointDefinition> provider = event.getEndpointDefinitionProvider();
146         unregisterEndpoint(provider.getMetadata().getReferenceId());
147         registerEndpoint(provider, null);
148     }
149 
150     @Override
151     public void onEndpointUnregistered(EndpointDefinitionRegistryEvent event) {
152         unregisterEndpoint(event.getEndpointName());
153     }
154 
155     protected Object registerEndpoint(DefinitionProvider<EndpointDefinition> provider, ResourceConfig config) {
156         if (provider.isValid()) {
157             boolean registerLocally = config == null;
158             if (registerLocally) {
159                 final ResourceConfig originalConfig = getConfiguration();
160                 config = new ResourceConfig();
161                 config.registerClasses(originalConfig.getClasses());
162                 config.registerResources(originalConfig.getResources());
163                 for (Object i : originalConfig.getInstances()) {
164                     config.register(i);
165                 }
166 
167             }
168             EndpointDefinition endpointDefinition = provider.get();
169             String endpointReferenceId = provider.getMetadata().getReferenceId();
170 
171             if (endpointDefinition.isEnabled()) {
172                 Object endpoint = instantiateEndpoint(endpointDefinition);
173                 endpoints.put(endpointReferenceId, endpoint);
174 
175                 if (supportDynamicPath(endpoint.getClass())) {
176 
177                     String path;
178                     if (StringUtils.isNotEmpty(endpointDefinition.getEndpointPath())) {
179                         path = truncatePath(endpointDefinition.getEndpointPath());
180                     } else {
181                         path = getBasePath(endpointReferenceId);
182                     }
183 
184                     final Resource.Builder resourceBuilder = Resource.builder(path);
185 
186                     ResourceModel.Builder builder = new ResourceModel.Builder(false);
187                     builder.addResource(Resource.from(endpointDefinition.getImplementationClass()));
188                     final ResourceModel resourceModel = builder.build();
189 
190                     for (Resource c : resourceModel.getResources()) {
191                         if (CollectionUtils.isNotEmpty(c.getResourceMethods())) {
192                             for (ResourceMethod m : c.getResourceMethods()) {
193                                 final Method handlingMethod = m.getInvocable().getHandlingMethod();
194                                 log.debug("Add path {}", c.getPath());
195                                 resourceBuilder.addChildResource(c.getPath())
196                                         .addMethod(m)
197                                         .handledBy(endpoint, handlingMethod);
198                             }
199                         }
200                         for (Resource cr : c.getChildResources()) {
201                             for (ResourceMethod m : cr.getResourceMethods()) {
202                                 final Method handlingMethod = m.getInvocable().getHandlingMethod();
203                                 final Path annotation = handlingMethod.getAnnotation(Path.class);
204                                 final String relativePath = new String(c.getPath() + annotation.value()).replaceAll("//", "/");
205                                 log.debug("Add child path {}", relativePath);
206                                 resourceBuilder.addChildResource(relativePath)
207                                         .addMethod(m)
208                                         .handledBy(endpoint, handlingMethod);
209                             }
210                         }
211                     }
212 
213                     config.registerResources(resourceBuilder.build());
214                 } else {
215                     config.register(endpoint);
216                 }
217 
218                 if (registerLocally) {
219                     reload(config);
220                 }
221 
222                 return endpoint;
223             } else {
224                 log.info("Endpoint {} is disabled, skipping registration", endpointReferenceId);
225             }
226         }
227 
228         return null;
229     }
230 
231     protected void unregisterEndpoint(String endpointReferenceId) {
232         Object endpoint = endpoints.remove(endpointReferenceId);
233 
234         if (endpoint == null) {
235             return;
236         }
237 
238         Class<?> endpointClass = endpoint.getClass();
239         ResourceConfig config = new ResourceConfig();
240         ResourceConfig orgConfig = getConfiguration();
241 
242         if (supportDynamicPath(endpointClass) && endpoint instanceof AbstractEndpoint) {
243             String configuredPath = ((AbstractEndpoint) endpoint).getEndpointDefinition().getEndpointPath();
244             String path = StringUtils.isEmpty(configuredPath) ? getBasePath(endpointReferenceId) : truncatePath(configuredPath);
245 
246             // re-register legacy resource except the un-registered one.
247             orgConfig.getResources().stream()
248                     .filter(r -> !r.getPath().startsWith(path))
249                     .forEach(r -> config.registerResources(r));
250 
251             // non-dynamic endpoints kept as is.
252             orgConfig.getInstances().forEach(i -> config.register(i));
253 
254             log.debug("Unregister endpoint {} with base path {} from registry.", endpointReferenceId, path);
255         } else {
256 
257             // re-register instances except removed one.
258             orgConfig.getInstances().stream()
259                     .filter(instance -> instance != endpoint)
260                     .forEach(instance -> config.register(instance));
261 
262             // keep resources as is
263             orgConfig.getResources().forEach(r -> config.registerResources(r));
264 
265             log.debug("Unregister endpoint which has reference id: {} from registry.", endpointReferenceId);
266         }
267         config.registerClasses(orgConfig.getClasses());
268         reload(config);
269     }
270 
271     protected Object instantiateEndpoint(EndpointDefinition endpointDefinition) {
272         return componentProvider.newInstance(endpointDefinition.getImplementationClass(), endpointDefinition);
273     }
274 
275     /**
276      * Extract base path from preference id and definition path.
277      * Only applicable for {@linkplain DynamicPath} annotated endpoint class.
278      * Convention:
279      * /module_name/restEndpoints/p1/p2/p3/def_v1.yaml -> {context}/p1/p2/p3/def/v1
280      * /module_name/restEndpoints/p1/p2/p3/defv1.yaml -> {context}/p1/p2/p3/defv1
281      * /module_name/restEndpoints/def_v1.yaml -> {context}/def/v1
282      * /module_name/restEndpoints/defv1.yaml -> {context}/defv1
283      *
284      * @return base path
285      */
286     protected String getBasePath(String endpointReferenceId) {
287         final String[] endpointPaths = StringUtils.split(endpointReferenceId, File.separator);
288         endpointPaths[endpointPaths.length - 1] = endpointPaths[endpointPaths.length - 1].replace('_', '/');
289         return Arrays.stream(endpointPaths).collect(Collectors.joining("/"));
290     }
291 
292     private boolean supportDynamicPath(final Class<?> implementationClass) {
293         return implementationClass != null && implementationClass.getAnnotation(DynamicPath.class) != null;
294     }
295 
296     private String truncatePath(String configuredPath) {
297         String path = StringUtils.removeEnd(configuredPath, "/");
298         path = StringUtils.removeStart(path, "/");
299         return path;
300     }
301 
302 }