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, "(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=" + RestConstants.HTTP_CONTEXT_ID + ")");
198 props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_NAME, "/");
199 props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/*");
200
201 servletServiceRegistration = componentContext.getBundleContext().registerService(Servlet.class, cxf, props);
202 } catch (Exception e) {
203 logger.info("Problem registering REST endpoint {} : {}", "/", e.getMessage());
204 return;
205 }
206
207 try {
208 jaxRsTracker = new JaxRsServiceTracker();
209 bundleTracker = new StaticResourceBundleTracker(componentContext.getBundleContext());
210 } catch (InvalidSyntaxException e) {
211 throw new IllegalStateException(e);
212 }
213 jaxRsTracker.open();
214 bundleTracker.open();
215 }
216
217
218
219
220 @Deactivate
221 protected void deactivate() {
222 logger.debug("deactivate()");
223 jaxRsTracker.close();
224 bundleTracker.close();
225 busServiceRegistration.unregister();
226 servletServiceRegistration.unregister();
227 }
228
229 @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.OPTIONAL)
230 public void bindHttpService(HttpService httpService) {
231 logger.debug("HttpService registered");
232 rewire();
233 }
234
235 public void unbindHttpService(HttpService httpService) {
236 logger.debug("HttpService unregistered");
237 }
238
239
240
241
242
243
244
245
246
247 protected synchronized void createEndpoint(ServiceReference<?> ref, Object service) {
248 String servicePath = (String) ref.getProperty(SERVICE_PATH_PROPERTY);
249 serviceBeans.add(service);
250 rewire();
251 logger.info("Registered REST endpoint at " + servicePath);
252 }
253
254
255
256
257
258
259
260
261
262 protected void destroyEndpoint(String alias, Object service) {
263 ServiceRegistration<?> reg = servletRegistrationMap.remove(alias);
264 serviceBeans.remove(service);
265 if (reg != null) {
266 reg.unregister();
267 }
268
269 rewire();
270 }
271
272 private synchronized void rewire() {
273
274 if (serviceBeans.isEmpty()) {
275 logger.info("No resource classes skip JAX-RS server recreation");
276 return;
277 }
278
279
280 JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
281 sf.setBus(bus);
282 sf.setProviders(providers);
283
284 sf.setAddress("/");
285
286
287 sf.setProperties(new HashMap<>());
288 BindingFactoryManager manager = sf.getBus().getExtension(BindingFactoryManager.class);
289 JAXRSBindingFactory factory = new JAXRSBindingFactory();
290 factory.setBus(bus);
291 manager.registerBindingFactory(JAXRSBindingFactory.JAXRS_BINDING_ID, factory);
292
293 if (server != null) {
294 logger.debug("Destroying JAX-RS server");
295 server.stop();
296 server.destroy();
297 }
298
299
300 final OpenApiFeature openApiFeature = getOpenApiFeature();
301 sf.getFeatures().add(openApiFeature);
302
303 sf.setServiceBeans(serviceBeans);
304 server = sf.create();
305 }
306
307 private OpenApiFeature getOpenApiFeature() {
308 final OpenApiFeature openApiFeature = new OpenApiFeature();
309 openApiFeature.setContactEmail("dev@opencast.org");
310 openApiFeature.setLicense("Educational Community License, Version 2.0");
311 openApiFeature.setLicenseUrl("https://opensource.org/licenses/ecl2.txt");
312 openApiFeature.setScan(false);
313 openApiFeature.setUseContextBasedConfig(true);
314
315
316 openApiFeature.setResourceClasses(Collections.singleton("io.swagger.v3.jaxrs2.Reader"));
317 OpenApiCustomizer customizer = new OpenApiCustomizer();
318 customizer.setDynamicBasePath(false);
319 openApiFeature.setCustomizer(customizer);
320 openApiFeature.setSupportSwaggerUi(false);
321 return openApiFeature;
322 }
323
324
325
326
327 protected static class OpencastJSONProvider<T> extends JSONProvider<T> {
328 private static final Charset UTF8 = StandardCharsets.UTF_8;
329
330
331
332
333 @Override
334 protected XMLStreamWriter createWriter(Object actualObject, Class<?> actualClass, Type genericType, String enc,
335 OutputStream os, boolean isCollection) throws Exception {
336 Configuration c = new Configuration(NAMESPACE_MAP);
337 c.setSupressAtAttributes(true);
338 MappedNamespaceConvention convention = new MappedNamespaceConvention(c);
339 return new MappedXMLStreamWriter(convention, new OutputStreamWriter(os, UTF8)) {
340 @Override
341 public void writeStartElement(String prefix, String local, String uri) throws XMLStreamException {
342 super.writeStartElement("", local, "");
343 }
344
345 @Override
346 public void writeStartElement(String uri, String local) throws XMLStreamException {
347 super.writeStartElement("", local, "");
348 }
349
350 @Override
351 public void setPrefix(String pfx, String uri) throws XMLStreamException {
352 }
353
354 @Override
355 public void setDefaultNamespace(String uri) throws XMLStreamException {
356 }
357 };
358 }
359 }
360
361
362
363
364
365 public class JaxRsServiceTracker extends ServiceTracker<Object, Object> {
366
367 JaxRsServiceTracker() throws InvalidSyntaxException {
368 super(componentContext.getBundleContext(),
369 componentContext.getBundleContext().createFilter(JAX_RS_SERVICE_FILTER), null);
370 }
371
372 @Override
373 public void removedService(ServiceReference<Object> reference, Object service) {
374 String servicePath = (String) reference.getProperty(SERVICE_PATH_PROPERTY);
375 destroyEndpoint(servicePath, service);
376 super.removedService(reference, service);
377 }
378
379 @Override
380 public Object addingService(ServiceReference<Object> reference) {
381 logger.trace("Adding jaxrs service {}", reference);
382 Object service = super.addingService(reference);
383 if (service == null) {
384 logger.info("JAX-RS service {} has not been instantiated yet, or has already been unregistered. Skipping "
385 + "endpoint creation.", reference);
386 } else {
387 Path pathAnnotation = getAnnotationFromType(service.getClass(), Path.class);
388 if (pathAnnotation == null) {
389 logger.warn(
390 "{} was registered with '{}={}', but the service is not annotated with the JAX-RS "
391 + "@Path annotation",
392 service, SERVICE_PATH_PROPERTY, reference.getProperty(SERVICE_PATH_PROPERTY));
393 } else {
394 createEndpoint(reference, service);
395 }
396 }
397 return service;
398 }
399 }
400
401 private <A extends Annotation> A getAnnotationFromType(Class<?> classType, final Class<A> annotationClass) {
402 A annotation;
403 do {
404 annotation = classType.getAnnotation(annotationClass);
405 classType = classType.getSuperclass();
406 } while (annotation == null && !classType.equals(Object.class));
407 return annotation;
408 }
409
410
411
412
413 static class StaticResourceClassLoader extends ClassLoader {
414 private Bundle bundle = null;
415
416 StaticResourceClassLoader(Bundle bundle) {
417 super();
418 this.bundle = bundle;
419 }
420
421 @Override
422 public URL getResource(String name) {
423 URL url = bundle.getResource(name);
424 logger.debug("{} found resource {} from name {}", this, url, name);
425 return url;
426 }
427 }
428
429
430
431
432 class StaticResourceBundleTracker extends BundleTracker<Object> {
433
434 private final HashMap<Bundle, ServiceRegistration<?>> servlets = new HashMap<>();
435
436
437
438
439
440
441
442 StaticResourceBundleTracker(BundleContext context) {
443 super(context, Bundle.ACTIVE, null);
444 }
445
446
447
448
449
450
451 @Override
452 public Object addingBundle(Bundle bundle, BundleEvent event) {
453 String classpath = bundle.getHeaders().get(RestConstants.HTTP_CLASSPATH);
454 String alias = bundle.getHeaders().get(RestConstants.HTTP_ALIAS);
455 String welcomeFile = bundle.getHeaders().get(RestConstants.HTTP_WELCOME);
456
457 boolean spaRedirect = Boolean.parseBoolean(bundle.getHeaders().get(RestConstants.HTTP_SPA_REDIRECT));
458
459 if (classpath != null && alias != null) {
460 Dictionary<String, String> props = new Hashtable<>();
461
462 props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT, "(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=" + RestConstants.HTTP_CONTEXT_ID + ")");
463 props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_NAME, alias);
464 if ("/".equals(alias)) {
465 props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, alias);
466 } else {
467 props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, alias + "/*");
468 }
469 StaticResource servlet = new StaticResource(new StaticResourceClassLoader(bundle), classpath, alias,
470 welcomeFile, spaRedirect);
471
472
473
474 logger.info("Registering servlet with alias {}", alias + "/*");
475
476 ServiceRegistration<?> serviceRegistration = componentContext.getBundleContext()
477 .registerService(Servlet.class.getName(), servlet, props);
478 servlets.put(bundle, serviceRegistration);
479 }
480
481 return super.addingBundle(bundle, event);
482 }
483
484 @Override
485 public void removedBundle(Bundle bundle, BundleEvent event, Object object) {
486 String classpath = bundle.getHeaders().get(RestConstants.HTTP_CLASSPATH);
487 String alias = bundle.getHeaders().get(RestConstants.HTTP_ALIAS);
488 if (classpath != null && alias != null) {
489 ServiceRegistration<?> serviceRegistration = servlets.get(bundle);
490 if (serviceRegistration != null) {
491 serviceRegistration.unregister();
492 servlets.remove(bundle);
493 }
494 }
495
496 super.removedBundle(bundle, event, object);
497 }
498 }
499
500
501
502
503 public static class RestServlet extends CXFNonSpringServlet {
504
505 private static final long serialVersionUID = -8963338160276371426L;
506
507
508
509
510 public RestServlet() {
511
512 }
513
514 @Override
515 public void destroyBus() {
516
517 }
518
519 @Override
520 protected void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException {
521 if (request.getRequestURI().endsWith("/docs")) {
522 try {
523 response.sendRedirect("/docs.html?path=" + request.getServletPath());
524 } catch (IOException e) {
525 logger.error("Unable to redirect to rest docs:", e);
526 }
527 } else {
528 super.handleRequest(request, response);
529 }
530 }
531
532 }
533
534 }