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.remote;
23  
24  import static java.lang.String.format;
25  import static javax.servlet.http.HttpServletResponse.SC_OK;
26  import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
27  import static javax.ws.rs.core.Response.Status.NOT_FOUND;
28  import static org.apache.commons.lang3.StringUtils.isBlank;
29  import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
30  import static org.apache.http.HttpStatus.SC_CREATED;
31  import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
32  import static org.apache.http.HttpStatus.SC_NOT_FOUND;
33  import static org.apache.http.HttpStatus.SC_NO_CONTENT;
34  import static org.apache.http.HttpStatus.SC_UNAUTHORIZED;
35  
36  import org.opencastproject.metadata.dublincore.DublinCore;
37  import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
38  import org.opencastproject.metadata.dublincore.DublinCores;
39  import org.opencastproject.security.api.AccessControlList;
40  import org.opencastproject.security.api.AccessControlParser;
41  import org.opencastproject.security.api.TrustedHttpClient;
42  import org.opencastproject.security.api.UnauthorizedException;
43  import org.opencastproject.series.api.Series;
44  import org.opencastproject.series.api.SeriesException;
45  import org.opencastproject.series.api.SeriesService;
46  import org.opencastproject.serviceregistry.api.RemoteBase;
47  import org.opencastproject.serviceregistry.api.ServiceRegistry;
48  import org.opencastproject.util.NotFoundException;
49  import org.opencastproject.util.doc.rest.RestService;
50  
51  import com.entwinemedia.fn.data.Opt;
52  import com.google.gson.Gson;
53  import com.google.gson.reflect.TypeToken;
54  
55  import org.apache.commons.io.IOUtils;
56  import org.apache.http.HttpResponse;
57  import org.apache.http.NameValuePair;
58  import org.apache.http.client.entity.UrlEncodedFormEntity;
59  import org.apache.http.client.methods.HttpDelete;
60  import org.apache.http.client.methods.HttpGet;
61  import org.apache.http.client.methods.HttpPost;
62  import org.apache.http.client.methods.HttpPut;
63  import org.apache.http.client.utils.URLEncodedUtils;
64  import org.apache.http.entity.ByteArrayEntity;
65  import org.apache.http.entity.ContentType;
66  import org.apache.http.message.BasicNameValuePair;
67  import org.codehaus.jettison.json.JSONArray;
68  import org.codehaus.jettison.json.JSONObject;
69  import org.json.simple.parser.JSONParser;
70  import org.osgi.service.component.annotations.Component;
71  import org.osgi.service.component.annotations.Reference;
72  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
73  import org.slf4j.Logger;
74  import org.slf4j.LoggerFactory;
75  
76  import java.io.IOException;
77  import java.io.InputStreamReader;
78  import java.io.Reader;
79  import java.io.StringWriter;
80  import java.lang.reflect.Type;
81  import java.nio.charset.StandardCharsets;
82  import java.util.ArrayList;
83  import java.util.Date;
84  import java.util.HashMap;
85  import java.util.List;
86  import java.util.Map;
87  import java.util.Optional;
88  import java.util.TreeMap;
89  
90  import javax.ws.rs.GET;
91  import javax.ws.rs.Path;
92  import javax.ws.rs.PathParam;
93  import javax.ws.rs.Produces;
94  import javax.ws.rs.WebApplicationException;
95  import javax.ws.rs.core.MediaType;
96  import javax.ws.rs.core.Response;
97  
98  /**
99   * A proxy to a remote series service.
100  */
101 @Path("/series")
102 @RestService(
103     name = "seriesservice",
104     title = "Series Service Remote",
105     abstractText = "This service creates, edits and retrieves and helps managing series.",
106     notes = {
107         "All paths above are relative to the REST endpoint base (something like http://your.server/files)",
108         "If the service is down or not working it will return a status 503, this means the the "
109             + "underlying service is not working and is either restarting or has failed",
110         "A status code 500 means a general failure has occurred which is not recoverable and was "
111             + "not anticipated. In other words, there is a bug! You should file an error report "
112             + "with your server logs from the time when the error occurred: "
113             + "<a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>"
114     }
115 )
116 @Component(
117     property = {
118         "service.description=Series Remote Service Proxy",
119         "opencast.service.type=org.opencastproject.series",
120         "opencast.service.path=/series",
121         "opencast.service.publish=false"
122     },
123     immediate = true,
124     service = { SeriesService.class, SeriesServiceRemoteImpl.class }
125 )
126 @JaxrsResource
127 public class SeriesServiceRemoteImpl extends RemoteBase implements SeriesService {
128 
129   private static final Logger logger = LoggerFactory.getLogger(SeriesServiceRemoteImpl.class);
130 
131 
132   private static final Gson gson = new Gson();
133   private static final Type seriesListType = new TypeToken<ArrayList<Series>>() { }.getType();
134 
135   public SeriesServiceRemoteImpl() {
136     super(JOB_TYPE);
137   }
138 
139   /**
140    * Sets the trusted http client
141    *
142    * @param client
143    */
144   @Override
145   @Reference
146   public void setTrustedHttpClient(TrustedHttpClient client) {
147     super.setTrustedHttpClient(client);
148   }
149 
150   /**
151    * Sets the remote service manager.
152    *
153    * @param remoteServiceManager
154    */
155   @Override
156   @Reference
157   public void setRemoteServiceManager(ServiceRegistry remoteServiceManager) {
158     super.setRemoteServiceManager(remoteServiceManager);
159   }
160 
161   @Override
162   public DublinCoreCatalog updateSeries(DublinCoreCatalog dc) throws SeriesException, UnauthorizedException {
163     String seriesId = dc.getFirst(DublinCore.PROPERTY_IDENTIFIER);
164 
165     HttpPost post = new HttpPost("/");
166     try {
167       List<BasicNameValuePair> params = new ArrayList<>();
168       params.add(new BasicNameValuePair("series", dc.toXmlString()));
169       post.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8));
170     } catch (Exception e) {
171       throw new SeriesException("Unable to assemble a remote series request for updating series " + seriesId, e);
172     }
173 
174     HttpResponse response = getResponse(post, SC_NO_CONTENT, SC_CREATED, SC_UNAUTHORIZED);
175     try {
176       if (response != null) {
177         int statusCode = response.getStatusLine().getStatusCode();
178         if (SC_NO_CONTENT == statusCode) {
179           logger.info("Successfully updated series {} in the series service", seriesId);
180           return null;
181         } else if (SC_UNAUTHORIZED == statusCode) {
182           throw new UnauthorizedException("Not authorized to update series " + seriesId);
183         } else if (SC_CREATED == statusCode) {
184           DublinCoreCatalog catalogImpl = DublinCores.read(response.getEntity().getContent());
185           logger.info("Successfully created series {} in the series service", seriesId);
186           return catalogImpl;
187         }
188       }
189     } catch (UnauthorizedException e) {
190       throw e;
191     } catch (Exception e) {
192       throw new SeriesException("Unable to update series " + seriesId + " using the remote series services: " + e);
193     } finally {
194       closeConnection(response);
195     }
196     throw new SeriesException("Unable to update series " + seriesId + " using the remote series services");
197   }
198 
199   @Override
200   public boolean updateAccessControl(String seriesID, AccessControlList accessControl)
201           throws NotFoundException, SeriesException, UnauthorizedException {
202     return updateAccessControl(seriesID, accessControl, false);
203   }
204 
205   @Override
206   public boolean updateAccessControl(String seriesID, AccessControlList accessControl, boolean overrideEpisodeAcl)
207           throws NotFoundException, SeriesException, UnauthorizedException {
208     HttpPost post = new HttpPost(seriesID + "/accesscontrol");
209     try {
210       List<BasicNameValuePair> params = new ArrayList<>();
211       params.add(new BasicNameValuePair("seriesID", seriesID));
212       params.add(new BasicNameValuePair("acl", AccessControlParser.toXml(accessControl)));
213       params.add(new BasicNameValuePair("overrideEpisodeAcl", Boolean.toString(overrideEpisodeAcl)));
214       post.setEntity(new UrlEncodedFormEntity(params,  StandardCharsets.UTF_8));
215     } catch (Exception e) {
216       throw new SeriesException("Unable to assemble a remote series request for updating an ACL " + accessControl, e);
217     }
218 
219     HttpResponse response = getResponse(post, SC_NO_CONTENT, SC_CREATED, SC_NOT_FOUND, SC_UNAUTHORIZED);
220     try {
221       if (response != null) {
222         int status = response.getStatusLine().getStatusCode();
223         if (SC_NOT_FOUND == status) {
224           throw new NotFoundException("Series not found: " + seriesID);
225         } else if (SC_NO_CONTENT == status) {
226           logger.info("Successfully updated ACL of {} to the series service", seriesID);
227           return true;
228         } else if (SC_UNAUTHORIZED == status) {
229           throw new UnauthorizedException("Not authorized to update series ACL of " + seriesID);
230         } else if (SC_CREATED == status) {
231           logger.info("Successfully created ACL of {} to the series service", seriesID);
232           return false;
233         }
234       }
235     } finally {
236       closeConnection(response);
237     }
238     throw new SeriesException("Unable to update series ACL " + accessControl + " using the remote series services");
239   }
240 
241   @Override
242   public void deleteSeries(String seriesID) throws SeriesException, NotFoundException, UnauthorizedException {
243     HttpDelete del = new HttpDelete(seriesID);
244     HttpResponse response = getResponse(del, SC_OK, SC_NOT_FOUND, SC_UNAUTHORIZED);
245     try {
246       if (response != null) {
247         int statusCode = response.getStatusLine().getStatusCode();
248         if (SC_NOT_FOUND == statusCode) {
249           throw new NotFoundException("Series not found: " + seriesID);
250         } else if (SC_UNAUTHORIZED == statusCode) {
251           throw new UnauthorizedException("Not authorized to delete series " + seriesID);
252         } else if (SC_OK == statusCode) {
253           logger.info("Successfully deleted {} from the remote series index", seriesID);
254           return;
255         }
256       }
257     } finally {
258       closeConnection(response);
259     }
260     throw new SeriesException("Unable to remove " + seriesID + " from a remote series index");
261   }
262 
263   @GET
264   @Produces(MediaType.APPLICATION_JSON)
265   @Path("{seriesID:.+}.json")
266   public Response getSeriesJSON(@PathParam("seriesID") String seriesID) throws UnauthorizedException {
267     logger.debug("Series Lookup: {}", seriesID);
268     try {
269       DublinCoreCatalog dc = getSeries(seriesID);
270       return Response.ok(dc.toJson()).build();
271     } catch (NotFoundException e) {
272       return Response.status(NOT_FOUND).build();
273     } catch (UnauthorizedException e) {
274       throw e;
275     } catch (Exception e) {
276       logger.error("Could not retrieve series: {}", e.getMessage());
277       throw new WebApplicationException(INTERNAL_SERVER_ERROR);
278     }
279   }
280 
281   @GET
282   @Produces(MediaType.APPLICATION_JSON)
283   @Path("/{seriesID:.+}/acl.json")
284   public Response getSeriesAccessControlListJson(@PathParam("seriesID") String seriesID) {
285     logger.debug("Series ACL lookup: {}", seriesID);
286     try {
287       AccessControlList acl = getSeriesAccessControl(seriesID);
288       return Response.ok(acl).build();
289     } catch (NotFoundException e) {
290       return Response.status(NOT_FOUND).build();
291     } catch (SeriesException e) {
292       logger.error("Could not retrieve series ACL: {}", e.getMessage());
293       throw new WebApplicationException(INTERNAL_SERVER_ERROR);
294     }
295   }
296 
297   @Override
298   public DublinCoreCatalog getSeries(String seriesID) throws SeriesException, NotFoundException, UnauthorizedException {
299     HttpGet get = new HttpGet(seriesID + ".xml");
300     HttpResponse response = getResponse(get, SC_OK, SC_NOT_FOUND, SC_UNAUTHORIZED);
301     try {
302       if (response != null) {
303         if (SC_NOT_FOUND == response.getStatusLine().getStatusCode()) {
304           throw new NotFoundException("Series " + seriesID + " not found in remote series index!");
305         } else if (SC_UNAUTHORIZED == response.getStatusLine().getStatusCode()) {
306           throw new UnauthorizedException("Not authorized to get series " + seriesID);
307         } else {
308           DublinCoreCatalog dublinCoreCatalog = DublinCores.read(response.getEntity().getContent());
309           logger.debug("Successfully received series {} from the remote series index", seriesID);
310           return dublinCoreCatalog;
311         }
312       }
313     } catch (UnauthorizedException e) {
314       throw e;
315     } catch (NotFoundException e) {
316       throw e;
317     } catch (Exception e) {
318       throw new SeriesException("Unable to parse series from remote series index: " + e);
319     } finally {
320       closeConnection(response);
321     }
322     throw new SeriesException("Unable to get series from remote series index");
323   }
324 
325   @Override
326   public List<Series> getAllForAdministrativeRead(Date from, Optional<Date> to, int limit)
327           throws SeriesException, UnauthorizedException {
328     // Assemble URL
329     StringBuilder url = new StringBuilder();
330     url.append("/allInRangeAdministrative.json?");
331 
332     List<NameValuePair> queryParams = new ArrayList<>();
333     queryParams.add(new BasicNameValuePair("from", Long.toString(from.getTime())));
334     queryParams.add(new BasicNameValuePair("limit", Integer.toString(limit)));
335     if (to.isPresent()) {
336       queryParams.add(new BasicNameValuePair("to", Long.toString(to.get().getTime())));
337     }
338     url.append(URLEncodedUtils.format(queryParams, StandardCharsets.UTF_8));
339     HttpGet get = new HttpGet(url.toString());
340 
341     // Send HTTP request
342     HttpResponse response = getResponse(get, SC_OK, SC_BAD_REQUEST, SC_UNAUTHORIZED);
343     try {
344       if (response == null) {
345         throw new SeriesException("Unable to get series from remote series index");
346       }
347 
348       if (response.getStatusLine().getStatusCode() == SC_BAD_REQUEST) {
349         throw new SeriesException("internal server error when fetching /allInRangeAdministrative.json");
350       } else if (response.getStatusLine().getStatusCode() == SC_UNAUTHORIZED) {
351         throw new UnauthorizedException("got UNAUTHORIZED when fetching /allInRangeAdministrative.json");
352       } else {
353         // Retrieve and deserialize data
354         Reader reader = new InputStreamReader(response.getEntity().getContent(), "UTF-8");
355         return gson.fromJson(reader, seriesListType);
356       }
357     } catch (IOException e) {
358       throw new SeriesException("failed to reader response body of /allInRangeAdministrative.json", e);
359     } finally {
360       closeConnection(response);
361     }
362   }
363 
364   @Override
365   public AccessControlList getSeriesAccessControl(String seriesID) throws NotFoundException, SeriesException {
366     HttpGet get = new HttpGet(seriesID + "/acl.xml");
367     HttpResponse response = getResponse(get, SC_OK, SC_NOT_FOUND);
368     try {
369       if (response != null) {
370         if (SC_NOT_FOUND == response.getStatusLine().getStatusCode()) {
371           throw new NotFoundException("Series ACL " + seriesID + " not found on remote series index!");
372         } else {
373           AccessControlList acl = AccessControlParser.parseAcl(response.getEntity().getContent());
374           logger.info("Successfully get series ACL {} from the remote series index", seriesID);
375           return acl;
376         }
377       }
378     } catch (NotFoundException e) {
379       throw e;
380     } catch (Exception e) {
381       throw new SeriesException("Unable to parse series ACL form remote series index: " + e);
382     } finally {
383       closeConnection(response);
384     }
385     throw new SeriesException("Unable to get series ACL from remote series index");
386   }
387 
388   @Override
389   public int getSeriesCount() throws SeriesException {
390     HttpGet get = new HttpGet("/count");
391     HttpResponse response = getResponse(get);
392     try {
393       if (response != null) {
394         int count = Integer.parseInt(IOUtils.toString(response.getEntity().getContent()));
395         logger.info("Successfully get series dublin core catalog list from the remote series index");
396         return count;
397       }
398     } catch (Exception e) {
399       throw new SeriesException("Unable to count series from remote series index: " + e);
400     } finally {
401       closeConnection(response);
402     }
403     throw new SeriesException("Unable to count series from remote series index");
404   }
405 
406   @Override
407   public Map<String, String> getSeriesProperties(String seriesID)
408           throws SeriesException, NotFoundException, UnauthorizedException {
409     HttpGet get = new HttpGet(seriesID + "/properties.json");
410     HttpResponse response = getResponse(get, SC_OK, SC_NOT_FOUND, SC_UNAUTHORIZED);
411     JSONParser parser = new JSONParser();
412     try {
413       if (response != null) {
414         if (SC_NOT_FOUND == response.getStatusLine().getStatusCode()) {
415           throw new NotFoundException("Series " + seriesID + " not found in remote series index!");
416         } else if (SC_UNAUTHORIZED == response.getStatusLine().getStatusCode()) {
417           throw new UnauthorizedException("Not authorized to get series " + seriesID);
418         } else {
419           logger.debug("Successfully received series {} properties from the remote series index", seriesID);
420           StringWriter writer = new StringWriter();
421           IOUtils.copy(response.getEntity().getContent(), writer, StandardCharsets.UTF_8);
422           JSONArray jsonProperties = (JSONArray) parser.parse(writer.toString());
423           Map<String, String> properties = new TreeMap<>();
424           for (int i = 0; i < jsonProperties.length(); i++) {
425             JSONObject property = (JSONObject) jsonProperties.get(i);
426             JSONArray names = property.names();
427             for (int j = 0; j < names.length(); j++) {
428               properties.put(names.get(j).toString(), property.get(names.get(j).toString()).toString());
429             }
430           }
431           return properties;
432         }
433       }
434     } catch (UnauthorizedException e) {
435       throw e;
436     } catch (NotFoundException e) {
437       throw e;
438     } catch (Exception e) {
439       throw new SeriesException("Unable to parse series properties from remote series index: " + e);
440     } finally {
441       closeConnection(response);
442     }
443     throw new SeriesException("Unable to get series from remote series index");
444   }
445 
446   @Override
447   public String getSeriesProperty(String seriesID, String propertyName)
448           throws SeriesException, NotFoundException, UnauthorizedException {
449     HttpGet get = new HttpGet(seriesID + "/property/" + propertyName + ".json");
450     HttpResponse response = getResponse(get, SC_OK, SC_NOT_FOUND, SC_UNAUTHORIZED);
451     try {
452       if (response != null) {
453         if (SC_NOT_FOUND == response.getStatusLine().getStatusCode()) {
454           throw new NotFoundException("Series " + seriesID + " not found in remote series index!");
455         } else if (SC_UNAUTHORIZED == response.getStatusLine().getStatusCode()) {
456           throw new UnauthorizedException("Not authorized to get series " + seriesID);
457         } else {
458           logger.debug("Successfully received series {} property {} from the remote series index", seriesID,
459                   propertyName);
460           StringWriter writer = new StringWriter();
461           IOUtils.copy(response.getEntity().getContent(), writer, StandardCharsets.UTF_8);
462           return writer.toString();
463         }
464       }
465     } catch (UnauthorizedException e) {
466       throw e;
467     } catch (NotFoundException e) {
468       throw e;
469     } catch (Exception e) {
470       throw new SeriesException("Unable to parse series from remote series index: " + e);
471     } finally {
472       closeConnection(response);
473     }
474     throw new SeriesException("Unable to get series from remote series index");
475   }
476 
477   @Override
478   public void updateSeriesProperty(String seriesID, String propertyName, String propertyValue)
479           throws SeriesException, NotFoundException, UnauthorizedException {
480     HttpPost post = new HttpPost("/" + seriesID + "/property");
481     try {
482       List<BasicNameValuePair> params = new ArrayList<>();
483       params.add(new BasicNameValuePair("name", propertyName));
484       params.add(new BasicNameValuePair("value", propertyValue));
485       post.setEntity(new UrlEncodedFormEntity(params,  StandardCharsets.UTF_8));
486     } catch (Exception e) {
487       throw new SeriesException("Unable to assemble a remote series request for updating series " + seriesID
488               + " series property " + propertyName + ":" + propertyValue, e);
489     }
490 
491     HttpResponse response = getResponse(post, SC_NO_CONTENT, SC_CREATED, SC_UNAUTHORIZED);
492     try {
493       if (response != null) {
494         int statusCode = response.getStatusLine().getStatusCode();
495         if (SC_NO_CONTENT == statusCode) {
496           logger.info("Successfully updated series {} with property name {} and value {} in the series service",
497                   seriesID, propertyName, propertyValue);
498           return;
499         } else if (SC_UNAUTHORIZED == statusCode) {
500           throw new UnauthorizedException("Not authorized to update series " + seriesID);
501         }
502       }
503     } catch (UnauthorizedException e) {
504       throw e;
505     } catch (Exception e) {
506       throw new SeriesException("Unable to update series " + seriesID + " with property " + propertyName + ":"
507               + propertyValue + " using the remote series services: ", e);
508     } finally {
509       closeConnection(response);
510     }
511     throw new SeriesException("Unable to update series " + seriesID + " using the remote series services");
512   }
513 
514   @Override
515   public void deleteSeriesProperty(String seriesID, String propertyName)
516           throws SeriesException, NotFoundException, UnauthorizedException {
517     HttpDelete del = new HttpDelete("/" + seriesID + "/property/" + propertyName);
518     HttpResponse response = getResponse(del, SC_OK, SC_NOT_FOUND, SC_UNAUTHORIZED);
519     try {
520       if (response != null) {
521         int statusCode = response.getStatusLine().getStatusCode();
522         if (SC_NOT_FOUND == statusCode) {
523           throw new NotFoundException("Series not found: " + seriesID);
524         } else if (SC_UNAUTHORIZED == statusCode) {
525           throw new UnauthorizedException("Not authorized to delete series " + seriesID);
526         } else if (SC_OK == statusCode) {
527           logger.info("Successfully deleted {} from the remote series index", seriesID);
528           return;
529         }
530       }
531     } finally {
532       closeConnection(response);
533     }
534     throw new SeriesException("Unable to remove " + seriesID + " from a remote series index");
535   }
536 
537   @Override
538   public boolean updateExtendedMetadata(String seriesId, String type, DublinCoreCatalog dc) throws SeriesException {
539     HttpPut put = new HttpPut("/" + seriesId + "/extendedMetadata/" + type);
540     try {
541       List<BasicNameValuePair> params = new ArrayList<>();
542       params.add(new BasicNameValuePair("dc", dc.toXmlString()));
543       put.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8));
544     } catch (Exception e) {
545       throw new SeriesException("Unable to assemble a remote series request for updating extended metadata of series "
546               + seriesId, e);
547     }
548 
549     HttpResponse response = getResponse(put, SC_NO_CONTENT, SC_CREATED, SC_INTERNAL_SERVER_ERROR);
550     try {
551       if (response == null) {
552         throw new SeriesException(format("Error while updating extended metadata catalog of type '%s' for series '%s'",
553                 type, seriesId));
554       } else {
555         final int statusCode = response.getStatusLine().getStatusCode();
556         switch (statusCode) {
557           case SC_NO_CONTENT:
558           case SC_CREATED:
559             return true;
560           case SC_INTERNAL_SERVER_ERROR:
561             throw new SeriesException(
562                     format("Error while updating extended metadata catalog of type '%s' for series '%s'", type,
563                             seriesId));
564           default:
565             throw new SeriesException(format("Unexpected status code", statusCode));
566         }
567       }
568     } finally {
569       closeConnection(response);
570     }
571   }
572 
573   @Override
574   public Opt<Map<String, byte[]>> getSeriesElements(String seriesID) throws SeriesException {
575     HttpGet get = new HttpGet("/" + seriesID + "/elements.json");
576     HttpResponse response = getResponse(get, SC_OK, SC_NOT_FOUND, SC_INTERNAL_SERVER_ERROR);
577     JSONParser parser = new JSONParser();
578 
579     try {
580       if (response == null) {
581         throw new SeriesException(format("Error while retrieving elements from series '%s'", seriesID));
582       } else {
583         final int statusCode = response.getStatusLine().getStatusCode();
584         switch (statusCode) {
585           case SC_OK:
586             JSONArray elementArray = (JSONArray) parser.parse(IOUtils.toString(response.getEntity().getContent()));
587             Map<String, byte[]> elements = new HashMap<>();
588             for (int i = 0; i < elementArray.length(); i++) {
589               final String type = elementArray.getString(i);
590               Opt<byte[]> optData = getSeriesElementData(seriesID, type);
591               if (optData.isSome()) {
592                 elements.put(type, optData.get());
593               } else {
594                 throw new SeriesException(format("Tried to load non-existing element of type '%s'", type));
595               }
596             }
597             return Opt.some(elements);
598           case SC_NOT_FOUND:
599             return Opt.none();
600           case SC_INTERNAL_SERVER_ERROR:
601             throw new SeriesException(format("Error while retrieving elements from series '%s'", seriesID));
602           default:
603             throw new SeriesException(format("Unexpected status code", statusCode));
604         }
605       }
606     } catch (Exception e) {
607       logger.warn("Error while retrieving elements from remote service:", e);
608       throw new SeriesException(e);
609     } finally {
610       closeConnection(response);
611     }
612   }
613 
614   @Override
615   public Opt<byte[]> getSeriesElementData(String seriesID, String type) throws SeriesException {
616     HttpGet get = new HttpGet("/" + seriesID + "/elements/" + type);
617     HttpResponse response = getResponse(get, SC_OK, SC_NOT_FOUND, SC_INTERNAL_SERVER_ERROR);
618 
619     try {
620       if (response == null) {
621         throw new SeriesException(
622                 format("Error while retrieving element of type '%s' from series '%s'", type, seriesID));
623       } else {
624         final int statusCode = response.getStatusLine().getStatusCode();
625         switch (statusCode) {
626           case SC_OK:
627             return Opt.some(IOUtils.toByteArray(response.getEntity().getContent()));
628           case SC_NOT_FOUND:
629             return Opt.none();
630           case SC_INTERNAL_SERVER_ERROR:
631             throw new SeriesException(
632                     format("Error while retrieving element of type '%s' from series '%s'", type, seriesID));
633           default:
634             throw new SeriesException(format("Unexpected status code", statusCode));
635         }
636       }
637     } catch (Exception e) {
638       logger.warn("Error while retrieving element from remote service:", e);
639       throw new SeriesException(e);
640     } finally {
641       closeConnection(response);
642     }
643   }
644 
645   @Override
646   public boolean updateSeriesElement(String seriesID, String type, byte[] data) throws SeriesException {
647     HttpPut put = new HttpPut("/" + seriesID + "/elements/" + type);
648     put.setEntity(new ByteArrayEntity(data, ContentType.DEFAULT_BINARY));
649 
650     HttpResponse response = getResponse(put, SC_CREATED, SC_NO_CONTENT, SC_INTERNAL_SERVER_ERROR);
651     try {
652       if (response == null) {
653         throw new SeriesException(format("Error while updating element of type '%s' in series '%s'", type, seriesID));
654       } else {
655         final int statusCode = response.getStatusLine().getStatusCode();
656         switch (statusCode) {
657           case SC_NO_CONTENT:
658           case SC_CREATED:
659             return true;
660           case SC_INTERNAL_SERVER_ERROR:
661             throw new SeriesException(
662                     format("Error while updating element of type '%s' in series '%s'", type, seriesID));
663           default:
664             throw new SeriesException(format("Unexpected status code", statusCode));
665         }
666       }
667     } finally {
668       closeConnection(response);
669     }
670   }
671 
672   @Override
673   public boolean deleteSeriesElement(String seriesID, String type) throws SeriesException {
674     if (isBlank(seriesID)) {
675       throw new IllegalArgumentException("Series ID must not be blank");
676     }
677     if (isBlank(type)) {
678       throw new IllegalArgumentException("Element type must not be blank");
679     }
680 
681     HttpDelete del = new HttpDelete("/" + seriesID + "/elements/" + type);
682     HttpResponse response = getResponse(del, SC_NO_CONTENT, SC_NOT_FOUND, SC_INTERNAL_SERVER_ERROR);
683     try {
684       if (response == null) {
685         throw new SeriesException("Unable to remove " + seriesID + " from a remote series index");
686       } else {
687         final int statusCode = response.getStatusLine().getStatusCode();
688         switch (statusCode) {
689           case SC_NO_CONTENT:
690             return true;
691           case SC_NOT_FOUND:
692             return false;
693           case SC_INTERNAL_SERVER_ERROR:
694             throw new SeriesException(
695                     format("Error while deleting element of type '%s' from series '%s'", type, seriesID));
696           default:
697             throw new SeriesException(format("Unexpected status code", statusCode));
698         }
699       }
700     } finally {
701       closeConnection(response);
702     }
703   }
704 }