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  
22  package org.opencastproject.series.endpoint;
23  
24  import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
25  import static javax.servlet.http.HttpServletResponse.SC_CREATED;
26  import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
27  import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
28  import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
29  import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
30  import static javax.servlet.http.HttpServletResponse.SC_OK;
31  import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
32  import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
33  import static javax.ws.rs.core.Response.Status.CREATED;
34  import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
35  import static javax.ws.rs.core.Response.Status.NOT_FOUND;
36  import static javax.ws.rs.core.Response.Status.NO_CONTENT;
37  import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_IDENTIFIER;
38  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
39  import static org.opencastproject.util.doc.rest.RestParameter.Type.TEXT;
40  
41  import org.opencastproject.mediapackage.EName;
42  import org.opencastproject.metadata.dublincore.DublinCore;
43  import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
44  import org.opencastproject.metadata.dublincore.DublinCoreCatalogService;
45  import org.opencastproject.metadata.dublincore.DublinCores;
46  import org.opencastproject.rest.RestConstants;
47  import org.opencastproject.security.api.AccessControlList;
48  import org.opencastproject.security.api.AccessControlParser;
49  import org.opencastproject.security.api.SecurityService;
50  import org.opencastproject.security.api.UnauthorizedException;
51  import org.opencastproject.series.api.Series;
52  import org.opencastproject.series.api.SeriesException;
53  import org.opencastproject.series.api.SeriesService;
54  import org.opencastproject.systems.OpencastConstants;
55  import org.opencastproject.util.NotFoundException;
56  import org.opencastproject.util.RestUtil.R;
57  import org.opencastproject.util.UrlSupport;
58  import org.opencastproject.util.doc.rest.RestParameter;
59  import org.opencastproject.util.doc.rest.RestParameter.Type;
60  import org.opencastproject.util.doc.rest.RestQuery;
61  import org.opencastproject.util.doc.rest.RestResponse;
62  import org.opencastproject.util.doc.rest.RestService;
63  
64  import com.google.gson.Gson;
65  import com.google.gson.JsonArray;
66  import com.google.gson.JsonPrimitive;
67  
68  import org.apache.commons.io.IOUtils;
69  import org.apache.commons.lang3.StringUtils;
70  import org.json.simple.JSONArray;
71  import org.json.simple.JSONObject;
72  import org.osgi.service.component.ComponentContext;
73  import org.osgi.service.component.annotations.Activate;
74  import org.osgi.service.component.annotations.Component;
75  import org.osgi.service.component.annotations.Reference;
76  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
77  import org.slf4j.Logger;
78  import org.slf4j.LoggerFactory;
79  
80  import java.io.ByteArrayInputStream;
81  import java.io.IOException;
82  import java.io.InputStream;
83  import java.io.UnsupportedEncodingException;
84  import java.net.URI;
85  import java.nio.charset.StandardCharsets;
86  import java.util.Date;
87  import java.util.List;
88  import java.util.Map;
89  import java.util.Optional;
90  
91  import javax.servlet.http.HttpServletRequest;
92  import javax.ws.rs.DELETE;
93  import javax.ws.rs.DefaultValue;
94  import javax.ws.rs.FormParam;
95  import javax.ws.rs.GET;
96  import javax.ws.rs.POST;
97  import javax.ws.rs.PUT;
98  import javax.ws.rs.Path;
99  import javax.ws.rs.PathParam;
100 import javax.ws.rs.Produces;
101 import javax.ws.rs.WebApplicationException;
102 import javax.ws.rs.core.Context;
103 import javax.ws.rs.core.MediaType;
104 import javax.ws.rs.core.Response;
105 
106 /**
107  * REST endpoint for Series Service.
108  *
109  */
110 @Path("/series")
111 @RestService(
112     name = "seriesservice",
113     title = "Series Service",
114     abstractText = "This service creates, edits and retrieves and helps managing series.",
115     notes = {
116         "All paths above are relative to the REST endpoint base (something like http://your.server/files)",
117         "If the service is down or not working it will return a status 503, this means the the "
118             + "underlying service is not working and is either restarting or has failed",
119         "A status code 500 means a general failure has occurred which is not recoverable and was "
120             + "not anticipated. In other words, there is a bug! You should file an error report "
121             + "with your server logs from the time when the error occurred: "
122             + "<a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>"
123     }
124 )
125 @Component(
126     immediate = true,
127     service = SeriesRestService.class,
128     property = {
129         "service.description=Series REST Endpoint",
130         "opencast.service.type=org.opencastproject.series",
131         "opencast.service.path=/series"
132     }
133 )
134 @JaxrsResource
135 public class SeriesRestService {
136 
137   private static final String SERIES_ELEMENT_CONTENT_TYPE_PREFIX = "series/";
138 
139   private static final Gson gson = new Gson();
140 
141   /** Logging utility */
142   private static final Logger logger = LoggerFactory.getLogger(SeriesRestService.class);
143 
144   /** Series Service */
145   private SeriesService seriesService;
146 
147   /** Dublin Core Catalog service */
148   private DublinCoreCatalogService dcService;
149 
150   /** The security service */
151   private SecurityService securityService;
152 
153   /** Default server URL */
154   protected String serverUrl = "http://localhost:8080";
155 
156   /** Service url */
157   protected String serviceUrl = null;
158 
159   /** Default number of items on page */
160   private static final int DEFAULT_LIMIT = 20;
161 
162   /** Suffix to mark descending ordering of results */
163   public static final String DESCENDING_SUFFIX = "_DESC";
164 
165   private static final String SAMPLE_DUBLIN_CORE = "<?xml version=\"1.0\"?>\n"
166       + "<dublincore xmlns=\"http://www.opencastproject.org/xsd/1.0/dublincore/\" "
167       + "    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
168       + "    xsi:schemaLocation=\"http://www.opencastproject.org http://www.opencastproject.org/schema.xsd\" "
169       + "    xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n"
170       + "    xmlns:dcterms=\"http://purl.org/dc/terms/\" "
171       + "    xmlns:oc=\"http://www.opencastproject.org/matterhorn/\">\n\n"
172       + "  <dcterms:title xml:lang=\"en\">\n"
173       + "    Land and Vegetation: Key players on the Climate Scene\n"
174       + "  </dcterms:title>\n"
175       + "  <dcterms:subject>"
176       + "    climate, land, vegetation\n"
177       + "  </dcterms:subject>\n"
178       + "  <dcterms:description xml:lang=\"en\">\n"
179       + "    Introduction lecture from the Institute for\n"
180       + "    Atmospheric and Climate Science.\n"
181       + "  </dcterms:description>\n"
182       + "  <dcterms:publisher>\n"
183       + "    ETH Zurich, Switzerland\n"
184       + "  </dcterms:publisher>\n"
185       + "  <dcterms:identifier>\n"
186       + "    10.0000/5819\n"
187       + "  </dcterms:identifier>\n"
188       + "  <dcterms:modified xsi:type=\"dcterms:W3CDTF\">\n"
189       + "    2007-12-05\n"
190       + "  </dcterms:modified>\n"
191       + "  <dcterms:format xsi:type=\"dcterms:IMT\">\n"
192       + "    video/x-dv\n"
193       + "  </dcterms:format>\n"
194       + "</dublincore>";
195 
196   private static final String SAMPLE_ACCESS_CONTROL_LIST =
197       "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"
198           + "<acl xmlns=\"http://org.opencastproject.security\">\n"
199           + "  <ace>\n"
200           + "    <role>admin</role>\n"
201           + "    <action>delete</action>\n"
202           + "    <allow>true</allow>\n"
203           + "  </ace>\n"
204           + "</acl>";
205 
206   /**
207    * OSGi callback for setting series service.
208    *
209    * @param seriesService
210    */
211   @Reference
212   public void setService(SeriesService seriesService) {
213     this.seriesService = seriesService;
214   }
215 
216   /**
217    * OSGi callback for setting Dublin Core Catalog service.
218    *
219    * @param dcService
220    */
221   @Reference
222   public void setDublinCoreService(DublinCoreCatalogService dcService) {
223     this.dcService = dcService;
224   }
225 
226   /** OSGi callback for the security service */
227   @Reference
228   public void setSecurityService(SecurityService securityService) {
229     this.securityService = securityService;
230   }
231 
232 
233   /**
234    * Activates REST service.
235    *
236    * @param cc
237    *          ComponentContext
238    */
239   @Activate
240   public void activate(ComponentContext cc) {
241     if (cc == null) {
242       this.serverUrl = "http://localhost:8080";
243     } else {
244       String ccServerUrl = cc.getBundleContext().getProperty(OpencastConstants.SERVER_URL_PROPERTY);
245       logger.debug("Configured server url is {}", ccServerUrl);
246       if (ccServerUrl == null) {
247         this.serverUrl = "http://localhost:8080";
248       } else {
249         this.serverUrl = ccServerUrl;
250       }
251     }
252     serviceUrl = (String) cc.getProperties().get(RestConstants.SERVICE_PATH_PROPERTY);
253   }
254 
255   public String getSeriesXmlUrl(String seriesId) {
256     return UrlSupport.concat(serverUrl, serviceUrl, seriesId + ".xml");
257   }
258 
259   public String getSeriesJsonUrl(String seriesId) {
260     return UrlSupport.concat(serverUrl, serviceUrl, seriesId + ".json");
261   }
262 
263   @GET
264   @Produces(MediaType.TEXT_XML)
265   @Path("{seriesID:.+}.xml")
266   @RestQuery(
267       name = "getAsXml",
268       description = "Returns the series with the given identifier",
269       returnDescription = "Returns the series dublin core XML document",
270       pathParameters = {
271           @RestParameter(name = "seriesID", isRequired = true, description = "The series identifier", type = STRING)
272       },
273       responses = {
274           @RestResponse(responseCode = SC_OK, description = "The series dublin core."),
275           @RestResponse(responseCode = SC_NOT_FOUND, description = "No series with this identifier was found."),
276           @RestResponse(responseCode = SC_FORBIDDEN, description = "You do not have permission to view this series."),
277           @RestResponse(
278               responseCode = SC_UNAUTHORIZED,
279               description = "You do not have permission to view this series. Maybe you need to authenticate."
280           )
281       }
282   )
283   public Response getSeriesXml(@PathParam("seriesID") String seriesID) {
284     logger.debug("Series Lookup: {}", seriesID);
285     try {
286       DublinCoreCatalog dc = this.seriesService.getSeries(seriesID);
287       return Response.ok(dc.toXmlString()).build();
288     } catch (NotFoundException e) {
289       return Response.status(Response.Status.NOT_FOUND).build();
290     } catch (UnauthorizedException e) {
291       logger.warn("permission exception retrieving series");
292       // TODO this should be an 403 (Forbidden) if the user is logged in
293       throw new WebApplicationException(Response.Status.UNAUTHORIZED);
294     } catch (Exception e) {
295       logger.error("Could not retrieve series: {}", e.getMessage());
296       throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
297     }
298   }
299 
300   @GET
301   @Produces(MediaType.APPLICATION_JSON)
302   @Path("{seriesID:.+}.json")
303   @RestQuery(
304       name = "getAsJson",
305       description = "Returns the series with the given identifier",
306       returnDescription = "Returns the series dublin core JSON document",
307       pathParameters = {
308           @RestParameter(name = "seriesID", isRequired = true, description = "The series identifier", type = STRING)
309       },
310       responses = {
311           @RestResponse(responseCode = SC_OK, description = "The series dublin core."),
312           @RestResponse(responseCode = SC_NOT_FOUND, description = "No series with this identifier was found."),
313           @RestResponse(
314               responseCode = SC_UNAUTHORIZED,
315               description = "You do not have permission to view this series. Maybe you need to authenticate."
316           )
317       }
318   )
319   public Response getSeriesJSON(@PathParam("seriesID") String seriesID) {
320     logger.debug("Series Lookup: {}", seriesID);
321     try {
322       DublinCoreCatalog dc = this.seriesService.getSeries(seriesID);
323       return Response.ok(dc.toJson()).build();
324     } catch (NotFoundException e) {
325       return Response.status(Response.Status.NOT_FOUND).build();
326     } catch (UnauthorizedException e) {
327       logger.warn("permission exception retrieving series");
328       // TODO this should be an 403 (Forbidden) if the user is logged in
329       throw new WebApplicationException(Response.Status.UNAUTHORIZED);
330     } catch (Exception e) {
331       logger.error("Could not retrieve series: {}", e.getMessage());
332       throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
333     }
334   }
335 
336   @GET
337   @Produces(MediaType.TEXT_XML)
338   @Path("/{seriesID:.+}/acl.xml")
339   @RestQuery(
340       name = "getAclAsXml",
341       description = "Returns the access control list for the series with the given identifier",
342       returnDescription = "Returns the series ACL as XML",
343       pathParameters = {
344           @RestParameter(name = "seriesID", isRequired = true, description = "The series identifier", type = STRING)
345       },
346       responses = {
347           @RestResponse(responseCode = SC_OK, description = "The access control list."),
348           @RestResponse(responseCode = SC_NOT_FOUND, description = "No series with this identifier was found.")
349       }
350   )
351   public Response getSeriesAccessControlListXml(@PathParam("seriesID") String seriesID) {
352     return getSeriesAccessControlList(seriesID);
353   }
354 
355   @GET
356   @Produces(MediaType.APPLICATION_JSON)
357   @Path("/{seriesID:.+}/acl.json")
358   @RestQuery(
359       name = "getAclAsJson",
360       description = "Returns the access control list for the series with the given identifier",
361       returnDescription = "Returns the series ACL as JSON",
362       pathParameters = {
363           @RestParameter(name = "seriesID", isRequired = true, description = "The series identifier", type = STRING)
364       },
365       responses = {
366           @RestResponse(responseCode = SC_OK, description = "The access control list."),
367           @RestResponse(responseCode = SC_NOT_FOUND, description = "No series with this identifier was found.")
368       }
369   )
370   public Response getSeriesAccessControlListJson(@PathParam("seriesID") String seriesID) {
371     return getSeriesAccessControlList(seriesID);
372   }
373 
374   @GET
375   @Produces(MediaType.APPLICATION_JSON)
376   @Path("/allInRangeAdministrative.json")
377   @RestQuery(
378       name = "allInRangeAdministrative",
379       description = "Internal API! Returns all series (included deleted ones!) in the given "
380           + "range 'from' (inclusive) .. 'to' (exclusive). Returns at most 'limit' many series. "
381           + "Can only be used as administrator!",
382       returnDescription = "Series in the range",
383       restParameters = {
384           @RestParameter(
385               name = "from",
386               isRequired = true,
387               description = "Start of date range (inclusive) in milliseconds "
388                   + "since 1970-01-01T00:00:00Z. Has to be >=0.",
389               type = Type.INTEGER
390           ),
391           @RestParameter(
392               name = "to",
393               isRequired = false,
394               // TODO: this shows the default value as 0 despite us not setting this value!
395               description = "End of date range (exclusive) in milliseconds "
396                   + "since 1970-01-01T00:00:00Z. Has to be > 'from'.",
397               type = Type.INTEGER
398           ),
399           @RestParameter(
400               name = "limit",
401               isRequired = true,
402               description = "Maximum number of series to be returned. Has to be >0.",
403               type = Type.INTEGER
404           ),
405       },
406       responses = {
407           @RestResponse(responseCode = SC_OK, description = "All series in the range"),
408           @RestResponse(responseCode = SC_BAD_REQUEST, description = "if the given parameters are invalid"),
409           @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the user is not an administrator"),
410       }
411   )
412   public Response getAllInRangeAdministrative(
413       @FormParam("from") Long from,
414       @FormParam("to") Long to,
415       @FormParam("limit") Integer limit
416   ) throws UnauthorizedException {
417     // Parameter error handling
418     if (from == null) {
419       return badRequestAllInRange("Required parameter 'from' not specified");
420     }
421     if (limit == null) {
422       return badRequestAllInRange("Required parameter 'limit' not specified");
423     }
424     if (from < 0) {
425       return badRequestAllInRange("Parameter 'from' < 0, but it has to be >= 0");
426     }
427     if (to != null && to <= from) {
428       return badRequestAllInRange("Parameter 'to' <= 'from', but that is not allowed");
429     }
430     if (limit <= 0) {
431       return badRequestAllInRange("Parameter 'limit' <= 0, but it has to be > 0");
432     }
433 
434     try {
435       final List<Series> series = seriesService.getAllForAdministrativeRead(
436           new Date(from),
437           Optional.ofNullable(to).map(millis -> new Date(millis)),
438           limit);
439 
440       return Response.ok(gson.toJson(series)).build();
441     } catch (SeriesException e) {
442       logger.error("Unexpected exception in getAllInRangeAdministrative", e);
443       return Response.status(INTERNAL_SERVER_ERROR)
444           .entity("internal server error")
445           .build();
446     }
447   }
448 
449   /**
450    * Returns a {@code Response} object representing a BAD_REQUEST to `allInRangeAdministrative`
451    * with the given message as body. Also logs the message.
452    */
453   private static Response badRequestAllInRange(String msg) {
454     logger.debug("Bad request to /series/allInRangeAdministrative: {}", msg);
455     return Response.status(BAD_REQUEST).entity(msg).build();
456   }
457 
458   /**
459    * Retrieves ACL associated with series.
460    *
461    * @param seriesID
462    *          series of which ACL should be retrieved
463    * @return
464    */
465   private Response getSeriesAccessControlList(String seriesID) {
466     logger.debug("Series ACL lookup: {}", seriesID);
467     try {
468       AccessControlList acl = seriesService.getSeriesAccessControl(seriesID);
469       return Response.ok(acl).build();
470     } catch (NotFoundException e) {
471       return Response.status(NOT_FOUND).build();
472     } catch (SeriesException e) {
473       logger.error("Could not retrieve series ACL: {}", e.getMessage());
474     }
475     throw new WebApplicationException(INTERNAL_SERVER_ERROR);
476   }
477 
478   private void addDcData(final DublinCoreCatalog dc, final String field, final String value) {
479     if (StringUtils.isNotBlank(value)) {
480       EName en = new EName(DublinCore.TERMS_NS_URI, field);
481       dc.add(en, value);
482     }
483   }
484 
485   @POST
486   @Path("/")
487   @RestQuery(
488       name = "updateSeries",
489       description = "Updates a series",
490       returnDescription = "No content.",
491       restParameters = {
492           @RestParameter(
493               name = "series",
494               isRequired = false,
495               defaultValue = SAMPLE_DUBLIN_CORE,
496               description = "The series document. Will take precedence over metadata fields",
497               type = TEXT
498           ),
499           @RestParameter(
500               name = "acl",
501               isRequired = false,
502               defaultValue = SAMPLE_ACCESS_CONTROL_LIST,
503               description = "The access control list for the series",
504               type = TEXT
505           ),
506           @RestParameter(
507               description = "Series metadata value",
508               isRequired = false,
509               name = "accessRights",
510               type = RestParameter.Type.STRING
511           ),
512           @RestParameter(
513               description = "Series metadata value",
514               isRequired = false,
515               name = "available",
516               type = RestParameter.Type.STRING
517           ),
518           @RestParameter(
519               description = "Series metadata value",
520               isRequired = false,
521               name = "contributor",
522               type = RestParameter.Type.STRING
523           ),
524           @RestParameter(
525               description = "Series metadata value",
526               isRequired = false,
527               name = "coverage",
528               type = RestParameter.Type.STRING
529           ),
530           @RestParameter(
531               description = "Series metadata value",
532               isRequired = false,
533               name = "created",
534               type = RestParameter.Type.STRING
535           ),
536           @RestParameter(
537               description = "Series metadata value",
538               isRequired = false,
539               name = "creator",
540               type = RestParameter.Type.STRING
541           ),
542           @RestParameter(
543               description = "Series metadata value",
544               isRequired = false,
545               name = "date",
546               type = RestParameter.Type.STRING
547           ),
548           @RestParameter(
549               description = "Series metadata value",
550               isRequired = false,
551               name = "description",
552               type = RestParameter.Type.STRING
553           ),
554           @RestParameter(
555               description = "Series metadata value",
556               isRequired = false,
557               name = "extent",
558               type = RestParameter.Type.STRING
559           ),
560           @RestParameter(
561               description = "Series metadata value",
562               isRequired = false,
563               name = "format",
564               type = RestParameter.Type.STRING
565           ),
566           @RestParameter(
567               description = "Series metadata value",
568               isRequired = false,
569               name = "identifier",
570               type = RestParameter.Type.STRING
571           ),
572           @RestParameter(
573               description = "Series metadata value",
574               isRequired = false,
575               name = "isPartOf",
576               type = RestParameter.Type.STRING
577           ),
578           @RestParameter(
579               description = "Series metadata value",
580               isRequired = false,
581               name = "isReferencedBy",
582               type = RestParameter.Type.STRING
583           ),
584           @RestParameter(
585               description = "Series metadata value",
586               isRequired = false,
587               name = "isReplacedBy",
588               type = RestParameter.Type.STRING
589           ),
590           @RestParameter(
591               description = "Series metadata value",
592               isRequired = false,
593               name = "language",
594               type = RestParameter.Type.STRING
595           ),
596           @RestParameter(
597               description = "Series metadata value",
598               isRequired = false,
599               name = "license",
600               type = RestParameter.Type.STRING
601           ),
602           @RestParameter(
603               description = "Series metadata value",
604               isRequired = false,
605               name = "publisher",
606               type = RestParameter.Type.STRING
607           ),
608           @RestParameter(
609               description = "Series metadata value",
610               isRequired = false,
611               name = "relation",
612               type = RestParameter.Type.STRING
613           ),
614           @RestParameter(
615               description = "Series metadata value",
616               isRequired = false,
617               name = "replaces",
618               type = RestParameter.Type.STRING
619           ),
620           @RestParameter(
621               description = "Series metadata value",
622               isRequired = false,
623               name = "rights",
624               type = RestParameter.Type.STRING
625           ),
626           @RestParameter(
627               description = "Series metadata value",
628               isRequired = false,
629               name = "rightsHolder",
630               type = RestParameter.Type.STRING
631           ),
632           @RestParameter(
633               description = "Series metadata value",
634               isRequired = false,
635               name = "source",
636               type = RestParameter.Type.STRING
637           ),
638           @RestParameter(
639               description = "Series metadata value",
640               isRequired = false,
641               name = "spatial",
642               type = RestParameter.Type.STRING
643           ),
644           @RestParameter(
645               description = "Series metadata value",
646               isRequired = false,
647               name = "subject",
648               type = RestParameter.Type.STRING
649           ),
650           @RestParameter(
651               description = "Series metadata value",
652               isRequired = false,
653               name = "temporal",
654               type = RestParameter.Type.STRING
655           ),
656           @RestParameter(
657               description = "Series metadata value",
658               isRequired = false,
659               name = "title",
660               type = RestParameter.Type.STRING
661           ),
662           @RestParameter(
663               description = "Series metadata value",
664               isRequired = false,
665               name = "type",
666               type = RestParameter.Type.STRING
667           ),
668           @RestParameter(
669               name = "override",
670               isRequired = false,
671               defaultValue = "false",
672               description = "If true the series ACL will take precedence over any existing episode ACL",
673               type = STRING
674           )
675       },
676       responses = {
677           @RestResponse(
678               responseCode = SC_BAD_REQUEST,
679               description = "The required form params were missing in the request."
680           ),
681           @RestResponse(
682               responseCode = SC_NO_CONTENT,
683               description = "The access control list has been updated."
684           ),
685           @RestResponse(
686               responseCode = SC_UNAUTHORIZED,
687               description = "If the current user is not authorized to perform this action"
688           ),
689           @RestResponse(
690               responseCode = SC_CREATED,
691               description = "The access control list has been created."
692           )
693       }
694   )
695   public Response addOrUpdateSeries(
696       @FormParam("series") String series,
697       @FormParam("acl") String accessControl,
698       @FormParam("accessRights") String dcAccessRights,
699       @FormParam("available") String dcAvailable,
700       @FormParam("contributor") String dcContributor,
701       @FormParam("coverage") String dcCoverage,
702       @FormParam("created") String dcCreated,
703       @FormParam("creator") String dcCreator,
704       @FormParam("date") String dcDate,
705       @FormParam("description") String dcDescription,
706       @FormParam("extent") String dcExtent,
707       @FormParam("format") String dcFormat,
708       @FormParam("identifier") String dcIdentifier,
709       @FormParam("isPartOf") String dcIsPartOf,
710       @FormParam("isReferencedBy") String dcIsReferencedBy,
711       @FormParam("isReplacedBy") String dcIsReplacedBy,
712       @FormParam("language") String dcLanguage,
713       @FormParam("license") String dcLicense,
714       @FormParam("publisher") String dcPublisher,
715       @FormParam("relation") String dcRelation,
716       @FormParam("replaces") String dcReplaces,
717       @FormParam("rights") String dcRights,
718       @FormParam("rightsHolder") String dcRightsHolder,
719       @FormParam("source") String dcSource,
720       @FormParam("spatial") String dcSpatial,
721       @FormParam("subject") String dcSubject,
722       @FormParam("temporal") String dcTemporal,
723       @FormParam("title") String dcTitle,
724       @FormParam("type") String dcType,
725       @DefaultValue("false") @FormParam("override") boolean override
726   ) throws UnauthorizedException {
727     DublinCoreCatalog dc;
728     if (StringUtils.isNotBlank(series)) {
729       try {
730         dc = this.dcService.load(new ByteArrayInputStream(series.getBytes(StandardCharsets.UTF_8)));
731       } catch (UnsupportedEncodingException e1) {
732         logger.error("Could not deserialize dublin core catalog", e1);
733         throw new WebApplicationException(INTERNAL_SERVER_ERROR);
734       } catch (IOException e1) {
735         logger.warn("Could not deserialize dublin core catalog", e1);
736         return Response.status(BAD_REQUEST).build();
737       }
738     } else if (StringUtils.isNotBlank(dcTitle)) {
739       dc = DublinCores.mkOpencastSeries().getCatalog();
740       addDcData(dc, "accessRights", dcAccessRights);
741       addDcData(dc, "available", dcAvailable);
742       addDcData(dc, "contributor", dcContributor);
743       addDcData(dc, "coverage", dcCoverage);
744       addDcData(dc, "created", dcCreated);
745       addDcData(dc, "creator", dcCreator);
746       addDcData(dc, "date", dcDate);
747       addDcData(dc, "description", dcDescription);
748       addDcData(dc, "extent", dcExtent);
749       addDcData(dc, "format", dcFormat);
750       addDcData(dc, "identifier", dcIdentifier);
751       addDcData(dc, "isPartOf", dcIsPartOf);
752       addDcData(dc, "isReferencedBy", dcIsReferencedBy);
753       addDcData(dc, "isReplacedBy", dcIsReplacedBy);
754       addDcData(dc, "language", dcLanguage);
755       addDcData(dc, "license", dcLicense);
756       addDcData(dc, "publisher", dcPublisher);
757       addDcData(dc, "relation", dcRelation);
758       addDcData(dc, "replaces", dcReplaces);
759       addDcData(dc, "rights", dcRights);
760       addDcData(dc, "rightsHolder", dcRightsHolder);
761       addDcData(dc, "source", dcSource);
762       addDcData(dc, "spatial", dcSpatial);
763       addDcData(dc, "subject", dcSubject);
764       addDcData(dc, "temporal", dcTemporal);
765       addDcData(dc, "title", dcTitle);
766       addDcData(dc, "type", dcType);
767     } else {
768       return Response.status(BAD_REQUEST).entity("Required series metadata not provided").build();
769     }
770     AccessControlList acl = null;
771     if (StringUtils.isNotBlank(accessControl)) {
772       try {
773         acl = AccessControlParser.parseAcl(accessControl);
774       } catch (Exception e) {
775         logger.debug("Could not parse ACL", e);
776         return Response.status(BAD_REQUEST).entity("Could not parse ACL").build();
777       }
778     }
779 
780     try {
781       DublinCoreCatalog newSeries = seriesService.updateSeries(dc);
782       if (acl != null) {
783         seriesService.updateAccessControl(dc.getFirst(PROPERTY_IDENTIFIER), acl, override);
784       }
785       if (newSeries == null) {
786         logger.debug("Updated series {} ", dc.getFirst(PROPERTY_IDENTIFIER));
787         return Response.status(NO_CONTENT).build();
788       }
789       String id = newSeries.getFirst(PROPERTY_IDENTIFIER);
790       logger.debug("Created series {} ", id);
791       return Response.status(CREATED)
792           .header("Location", getSeriesXmlUrl(id))
793           .header("Location", getSeriesJsonUrl(id))
794           .entity(newSeries.toXmlString())
795           .build();
796     } catch (UnauthorizedException e) {
797       throw e;
798     } catch (Exception e) {
799       logger.error("Could not add/update series", e);
800     }
801     return Response.serverError().build();
802   }
803 
804   @POST
805   @Path("/{seriesID:.+}/accesscontrol")
806   @RestQuery(
807       name = "updateAcl",
808       description = "Updates the access control list for a series",
809       returnDescription = "No content.",
810       restParameters = {
811           @RestParameter(
812               name = "acl",
813               isRequired = true,
814               defaultValue = SAMPLE_ACCESS_CONTROL_LIST,
815               description = "The access control list for the series",
816               type = TEXT
817           ),
818           @RestParameter(
819               name = "override",
820               isRequired = false,
821               defaultValue = "false",
822               description = "If true the series ACL will take precedence over any existing episode ACL",
823               type = STRING
824           )
825       },
826       pathParameters = {
827           @RestParameter(name = "seriesID", isRequired = true, description = "The series identifier", type = STRING)
828       },
829       responses = {
830           @RestResponse(
831               responseCode = SC_NOT_FOUND,
832               description = "No series with this identifier was found."
833           ),
834           @RestResponse(
835               responseCode = SC_NO_CONTENT,
836               description = "The access control list has been updated."
837           ),
838           @RestResponse(
839               responseCode = SC_CREATED,
840               description = "The access control list has been created."
841           ),
842           @RestResponse(
843               responseCode = SC_UNAUTHORIZED,
844               description = "If the current user is not authorized to perform this action"
845           ),
846           @RestResponse(
847               responseCode = SC_BAD_REQUEST,
848               description = "The required path or form params were missing in the request."
849           )
850       }
851   )
852   public Response updateAccessControl(
853       @PathParam("seriesID") String seriesID,
854       @FormParam("acl") String accessControl,
855       @DefaultValue("false") @FormParam("override") boolean override
856   ) throws UnauthorizedException {
857     if (accessControl == null) {
858       logger.warn("Access control parameter is null.");
859       return Response.status(BAD_REQUEST).build();
860     }
861     AccessControlList acl;
862     try {
863       acl = AccessControlParser.parseAcl(accessControl);
864     } catch (Exception e) {
865       logger.warn("Could not parse ACL: {}", e.getMessage());
866       return Response.status(BAD_REQUEST).build();
867     }
868     try {
869       boolean updated = seriesService.updateAccessControl(seriesID, acl, override);
870       if (updated) {
871         return Response.status(NO_CONTENT).build();
872       }
873       return Response.status(CREATED).build();
874     } catch (NotFoundException e) {
875       return Response.status(NOT_FOUND).build();
876     } catch (SeriesException e) {
877       logger.warn("Could not update ACL for {}: {}", seriesID, e.getMessage());
878     }
879     throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
880   }
881 
882   @DELETE
883   @Path("/{seriesID:.+}")
884   @RestQuery(
885       name = "delete",
886       description = "Delete a series",
887       returnDescription = "No content.",
888       pathParameters = {
889           @RestParameter(name = "seriesID", isRequired = true, description = "The series identifier", type = STRING)
890       },
891       responses = {
892           @RestResponse(
893               responseCode = SC_NOT_FOUND,
894               description = "No series with this identifier was found."
895           ),
896           @RestResponse(
897               responseCode = SC_UNAUTHORIZED,
898               description = "If the current user is not authorized to perform this action"
899           ),
900           @RestResponse(
901               responseCode = SC_NO_CONTENT,
902               description = "The series was deleted."
903           )
904       }
905   )
906   public Response deleteSeries(@PathParam("seriesID") String seriesID) throws UnauthorizedException {
907     try {
908       this.seriesService.deleteSeries(seriesID);
909       return Response.ok().build();
910     } catch (NotFoundException e) {
911       return Response.status(Response.Status.NOT_FOUND).build();
912     } catch (SeriesException se) {
913       logger.warn("Could not delete series {}: {}", seriesID, se.getMessage());
914     }
915     throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
916   }
917 
918   @GET
919   @Produces(MediaType.TEXT_PLAIN)
920   @Path("/count")
921   @RestQuery(
922       name = "count",
923       description = "Returns the number of series",
924       returnDescription = "Returns the number of series",
925       responses = {
926           @RestResponse(responseCode = SC_OK, description = "The number of series")
927       }
928   )
929   public Response getCount() throws UnauthorizedException {
930     try {
931       int count = seriesService.getSeriesCount();
932       return Response.ok(count).build();
933     } catch (SeriesException se) {
934       logger.warn("Could not count series: {}", se.getMessage());
935       throw new WebApplicationException(se);
936     }
937   }
938 
939   @SuppressWarnings("unchecked")
940   @GET
941   @Produces(MediaType.APPLICATION_JSON)
942   @Path("{id}/properties.json")
943   @RestQuery(
944       name = "getSeriesProperties",
945       description = "Returns the series properties",
946       returnDescription = "Returns the series properties as JSON",
947       pathParameters = {
948           @RestParameter(name = "id", description = "ID of series", isRequired = true, type = Type.STRING)
949       },
950       responses = {
951           @RestResponse(
952               responseCode = SC_OK,
953               description = "The access control list."
954           ),
955           @RestResponse(
956               responseCode = SC_UNAUTHORIZED,
957               description = "If the current user is not authorized to perform this action"
958           )
959       }
960   )
961   public Response getSeriesPropertiesAsJson(@PathParam("id") String seriesId)
962           throws UnauthorizedException, NotFoundException {
963     if (StringUtils.isBlank(seriesId)) {
964       logger.warn("Series id parameter is blank '{}'.", seriesId);
965       return Response.status(BAD_REQUEST).build();
966     }
967     try {
968       Map<String, String> properties = seriesService.getSeriesProperties(seriesId);
969       JSONArray jsonProperties = new JSONArray();
970       for (String name : properties.keySet()) {
971         JSONObject property = new JSONObject();
972         property.put(name, properties.get(name));
973         jsonProperties.add(property);
974       }
975       return Response.ok(jsonProperties.toString()).build();
976     } catch (UnauthorizedException e) {
977       throw e;
978     } catch (NotFoundException e) {
979       throw e;
980     } catch (Exception e) {
981       logger.warn("Could not perform search query: {}", e.getMessage());
982     }
983     throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
984   }
985 
986   @GET
987   @Produces(MediaType.APPLICATION_JSON)
988   @Path("{seriesId}/property/{propertyName}.json")
989   @RestQuery(
990       name = "getSeriesProperty",
991       description = "Returns a series property value",
992       returnDescription = "Returns the series property value",
993       pathParameters = {
994           @RestParameter(
995               name = "seriesId",
996               description = "ID of series",
997               isRequired = true,
998               type = Type.STRING
999           ),
1000           @RestParameter(
1001               name = "propertyName",
1002               description = "Name of series property",
1003               isRequired = true,
1004               type = Type.STRING
1005           )
1006       },
1007       responses = {
1008           @RestResponse(
1009               responseCode = SC_OK,
1010               description = "The access control list."
1011           ),
1012           @RestResponse(
1013               responseCode = SC_UNAUTHORIZED,
1014               description = "If the current user is not authorized to perform this action"
1015           )
1016       }
1017   )
1018   public Response getSeriesProperty(@PathParam("seriesId") String seriesId,
1019           @PathParam("propertyName") String propertyName) throws UnauthorizedException, NotFoundException {
1020     if (StringUtils.isBlank(seriesId)) {
1021       logger.warn("Series id parameter is blank '{}'.", seriesId);
1022       return Response.status(BAD_REQUEST).build();
1023     }
1024     if (StringUtils.isBlank(propertyName)) {
1025       logger.warn("Series property name parameter is blank '{}'.", propertyName);
1026       return Response.status(BAD_REQUEST).build();
1027     }
1028     try {
1029       String propertyValue = seriesService.getSeriesProperty(seriesId, propertyName);
1030       return Response.ok(propertyValue).build();
1031     } catch (UnauthorizedException e) {
1032       throw e;
1033     } catch (NotFoundException e) {
1034       throw e;
1035     } catch (Exception e) {
1036       logger.warn("Could not perform search query", e);
1037     }
1038     throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1039   }
1040 
1041   @POST
1042   @Path("/{seriesId}/property")
1043   @RestQuery(
1044       name = "updateSeriesProperty",
1045       description = "Updates a series property",
1046       returnDescription = "No content.",
1047       restParameters = {
1048           @RestParameter(name = "name", isRequired = true, description = "The property's name", type = TEXT),
1049           @RestParameter(name = "value", isRequired = true, description = "The property's value", type = TEXT)
1050       },
1051       pathParameters = {
1052           @RestParameter(name = "seriesId", isRequired = true, description = "The series identifier", type = STRING)
1053       },
1054       responses = {
1055           @RestResponse(
1056               responseCode = SC_NOT_FOUND,
1057               description = "No series with this identifier was found."
1058           ),
1059           @RestResponse(
1060               responseCode = SC_NO_CONTENT,
1061               description = "The access control list has been updated."
1062           ),
1063           @RestResponse(
1064               responseCode = SC_UNAUTHORIZED,
1065               description = "If the current user is not authorized to perform this action"
1066           ),
1067           @RestResponse(
1068               responseCode = SC_BAD_REQUEST,
1069               description = "The required path or form params were missing in the request."
1070           )
1071       }
1072   )
1073   public Response updateSeriesProperty(
1074       @PathParam("seriesId") String seriesId,
1075       @FormParam("name") String name,
1076       @FormParam("value") String value
1077   ) throws UnauthorizedException {
1078     if (StringUtils.isBlank(seriesId)) {
1079       logger.warn("Series id parameter is blank '{}'.", seriesId);
1080       return Response.status(BAD_REQUEST).build();
1081     }
1082     if (StringUtils.isBlank(name)) {
1083       logger.warn("Name parameter is blank '{}'.", name);
1084       return Response.status(BAD_REQUEST).build();
1085     }
1086     if (StringUtils.isBlank(value)) {
1087       logger.warn("Series id parameter is blank '{}'.", value);
1088       return Response.status(BAD_REQUEST).build();
1089     }
1090     try {
1091       seriesService.updateSeriesProperty(seriesId, name, value);
1092       return Response.status(NO_CONTENT).build();
1093     } catch (NotFoundException e) {
1094       return Response.status(NOT_FOUND).build();
1095     } catch (SeriesException e) {
1096       logger.warn("Could not update series property for series {} property {}:{} :", seriesId, name, value, e);
1097     }
1098     throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1099   }
1100 
1101   @DELETE
1102   @Path("{seriesId}/property/{propertyName}")
1103   @RestQuery(
1104       name = "deleteSeriesProperty",
1105       description = "Deletes a series property",
1106       returnDescription = "No Content",
1107       pathParameters = {
1108           @RestParameter(
1109               name = "seriesId",
1110               description = "ID of series",
1111               isRequired = true,
1112               type = Type.STRING
1113           ),
1114           @RestParameter(
1115               name = "propertyName",
1116               description = "Name of series property",
1117               isRequired = true,
1118               type = Type.STRING
1119           )
1120       },
1121       responses = {
1122           @RestResponse(
1123               responseCode = SC_NO_CONTENT,
1124               description = "The series property has been deleted."
1125           ),
1126           @RestResponse(
1127               responseCode = SC_NOT_FOUND,
1128               description = "The series or property has not been found."
1129           ),
1130           @RestResponse(
1131               responseCode = SC_UNAUTHORIZED,
1132               description = "If the current user is not authorized to perform this action"
1133           )
1134       }
1135   )
1136   public Response deleteSeriesProperty(
1137       @PathParam("seriesId") String seriesId,
1138       @PathParam("propertyName") String propertyName
1139   ) throws UnauthorizedException, NotFoundException {
1140     if (StringUtils.isBlank(seriesId)) {
1141       logger.warn("Series id parameter is blank '{}'.", seriesId);
1142       return Response.status(BAD_REQUEST).build();
1143     }
1144     if (StringUtils.isBlank(propertyName)) {
1145       logger.warn("Series property name parameter is blank '{}'.", propertyName);
1146       return Response.status(BAD_REQUEST).build();
1147     }
1148     try {
1149       seriesService.deleteSeriesProperty(seriesId, propertyName);
1150       return Response.status(NO_CONTENT).build();
1151     } catch (UnauthorizedException e) {
1152       throw e;
1153     } catch (NotFoundException e) {
1154       throw e;
1155     } catch (Exception e) {
1156       logger.warn("Could not delete series '{}' property '{}' query:", seriesId, propertyName, e);
1157     }
1158     throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
1159   }
1160 
1161   @GET
1162   @Path("{seriesId}/elements.json")
1163   @Produces(MediaType.APPLICATION_JSON)
1164   @RestQuery(
1165       name = "getSeriesElements",
1166       description = "Returns all the element types of a series",
1167       returnDescription = "Returns a JSON array with all the types of elements of the given series.",
1168       pathParameters = {
1169           @RestParameter(name = "seriesId", description = "The series identifier", type = STRING, isRequired = true)
1170       },
1171       responses = {
1172           @RestResponse(responseCode = SC_OK, description = "Series found"),
1173           @RestResponse(responseCode = SC_NOT_FOUND, description = "Series not found"),
1174           @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error while processing the request")
1175       }
1176   )
1177   public Response getSeriesElements(@PathParam("seriesId") String seriesId) {
1178     try {
1179       Optional<Map<String, byte[]>> optSeriesElements = seriesService.getSeriesElements(seriesId);
1180       if (optSeriesElements.isPresent()) {
1181         Map<String, byte[]> seriesElements = optSeriesElements.get();
1182         JsonArray jsonArray = new JsonArray();
1183         for (String key : seriesElements.keySet()) {
1184           jsonArray.add(new JsonPrimitive(key));
1185         }
1186         String json = new Gson().toJson(jsonArray);
1187         return Response.ok(json).build();
1188       } else {
1189         return R.notFound();
1190       }
1191     } catch (SeriesException e) {
1192       logger.warn("Error while retrieving elements for sieres '{}'", seriesId, e);
1193       return R.serverError();
1194     }
1195   }
1196 
1197   @GET
1198   @Path("{seriesId}/elements/{elementType}")
1199   @RestQuery(
1200       name = "getSeriesElement",
1201       description = "Returns the series element",
1202       returnDescription = "The data of the series element",
1203       pathParameters = {
1204           @RestParameter(
1205               name = "seriesId",
1206               description = "The series identifier",
1207               type = STRING,
1208               isRequired = true
1209           ),
1210           @RestParameter(
1211               name = "elementType",
1212               description = "The element type. This is equal to the subtype of the media type of "
1213                   + "this element: series/<elementtype>",
1214               type = STRING,
1215               isRequired = true
1216           )
1217       },
1218       responses = {
1219           @RestResponse(responseCode = SC_OK, description = "Series element found"),
1220           @RestResponse(responseCode = SC_NOT_FOUND, description = "Series element not found"),
1221           @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error while processing the request")
1222       }
1223   )
1224   public Response getSeriesElement(
1225       @PathParam("seriesId") String seriesId,
1226       @PathParam("elementType") String elementType
1227   ) {
1228     try {
1229       Optional<byte[]> data = seriesService.getSeriesElementData(seriesId, elementType);
1230       if (data.isPresent()) {
1231         return Response.ok().entity(new ByteArrayInputStream(data.get()))
1232                 .type(SERIES_ELEMENT_CONTENT_TYPE_PREFIX + elementType).build();
1233       } else {
1234         return R.notFound();
1235       }
1236     } catch (SeriesException e) {
1237       logger.warn("Error while returning element '{}' of series '{}':", elementType, seriesId, e);
1238       return R.serverError();
1239     }
1240   }
1241 
1242   @PUT
1243   @Path("{seriesId}/extendedMetadata/{type}")
1244   @RestQuery(
1245           name = "updateExtendedMetadata",
1246           description = "Updates extended metadata of a series",
1247           returnDescription = "An empty response",
1248           pathParameters = {
1249                   @RestParameter(name = "seriesId", description = "The series identifier", type = STRING,
1250                           isRequired = true),
1251                   @RestParameter(name = "type", description = "The type of the catalog flavor", type = STRING,
1252                           isRequired = true)
1253           },
1254           restParameters = {
1255                   @RestParameter(name = "dc", description = "The catalog with extended metadata.", type = TEXT,
1256                           isRequired = true, defaultValue = SAMPLE_DUBLIN_CORE
1257                   )
1258           },
1259           responses = {
1260                   @RestResponse(responseCode = SC_NO_CONTENT, description = "Extended metadata updated"),
1261                   @RestResponse(responseCode = SC_CREATED, description = "Extended metadata created"),
1262                   @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR,
1263                           description = "Error while processing the request")
1264           }
1265   )
1266   public Response putSeriesExtendedMetadata(
1267           @PathParam("seriesId") String seriesId,
1268           @PathParam("type") String type,
1269           @FormParam("dc") String dcString
1270   ) {
1271     try {
1272       DublinCoreCatalog dc = dcService.load(new ByteArrayInputStream(dcString.getBytes(StandardCharsets.UTF_8)));
1273       boolean elementExists = seriesService.getSeriesElementData(seriesId, type).isPresent();
1274       if (seriesService.updateExtendedMetadata(seriesId, type, dc)) {
1275         if (elementExists) {
1276           return R.noContent();
1277         } else {
1278           return R.created(URI.create(UrlSupport.concat(serverUrl, serviceUrl, seriesId, "elements", type)));
1279         }
1280       } else {
1281         return R.serverError();
1282       }
1283     } catch (IOException e) {
1284       logger.warn("Could not deserialize dublin core catalog", e);
1285       return Response.status(BAD_REQUEST).build();
1286     } catch (SeriesException e) {
1287       logger.warn("Error while updating extended metadata of series '{}'", seriesId, e);
1288       return R.serverError();
1289     }
1290   }
1291 
1292 
1293   @PUT
1294   @Path("{seriesId}/elements/{elementType}")
1295   @RestQuery(
1296       name = "updateSeriesElement",
1297       description = "Updates an existing series element",
1298       returnDescription = "An empty response",
1299       pathParameters = {
1300           @RestParameter(name = "seriesId", description = "The series identifier", type = STRING, isRequired = true),
1301           @RestParameter(name = "elementType", description = "The element type", type = STRING, isRequired = true)
1302       },
1303       responses = {
1304           @RestResponse(responseCode = SC_NO_CONTENT, description = "Series element updated"),
1305           @RestResponse(responseCode = SC_CREATED, description = "Series element created"),
1306           @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error while processing the request")
1307       }
1308   )
1309   public Response putSeriesElement(
1310       @Context HttpServletRequest request,
1311       @PathParam("seriesId") String seriesId,
1312       @PathParam("elementType") String elementType
1313   ) {
1314     InputStream is = null;
1315     try {
1316       is = request.getInputStream();
1317       final byte[] data = IOUtils.toByteArray(is);
1318       boolean elementExists = seriesService.getSeriesElementData(seriesId, elementType).isPresent();
1319       if (seriesService.updateSeriesElement(seriesId, elementType, data)) {
1320         if (elementExists) {
1321           return R.noContent();
1322         } else {
1323           return R.created(URI.create(UrlSupport.concat(serverUrl, serviceUrl, seriesId, "elements", elementType)));
1324         }
1325       } else {
1326         return R.serverError();
1327       }
1328     } catch (IOException e) {
1329       logger.error("Error while trying to read from request", e);
1330       return R.serverError();
1331     } catch (SeriesException e) {
1332       logger.warn("Error while adding element to series '{}'", seriesId, e);
1333       return R.serverError();
1334     } finally {
1335       IOUtils.closeQuietly(is);
1336     }
1337   }
1338 
1339   @DELETE
1340   @Path("{seriesId}/elements/{elementType}")
1341   @RestQuery(
1342       name = "deleteSeriesElement",
1343       description = "Deletes a series element",
1344       returnDescription = "An empty response",
1345       pathParameters = {
1346           @RestParameter(name = "seriesId", description = "The series identifier", type = STRING, isRequired = true),
1347           @RestParameter(name = "elementType", description = "The element type", type = STRING, isRequired = true)
1348       },
1349       responses = {
1350           @RestResponse(responseCode = SC_NO_CONTENT, description = "Series element deleted"),
1351           @RestResponse(responseCode = SC_NOT_FOUND, description = "Series element not found"),
1352           @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "Error while processing the request")
1353       }
1354   )
1355   public Response deleteSeriesElement(
1356       @PathParam("seriesId") String seriesId,
1357       @PathParam("elementType") String elementType
1358   ) {
1359     try {
1360       if (seriesService.deleteSeriesElement(seriesId, elementType)) {
1361         return R.noContent();
1362       } else {
1363         return R.notFound();
1364       }
1365     } catch (SeriesException e) {
1366       return R.serverError();
1367     }
1368   }
1369 
1370 }