View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
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   * Rest endpoint for publishing media to the OAI-PMH publication channel.
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    /** The logger */
95    private static final Logger logger = LoggerFactory.getLogger(OaiPmhPublicationRestService.class);
96    private static final Pattern SEPARATE_PATTERN = Pattern.compile(SEPARATOR);
97  
98    /** The OAI-PMH publication service */
99    protected OaiPmhPublicationService service;
100 
101   /** The service registry */
102   protected ServiceRegistry serviceRegistry = null;
103 
104   /**
105    * Callback from the OSGi declarative services to set the service registry.
106    *
107    * @param serviceRegistry
108    *          the service registry
109    */
110   @Reference
111   protected void setServiceRegistry(ServiceRegistry serviceRegistry) {
112     this.serviceRegistry = serviceRegistry;
113   }
114 
115   /**
116    * @param service
117    *          the service to set
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    * {@inheritDoc}
509    *
510    * @see org.opencastproject.rest.AbstractJobProducerEndpoint#getService()
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    * {@inheritDoc}
523    *
524    * @see org.opencastproject.job.api.AbstractJobProducer#getServiceRegistry()
525    */
526   @Override
527   public ServiceRegistry getServiceRegistry() {
528     return serviceRegistry;
529   }
530 
531 }