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