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