1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.opencastproject.publication.oaipmh.endpoint;
22
23 import static javax.servlet.http.HttpServletResponse.SC_OK;
24 import static org.opencastproject.publication.api.OaiPmhPublicationService.SEPARATOR;
25
26 import org.opencastproject.job.api.JaxbJob;
27 import org.opencastproject.job.api.Job;
28 import org.opencastproject.job.api.JobProducer;
29 import org.opencastproject.mediapackage.MediaPackage;
30 import org.opencastproject.mediapackage.MediaPackageElement;
31 import org.opencastproject.mediapackage.MediaPackageElementFlavor;
32 import org.opencastproject.mediapackage.MediaPackageElementParser;
33 import org.opencastproject.mediapackage.MediaPackageException;
34 import org.opencastproject.mediapackage.MediaPackageParser;
35 import org.opencastproject.mediapackage.Publication;
36 import org.opencastproject.publication.api.OaiPmhPublicationService;
37 import org.opencastproject.rest.AbstractJobProducerEndpoint;
38 import org.opencastproject.serviceregistry.api.ServiceRegistry;
39 import org.opencastproject.util.doc.rest.RestParameter;
40 import org.opencastproject.util.doc.rest.RestParameter.Type;
41 import org.opencastproject.util.doc.rest.RestQuery;
42 import org.opencastproject.util.doc.rest.RestResponse;
43 import org.opencastproject.util.doc.rest.RestService;
44
45 import org.osgi.service.component.annotations.Component;
46 import org.osgi.service.component.annotations.Reference;
47 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 import java.util.HashSet;
52 import java.util.Set;
53 import java.util.regex.Pattern;
54 import java.util.stream.Collectors;
55
56 import javax.ws.rs.DefaultValue;
57 import javax.ws.rs.FormParam;
58 import javax.ws.rs.POST;
59 import javax.ws.rs.Path;
60 import javax.ws.rs.Produces;
61 import javax.ws.rs.core.MediaType;
62 import javax.ws.rs.core.Response;
63 import javax.ws.rs.core.Response.Status;
64
65
66
67
68 @Path("/publication/oaipmh")
69 @RestService(
70 name = "oaipmhpublicationservice",
71 title = "OAI-PMH Publication Service",
72 abstractText = "This service publishes a media package element to the Opencast OAI-PMH channel.",
73 notes = {
74 "All paths above are relative to the REST endpoint base (something like http://your.server/files). "
75 + "If the service is down or not working it will return a status 503, this means the "
76 + "the underlying service is not working and is either restarting or has failed. "
77 + "A status code 500 means a general failure has occurred which is not recoverable "
78 + "and was not anticipated. In other words, there is a bug!"
79 }
80 )
81 @Component(
82 immediate = true,
83 service = OaiPmhPublicationRestService.class,
84 property = {
85 "service.description=OAI-PMH Publication REST Endpoint",
86 "opencast.service.type=org.opencastproject.publication.oaipmh",
87 "opencast.service.path=/publication/oaipmh",
88 "opencast.service.jobproducer=true"
89 }
90 )
91 @JaxrsResource
92 public class OaiPmhPublicationRestService extends AbstractJobProducerEndpoint {
93
94
95 private static final Logger logger = LoggerFactory.getLogger(OaiPmhPublicationRestService.class);
96 private static final Pattern SEPARATE_PATTERN = Pattern.compile(SEPARATOR);
97
98
99 protected OaiPmhPublicationService service;
100
101
102 protected ServiceRegistry serviceRegistry = null;
103
104
105
106
107
108
109
110 @Reference
111 protected void setServiceRegistry(ServiceRegistry serviceRegistry) {
112 this.serviceRegistry = serviceRegistry;
113 }
114
115
116
117
118
119 @Reference
120 public void setService(OaiPmhPublicationService service) {
121 this.service = service;
122 }
123
124 private static Set<String> split(final String s) {
125 if (s == null) {
126 return java.util.Collections.emptySet();
127 }
128 return SEPARATE_PATTERN.splitAsStream(s).collect(Collectors.toSet());
129 }
130
131 @POST
132 @Path("/")
133 @Produces(MediaType.TEXT_XML)
134 @RestQuery(
135 name = "publish",
136 description = "Publish a media package element to this publication channel",
137 returnDescription = "The job that can be used to track the publication",
138 restParameters = {
139 @RestParameter(
140 name = "mediapackage",
141 isRequired = true,
142 description = "The media package",
143 type = Type.TEXT
144 ),
145 @RestParameter(
146 name = "channel",
147 isRequired = true,
148 description = "The channel name",
149 type = Type.STRING
150 ),
151 @RestParameter(
152 name = "downloadElementIds",
153 isRequired = true,
154 description = "The elements to publish to download separated by '" + SEPARATOR + "'",
155 type = Type.STRING
156 ),
157 @RestParameter(
158 name = "streamingElementIds",
159 isRequired = true,
160 description = "The elements to publish to streaming separated by '" + SEPARATOR + "'",
161 type = Type.STRING
162 ),
163 @RestParameter(
164 name = "checkAvailability",
165 isRequired = false,
166 description = "Whether to check for availability",
167 type = Type.BOOLEAN,
168 defaultValue = "true"
169 )
170 },
171 responses = {
172 @RestResponse(responseCode = SC_OK, description = "An XML representation of the publication job")
173 }
174 )
175 public Response publish(
176 @FormParam("mediapackage") String mediaPackageXml,
177 @FormParam("channel") String channel,
178 @FormParam("downloadElementIds") String downloadElementIds,
179 @FormParam("streamingElementIds") String streamingElementIds,
180 @FormParam("checkAvailability") @DefaultValue("true") boolean checkAvailability
181 ) throws Exception {
182 final Job job;
183 try {
184 final MediaPackage mediaPackage = MediaPackageParser.getFromXml(mediaPackageXml);
185 job = service.publish(mediaPackage, channel, split(downloadElementIds),
186 split(streamingElementIds), checkAvailability);
187 } catch (IllegalArgumentException e) {
188 logger.warn("Unable to create an publication job", e);
189 return Response.status(Status.BAD_REQUEST).build();
190 } catch (Exception e) {
191 logger.warn("Error publishing element", e);
192 return Response.serverError().build();
193 }
194 return Response.ok(new JaxbJob(job)).build();
195 }
196
197 @POST
198 @Path("/replace")
199 @Produces(MediaType.TEXT_XML)
200 @RestQuery(
201 name = "replace",
202 description = "Replace a media package in this publication channel",
203 returnDescription = "The job that can be used to track the publication",
204 restParameters = {
205 @RestParameter(
206 name = "mediapackage",
207 isRequired = true,
208 description = "The media package",
209 type = Type.TEXT
210 ),
211 @RestParameter(
212 name = "channel",
213 isRequired = true,
214 description = "The channel name",
215 type = Type.STRING
216 ),
217 @RestParameter(
218 name = "downloadElements",
219 isRequired = true,
220 description = "The additional elements to publish to download",
221 type = Type.STRING
222 ),
223 @RestParameter(
224 name = "streamingElements",
225 isRequired = true,
226 description = "The additional elements to publish to streaming",
227 type = Type.STRING
228 ),
229 @RestParameter(
230 name = "retractDownloadFlavors",
231 isRequired = true,
232 description = "The flavors of the elements to retract from download separated by '" + SEPARATOR + "'",
233 type = Type.STRING
234 ),
235 @RestParameter(
236 name = "retractStreamingFlavors",
237 isRequired = true,
238 description = "The flavors of the elements to retract from streaming separated by '" + SEPARATOR + "'",
239 type = Type.STRING
240 ),
241 @RestParameter(
242 name = "publications",
243 isRequired = true,
244 description = "The publications to update",
245 type = Type.STRING
246 ),
247 @RestParameter(
248 name = "checkAvailability",
249 isRequired = false,
250 description = "Whether to check for availability",
251 type = Type.BOOLEAN,
252 defaultValue = "true"
253 )
254 },
255 responses = {
256 @RestResponse(responseCode = SC_OK, description = "An XML representation of the publication job")
257 }
258 )
259 public Response replace(
260 @FormParam("mediapackage") final String mediaPackageXml,
261 @FormParam("channel") final String channel,
262 @FormParam("downloadElements") final String downloadElementsXml,
263 @FormParam("streamingElements") final String streamingElementsXml,
264 @FormParam("retractDownloadFlavors") final String retractDownloadFlavorsString,
265 @FormParam("retractStreamingFlavors") final String retractStreamingFlavorsString,
266 @FormParam("publications") final String publicationsXml,
267 @FormParam("checkAvailability") @DefaultValue("true") final boolean checkAvailability) {
268 final Job job;
269 try {
270 final MediaPackage mediaPackage = MediaPackageParser.getFromXml(mediaPackageXml);
271 final Set<? extends MediaPackageElement> downloadElements = new HashSet<MediaPackageElement>(
272 MediaPackageElementParser.getArrayFromXml(downloadElementsXml));
273 final Set<? extends MediaPackageElement> streamingElements = new HashSet<MediaPackageElement>(
274 MediaPackageElementParser.getArrayFromXml(streamingElementsXml));
275 final Set<MediaPackageElementFlavor> retractDownloadFlavors = split(retractDownloadFlavorsString).stream()
276 .filter(s -> !s.isEmpty())
277 .map(MediaPackageElementFlavor::parseFlavor)
278 .collect(Collectors.toSet());
279 final Set<MediaPackageElementFlavor> retractStreamingFlavors = split(retractStreamingFlavorsString).stream()
280 .filter(s -> !s.isEmpty())
281 .map(MediaPackageElementFlavor::parseFlavor)
282 .collect(Collectors.toSet());
283 final Set<? extends Publication> publications = MediaPackageElementParser.getArrayFromXml(publicationsXml)
284 .stream().map(p -> (Publication) p).collect(Collectors.toSet());
285 job = service.replace(mediaPackage, channel, downloadElements, streamingElements, retractDownloadFlavors,
286 retractStreamingFlavors, publications, checkAvailability);
287 } catch (IllegalArgumentException e) {
288 logger.warn("Unable to create a publication job", e);
289 return Response.status(Status.BAD_REQUEST).build();
290 } catch (Exception e) {
291 logger.warn("Error publishing or retracting element", e);
292 return Response.serverError().build();
293 }
294 return Response.ok(new JaxbJob(job)).build();
295 }
296
297 @POST
298 @Path("/replacesync")
299 @Produces(MediaType.TEXT_XML)
300 @RestQuery(
301 name = "replacesync",
302 description = "Synchronously Replace a media package in this publication channel",
303 returnDescription = "The publication",
304 restParameters = {
305 @RestParameter(
306 name = "mediapackage",
307 isRequired = true,
308 description = "The media package",
309 type = Type.TEXT
310 ),
311 @RestParameter(
312 name = "channel",
313 isRequired = true,
314 description = "The channel name",
315 type = Type.STRING
316 ),
317 @RestParameter(
318 name = "downloadElements",
319 isRequired = true,
320 description = "The additional elements to publish to download",
321 type = Type.STRING
322 ),
323 @RestParameter(
324 name = "streamingElements",
325 isRequired = true,
326 description = "The additional elements to publish to streaming",
327 type = Type.STRING
328 ),
329 @RestParameter(
330 name = "retractDownloadFlavors",
331 isRequired = true,
332 description = "The flavors of the elements to retract from download separated by '" + SEPARATOR + "'",
333 type = Type.STRING
334 ),
335 @RestParameter(
336 name = "retractStreamingFlavors",
337 isRequired = true,
338 description = "The flavors of the elements to retract from streaming separated by '" + SEPARATOR + "'",
339 type = Type.STRING
340 ),
341 @RestParameter(
342 name = "publications",
343 isRequired = true,
344 description = "The publications to update",
345 type = Type.STRING
346 ),
347 @RestParameter(
348 name = "checkAvailability",
349 isRequired = false,
350 description = "Whether to check for availability",
351 type = Type.BOOLEAN,
352 defaultValue = "true"
353 )
354 },
355 responses = {
356 @RestResponse(responseCode = SC_OK, description = "An XML representation of the publication")
357 }
358 )
359 public Response replaceSync(
360 @FormParam("mediapackage") final String mediaPackageXml,
361 @FormParam("channel") final String channel,
362 @FormParam("downloadElements") final String downloadElementsXml,
363 @FormParam("streamingElements") final String streamingElementsXml,
364 @FormParam("retractDownloadFlavors") final String retractDownloadFlavorsString,
365 @FormParam("retractStreamingFlavors") final String retractStreamingFlavorsString,
366 @FormParam("publications") final String publicationsXml,
367 @FormParam("checkAvailability") @DefaultValue("true") final boolean checkAvailability
368 ) throws MediaPackageException {
369 final Publication publication;
370 try {
371 final MediaPackage mediaPackage = MediaPackageParser.getFromXml(mediaPackageXml);
372 final Set<? extends MediaPackageElement> downloadElements = new HashSet<MediaPackageElement>(
373 MediaPackageElementParser.getArrayFromXml(downloadElementsXml));
374 final Set<? extends MediaPackageElement> streamingElements = new HashSet<MediaPackageElement>(
375 MediaPackageElementParser.getArrayFromXml(streamingElementsXml));
376 final Set<MediaPackageElementFlavor> retractDownloadFlavors = split(retractDownloadFlavorsString).stream()
377 .filter(s -> !s.isEmpty())
378 .map(MediaPackageElementFlavor::parseFlavor)
379 .collect(Collectors.toSet());
380 final Set<MediaPackageElementFlavor> retractStreamingFlavors = split(retractStreamingFlavorsString).stream()
381 .filter(s -> !s.isEmpty())
382 .map(MediaPackageElementFlavor::parseFlavor)
383 .collect(Collectors.toSet());
384 final Set<? extends Publication> publications = MediaPackageElementParser.getArrayFromXml(publicationsXml)
385 .stream().map(p -> (Publication) p).collect(Collectors.toSet());
386 publication = service.replaceSync(mediaPackage, channel, downloadElements, streamingElements,
387 retractDownloadFlavors, retractStreamingFlavors, publications, checkAvailability);
388 } catch (Exception e) {
389 logger.warn("Error publishing or retracting element", e);
390 return Response.serverError().build();
391 }
392 return Response.ok(MediaPackageElementParser.getAsXml(publication)).build();
393 }
394
395 @POST
396 @Path("/retract")
397 @Produces(MediaType.TEXT_XML)
398 @RestQuery(
399 name = "retract",
400 description = "Retract a media package element from this publication channel",
401 returnDescription = "The job that can be used to track the retraction",
402 restParameters = {
403 @RestParameter(
404 name = "mediapackage",
405 isRequired = true,
406 description = "The media package",
407 type = Type.TEXT
408 ),
409 @RestParameter(
410 name = "channel",
411 isRequired = true,
412 description = "The OAI-PMH channel to retract from",
413 type = Type.STRING
414 )
415 },
416 responses = {
417 @RestResponse(responseCode = SC_OK, description = "An XML representation of the retraction job")
418 }
419 )
420 public Response retract(
421 @FormParam("mediapackage") String mediaPackageXml,
422 @FormParam("channel") String channel
423 ) throws Exception {
424 Job job = null;
425 MediaPackage mediaPackage = null;
426 try {
427 mediaPackage = MediaPackageParser.getFromXml(mediaPackageXml);
428 job = service.retract(mediaPackage, channel);
429 } catch (IllegalArgumentException e) {
430 logger.debug("Unable to create an retract job", e);
431 return Response.status(Status.BAD_REQUEST).build();
432 } catch (Exception e) {
433 logger.warn("Unable to retract media package '{}' from the OAI-PMH channel {}",
434 mediaPackage != null ? mediaPackage.getIdentifier().toString() : "<parsing error>", channel, e);
435 return Response.serverError().build();
436 }
437 return Response.ok(new JaxbJob(job)).build();
438 }
439
440 @POST
441 @Path("/updateMetadata")
442 @Produces(MediaType.TEXT_XML)
443 @RestQuery(
444 name = "update",
445 description = "Update metadata of an published media package. "
446 + "This endpoint does not update any media files. If you want to update the whole media package, use the "
447 + "publish endpoint.",
448 returnDescription = "The job that can be used to update the metadata of an media package",
449 restParameters = {
450 @RestParameter(
451 name = "mediapackage",
452 isRequired = true,
453 description = "The updated media package",
454 type = Type.TEXT
455 ),
456 @RestParameter(
457 name = "channel",
458 isRequired = true,
459 description = "The channel name",
460 type = Type.STRING
461 ),
462 @RestParameter(
463 name = "flavors",
464 isRequired = true,
465 description = "The element flavors to be updated, separated by '" + SEPARATOR + "'",
466 type = Type.STRING
467 ),
468 @RestParameter(
469 name = "tags",
470 isRequired = true,
471 description = "The element tags to be updated, separated by '" + SEPARATOR + "'",
472 type = Type.STRING
473 ),
474 @RestParameter(
475 name = "checkAvailability",
476 isRequired = false,
477 description = "Whether to check for availability",
478 type = Type.BOOLEAN,
479 defaultValue = "true"
480 )
481 },
482 responses = {
483 @RestResponse(responseCode = SC_OK, description = "An XML representation of the publication job")
484 }
485 )
486 public Response updateMetadata(
487 @FormParam("mediapackage") String mediaPackageXml,
488 @FormParam("channel") String channel,
489 @FormParam("flavors") String flavors,
490 @FormParam("tags") String tags,
491 @FormParam("checkAvailability") @DefaultValue("true") boolean checkAvailability
492 ) throws Exception {
493 final Job job;
494 try {
495 final MediaPackage mediaPackage = MediaPackageParser.getFromXml(mediaPackageXml);
496 job = service.updateMetadata(mediaPackage, channel, split(flavors), split(tags), checkAvailability);
497 } catch (IllegalArgumentException e) {
498 logger.debug("Unable to create an update metadata job", e);
499 return Response.status(Status.BAD_REQUEST).build();
500 } catch (Exception e) {
501 logger.warn("Error publishing element", e);
502 return Response.serverError().build();
503 }
504 return Response.ok(new JaxbJob(job)).build();
505 }
506
507
508
509
510
511
512 @Override
513 public JobProducer getService() {
514 if (service instanceof JobProducer) {
515 return (JobProducer) service;
516 } else {
517 return null;
518 }
519 }
520
521
522
523
524
525
526 @Override
527 public ServiceRegistry getServiceRegistry() {
528 return serviceRegistry;
529 }
530
531 }