1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.opencastproject.kernel.rest;
23
24 import org.opencastproject.rest.RestConstants;
25 import org.opencastproject.rest.StaticResource;
26 import org.opencastproject.security.api.UnauthorizedException;
27 import org.opencastproject.systems.OpencastConstants;
28 import org.opencastproject.util.NotFoundException;
29
30 import org.apache.cxf.Bus;
31 import org.apache.cxf.BusFactory;
32 import org.apache.cxf.binding.BindingFactoryManager;
33 import org.apache.cxf.endpoint.Server;
34 import org.apache.cxf.jaxrs.JAXRSBindingFactory;
35 import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
36 import org.apache.cxf.jaxrs.openapi.OpenApiCustomizer;
37 import org.apache.cxf.jaxrs.openapi.OpenApiFeature;
38 import org.apache.cxf.jaxrs.provider.json.JSONProvider;
39 import org.apache.cxf.transport.servlet.CXFNonSpringServlet;
40 import org.apache.http.HttpStatus;
41 import org.codehaus.jettison.mapped.Configuration;
42 import org.codehaus.jettison.mapped.MappedNamespaceConvention;
43 import org.codehaus.jettison.mapped.MappedXMLStreamWriter;
44 import org.osgi.framework.Bundle;
45 import org.osgi.framework.BundleContext;
46 import org.osgi.framework.BundleEvent;
47 import org.osgi.framework.InvalidSyntaxException;
48 import org.osgi.framework.ServiceReference;
49 import org.osgi.framework.ServiceRegistration;
50 import org.osgi.service.component.ComponentContext;
51 import org.osgi.service.component.annotations.Activate;
52 import org.osgi.service.component.annotations.Component;
53 import org.osgi.service.component.annotations.Deactivate;
54 import org.osgi.service.component.annotations.Reference;
55 import org.osgi.service.component.annotations.ReferenceCardinality;
56 import org.osgi.service.component.annotations.ReferencePolicy;
57 import org.osgi.service.http.HttpService;
58 import org.osgi.service.http.whiteboard.HttpWhiteboardConstants;
59 import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
60 import org.osgi.util.tracker.BundleTracker;
61 import org.osgi.util.tracker.ServiceTracker;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64
65 import java.io.IOException;
66 import java.io.OutputStream;
67 import java.io.OutputStreamWriter;
68 import java.lang.annotation.Annotation;
69 import java.lang.reflect.Type;
70 import java.net.URL;
71 import java.nio.charset.Charset;
72 import java.nio.charset.StandardCharsets;
73 import java.util.ArrayList;
74 import java.util.Collections;
75 import java.util.Dictionary;
76 import java.util.HashMap;
77 import java.util.Hashtable;
78 import java.util.List;
79 import java.util.Map;
80 import java.util.concurrent.ConcurrentHashMap;
81 import java.util.concurrent.CopyOnWriteArrayList;
82
83 import javax.servlet.Servlet;
84 import javax.servlet.ServletException;
85 import javax.servlet.http.HttpServletRequest;
86 import javax.servlet.http.HttpServletResponse;
87 import javax.ws.rs.Path;
88 import javax.ws.rs.core.MediaType;
89 import javax.ws.rs.core.Response;
90 import javax.ws.rs.ext.ExceptionMapper;
91 import javax.xml.stream.XMLStreamException;
92 import javax.xml.stream.XMLStreamWriter;
93
94
95
96
97 @Component(
98 immediate = true,
99 service = RestPublisher.class,
100 property = {
101 "service.description=Opencast REST Endpoint Publisher"
102 }
103 )
104 public class RestPublisher implements RestConstants {
105
106
107 protected static final Logger logger = LoggerFactory.getLogger(RestPublisher.class);
108
109
110 public static final String JAX_RS_SERVICE_FILTER = "(" + JaxrsWhiteboardConstants.JAX_RS_RESOURCE + "=true)";
111
112
113 protected static final ConcurrentHashMap<String, String> NAMESPACE_MAP;
114
115 protected List<Object> providers = null;
116
117 static {
118 NAMESPACE_MAP = new ConcurrentHashMap<>();
119 NAMESPACE_MAP.put("http://www.w3.org/2001/XMLSchema-instance", "");
120 }
121
122
123 protected ComponentContext componentContext;
124
125
126 protected ServiceTracker<Object, Object> jaxRsTracker = null;
127
128
129
130
131 protected BundleTracker<Object> bundleTracker = null;
132
133
134 protected String baseServerUri;
135
136
137 protected Map<String, ServiceRegistration<?>> servletRegistrationMap;
138
139
140 private Server server;
141
142
143 private Bus bus;
144
145 private ServiceRegistration<Servlet> servletServiceRegistration;
146
147 private ServiceRegistration<Bus> busServiceRegistration;
148
149
150 private final List<Object> serviceBeans = new CopyOnWriteArrayList<>();
151
152
153 @SuppressWarnings("unchecked")
154 @Activate
155 protected void activate(ComponentContext componentContext) {
156 logger.debug("activate()");
157 baseServerUri = componentContext.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
158 this.componentContext = componentContext;
159 servletRegistrationMap = new ConcurrentHashMap<>();
160 providers = new ArrayList<>();
161
162 JSONProvider jsonProvider = new OpencastJSONProvider();
163 jsonProvider.setIgnoreNamespaces(true);
164 jsonProvider.setNamespaceMap(NAMESPACE_MAP);
165 providers.add(jsonProvider);
166
167 providers.add(new ExceptionMapper<NotFoundException>() {
168 @Override
169 public Response toResponse(NotFoundException e) {
170 return Response
171 .status(HttpStatus.SC_NOT_FOUND)
172 .entity("The resource you requested does not exist.")
173 .type(MediaType.TEXT_PLAIN)
174 .build();
175 }
176 });
177 providers.add(new ExceptionMapper<UnauthorizedException>() {
178 @Override
179 public Response toResponse(UnauthorizedException e) {
180 return Response
181 .status(HttpStatus.SC_UNAUTHORIZED)
182 .entity("unauthorized")
183 .type(MediaType.TEXT_PLAIN)
184 .build();
185 }
186 });
187
188 this.bus = BusFactory.getDefaultBus();
189
190 busServiceRegistration = componentContext.getBundleContext().registerService(Bus.class, bus, new Hashtable<>());
191
192 RestServlet cxf = new RestServlet();
193 cxf.setBus(bus);
194 try {
195 Dictionary<String, Object> props = new Hashtable<>();
196
197 props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT, "("
198 + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=" + RestConstants.HTTP_CONTEXT_ID + ")");
199 props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_NAME, "/");
200 props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/*");
201
202 servletServiceRegistration = componentContext.getBundleContext().registerService(Servlet.class, cxf, props);
203 } catch (Exception e) {
204 logger.info("Problem registering REST endpoint {} : {}", "/", e.getMessage());
205 return;
206 }
207
208 try {
209 jaxRsTracker = new JaxRsServiceTracker();
210 bundleTracker = new StaticResourceBundleTracker(componentContext.getBundleContext());
211 } catch (InvalidSyntaxException e) {
212 throw new IllegalStateException(e);
213 }
214 jaxRsTracker.open();
215 bundleTracker.open();
216 }
217
218
219
220
221 @Deactivate
222 protected void deactivate() {
223 logger.debug("deactivate()");
224 jaxRsTracker.close();
225 bundleTracker.close();
226 busServiceRegistration.unregister();
227 servletServiceRegistration.unregister();
228 }
229
230 @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.OPTIONAL)
231 public void bindHttpService(HttpService httpService) {
232 logger.debug("HttpService registered");
233 rewire();
234 }
235
236 public void unbindHttpService(HttpService httpService) {
237 logger.debug("HttpService unregistered");
238 }
239
240
241
242
243
244
245
246
247
248 protected synchronized void createEndpoint(ServiceReference<?> ref, Object service) {
249 String servicePath = (String) ref.getProperty(SERVICE_PATH_PROPERTY);
250 serviceBeans.add(service);
251 rewire();
252 logger.info("Registered REST endpoint at " + servicePath);
253 }
254
255
256
257
258
259
260
261
262
263 protected void destroyEndpoint(String alias, Object service) {
264 ServiceRegistration<?> reg = servletRegistrationMap.remove(alias);
265 serviceBeans.remove(service);
266 if (reg != null) {
267 reg.unregister();
268 }
269
270 rewire();
271 }
272
273 private synchronized void rewire() {
274
275 if (serviceBeans.isEmpty()) {
276 logger.info("No resource classes skip JAX-RS server recreation");
277 return;
278 }
279
280
281 JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
282 sf.setBus(bus);
283 sf.setProviders(providers);
284
285 sf.setAddress("/");
286
287
288 sf.setProperties(new HashMap<>());
289 BindingFactoryManager manager = sf.getBus().getExtension(BindingFactoryManager.class);
290 JAXRSBindingFactory factory = new JAXRSBindingFactory();
291 factory.setBus(bus);
292 manager.registerBindingFactory(JAXRSBindingFactory.JAXRS_BINDING_ID, factory);
293
294 if (server != null) {
295 logger.debug("Destroying JAX-RS server");
296 server.stop();
297 server.destroy();
298 }
299
300
301 final OpenApiFeature openApiFeature = getOpenApiFeature();
302 sf.getFeatures().add(openApiFeature);
303
304 sf.setServiceBeans(serviceBeans);
305 server = sf.create();
306 }
307
308 private OpenApiFeature getOpenApiFeature() {
309 final OpenApiFeature openApiFeature = new OpenApiFeature();
310 openApiFeature.setContactEmail("dev@opencast.org");
311 openApiFeature.setLicense("Educational Community License, Version 2.0");
312 openApiFeature.setLicenseUrl("https://opensource.org/licenses/ecl2.txt");
313 openApiFeature.setScan(false);
314 openApiFeature.setUseContextBasedConfig(true);
315
316
317 openApiFeature.setResourceClasses(Collections.singleton("io.swagger.v3.jaxrs2.Reader"));
318 OpenApiCustomizer customizer = new OpenApiCustomizer();
319 customizer.setDynamicBasePath(false);
320 openApiFeature.setCustomizer(customizer);
321 openApiFeature.setSupportSwaggerUi(false);
322 return openApiFeature;
323 }
324
325
326
327
328 protected static class OpencastJSONProvider<T> extends JSONProvider<T> {
329 private static final Charset UTF8 = StandardCharsets.UTF_8;
330
331
332
333
334 @Override
335 protected XMLStreamWriter createWriter(Object actualObject, Class<?> actualClass, Type genericType, String enc,
336 OutputStream os, boolean isCollection) throws Exception {
337 Configuration c = new Configuration(NAMESPACE_MAP);
338 c.setSupressAtAttributes(true);
339 MappedNamespaceConvention convention = new MappedNamespaceConvention(c);
340 return new MappedXMLStreamWriter(convention, new OutputStreamWriter(os, UTF8)) {
341 @Override
342 public void writeStartElement(String prefix, String local, String uri) throws XMLStreamException {
343 super.writeStartElement("", local, "");
344 }
345
346 @Override
347 public void writeStartElement(String uri, String local) throws XMLStreamException {
348 super.writeStartElement("", local, "");
349 }
350
351 @Override
352 public void setPrefix(String pfx, String uri) throws XMLStreamException {
353 }
354
355 @Override
356 public void setDefaultNamespace(String uri) throws XMLStreamException {
357 }
358 };
359 }
360 }
361
362
363
364
365
366 public class JaxRsServiceTracker extends ServiceTracker<Object, Object> {
367
368 JaxRsServiceTracker() throws InvalidSyntaxException {
369 super(componentContext.getBundleContext(),
370 componentContext.getBundleContext().createFilter(JAX_RS_SERVICE_FILTER), null);
371 }
372
373 @Override
374 public void removedService(ServiceReference<Object> reference, Object service) {
375 String servicePath = (String) reference.getProperty(SERVICE_PATH_PROPERTY);
376 destroyEndpoint(servicePath, service);
377 super.removedService(reference, service);
378 }
379
380 @Override
381 public Object addingService(ServiceReference<Object> reference) {
382 logger.trace("Adding jaxrs service {}", reference);
383 Object service = super.addingService(reference);
384 if (service == null) {
385 logger.info("JAX-RS service {} has not been instantiated yet, or has already been unregistered. Skipping "
386 + "endpoint creation.", reference);
387 } else {
388 Path pathAnnotation = getAnnotationFromType(service.getClass(), Path.class);
389 if (pathAnnotation == null) {
390 logger.warn(
391 "{} was registered with '{}={}', but the service is not annotated with the JAX-RS "
392 + "@Path annotation",
393 service, SERVICE_PATH_PROPERTY, reference.getProperty(SERVICE_PATH_PROPERTY));
394 } else {
395 createEndpoint(reference, service);
396 }
397 }
398 return service;
399 }
400 }
401
402 private <A extends Annotation> A getAnnotationFromType(Class<?> classType, final Class<A> annotationClass) {
403 A annotation;
404 do {
405 annotation = classType.getAnnotation(annotationClass);
406 classType = classType.getSuperclass();
407 } while (annotation == null && !classType.equals(Object.class));
408 return annotation;
409 }
410
411
412
413
414 static class StaticResourceClassLoader extends ClassLoader {
415 private Bundle bundle = null;
416
417 StaticResourceClassLoader(Bundle bundle) {
418 super();
419 this.bundle = bundle;
420 }
421
422 @Override
423 public URL getResource(String name) {
424 URL url = bundle.getResource(name);
425 logger.debug("{} found resource {} from name {}", this, url, name);
426 return url;
427 }
428 }
429
430
431
432
433 class StaticResourceBundleTracker extends BundleTracker<Object> {
434
435 private final HashMap<Bundle, ServiceRegistration<?>> servlets = new HashMap<>();
436
437
438
439
440
441
442
443 StaticResourceBundleTracker(BundleContext context) {
444 super(context, Bundle.ACTIVE, null);
445 }
446
447
448
449
450
451
452 @Override
453 public Object addingBundle(Bundle bundle, BundleEvent event) {
454 String classpath = bundle.getHeaders().get(RestConstants.HTTP_CLASSPATH);
455 String alias = bundle.getHeaders().get(RestConstants.HTTP_ALIAS);
456 String welcomeFile = bundle.getHeaders().get(RestConstants.HTTP_WELCOME);
457
458 boolean spaRedirect = Boolean.parseBoolean(bundle.getHeaders().get(RestConstants.HTTP_SPA_REDIRECT));
459
460 if (classpath != null && alias != null) {
461 Dictionary<String, String> props = new Hashtable<>();
462
463 props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT, "("
464 + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=" + RestConstants.HTTP_CONTEXT_ID + ")");
465 props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_NAME, alias);
466 if ("/".equals(alias)) {
467 props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, alias);
468 } else {
469 props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, alias + "/*");
470 }
471 StaticResource servlet = new StaticResource(new StaticResourceClassLoader(bundle), classpath, alias,
472 welcomeFile, spaRedirect);
473
474
475
476 logger.info("Registering servlet with alias {}", alias + "/*");
477
478 ServiceRegistration<?> serviceRegistration = componentContext.getBundleContext()
479 .registerService(Servlet.class.getName(), servlet, props);
480 servlets.put(bundle, serviceRegistration);
481 }
482
483 return super.addingBundle(bundle, event);
484 }
485
486 @Override
487 public void removedBundle(Bundle bundle, BundleEvent event, Object object) {
488 String classpath = bundle.getHeaders().get(RestConstants.HTTP_CLASSPATH);
489 String alias = bundle.getHeaders().get(RestConstants.HTTP_ALIAS);
490 if (classpath != null && alias != null) {
491 ServiceRegistration<?> serviceRegistration = servlets.get(bundle);
492 if (serviceRegistration != null) {
493 serviceRegistration.unregister();
494 servlets.remove(bundle);
495 }
496 }
497
498 super.removedBundle(bundle, event, object);
499 }
500 }
501
502
503
504
505 public static class RestServlet extends CXFNonSpringServlet {
506
507 private static final long serialVersionUID = -8963338160276371426L;
508
509
510
511
512 public RestServlet() {
513
514 }
515
516 @Override
517 public void destroyBus() {
518
519 }
520
521 @Override
522 protected void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException {
523 if (request.getRequestURI().endsWith("/docs")) {
524 try {
525 response.sendRedirect("/docs.html?path=" + request.getServletPath());
526 } catch (IOException e) {
527 logger.error("Unable to redirect to rest docs:", e);
528 }
529 } else {
530 super.handleRequest(request, response);
531 }
532 }
533
534 }
535
536 }