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.adminui.endpoint;
23  
24  import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
25  import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
26  import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
27  import static javax.servlet.http.HttpServletResponse.SC_OK;
28  import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
29  import static org.apache.commons.lang3.StringUtils.isNotBlank;
30  import static org.apache.commons.lang3.StringUtils.trimToNull;
31  import static org.opencastproject.index.service.util.JSONUtils.safeString;
32  import static org.opencastproject.index.service.util.RestUtils.notFound;
33  import static org.opencastproject.index.service.util.RestUtils.okJson;
34  import static org.opencastproject.index.service.util.RestUtils.okJsonList;
35  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
36  
37  import org.opencastproject.elasticsearch.api.SearchIndexException;
38  import org.opencastproject.elasticsearch.api.SearchResult;
39  import org.opencastproject.elasticsearch.api.SearchResultItem;
40  import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
41  import org.opencastproject.elasticsearch.index.objects.series.Series;
42  import org.opencastproject.elasticsearch.index.objects.series.SeriesSearchQuery;
43  import org.opencastproject.index.service.resources.list.query.ThemesListQuery;
44  import org.opencastproject.index.service.util.RestUtils;
45  import org.opencastproject.security.api.SecurityService;
46  import org.opencastproject.security.api.UnauthorizedException;
47  import org.opencastproject.security.api.User;
48  import org.opencastproject.series.api.SeriesException;
49  import org.opencastproject.series.api.SeriesService;
50  import org.opencastproject.staticfiles.api.StaticFileService;
51  import org.opencastproject.staticfiles.endpoint.StaticFileRestService;
52  import org.opencastproject.themes.Theme;
53  import org.opencastproject.themes.ThemesServiceDatabase;
54  import org.opencastproject.themes.persistence.ThemesServiceDatabaseException;
55  import org.opencastproject.util.DateTimeSupport;
56  import org.opencastproject.util.EqualsUtil;
57  import org.opencastproject.util.NotFoundException;
58  import org.opencastproject.util.RestUtil;
59  import org.opencastproject.util.RestUtil.R;
60  import org.opencastproject.util.doc.rest.RestParameter;
61  import org.opencastproject.util.doc.rest.RestParameter.Type;
62  import org.opencastproject.util.doc.rest.RestQuery;
63  import org.opencastproject.util.doc.rest.RestResponse;
64  import org.opencastproject.util.doc.rest.RestService;
65  import org.opencastproject.util.requests.SortCriterion;
66  
67  import com.google.gson.JsonArray;
68  import com.google.gson.JsonObject;
69  
70  import org.apache.commons.lang3.BooleanUtils;
71  import org.apache.commons.lang3.StringUtils;
72  import org.osgi.framework.BundleContext;
73  import org.osgi.service.component.annotations.Activate;
74  import org.osgi.service.component.annotations.Component;
75  import org.osgi.service.component.annotations.Reference;
76  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
77  import org.slf4j.Logger;
78  import org.slf4j.LoggerFactory;
79  
80  import java.io.IOException;
81  import java.util.ArrayList;
82  import java.util.Date;
83  import java.util.List;
84  import java.util.Map;
85  import java.util.Optional;
86  
87  import javax.servlet.http.HttpServletResponse;
88  import javax.ws.rs.DELETE;
89  import javax.ws.rs.FormParam;
90  import javax.ws.rs.GET;
91  import javax.ws.rs.POST;
92  import javax.ws.rs.PUT;
93  import javax.ws.rs.Path;
94  import javax.ws.rs.PathParam;
95  import javax.ws.rs.Produces;
96  import javax.ws.rs.QueryParam;
97  import javax.ws.rs.WebApplicationException;
98  import javax.ws.rs.core.MediaType;
99  import javax.ws.rs.core.Response;
100 import javax.ws.rs.core.Response.Status;
101 
102 @Path("/admin-ng/themes")
103 @RestService(
104     name = "themes",
105     title = "Themes facade service",
106     abstractText = "Provides operations for the themes",
107     notes = { "This service offers the default themes CRUD Operations for the admin UI.",
108               "<strong>Important:</strong> "
109                 + "<em>This service is for exclusive use by the module admin-ui. Its API might change "
110                 + "anytime without prior notice. Any dependencies other than the admin UI will be strictly ignored. "
111                 + "DO NOT use this for integration of third-party applications.<em>"})
112 @Component(
113     immediate = true,
114     service = ThemesEndpoint.class,
115     property = {
116         "service.description=Admin UI - Themes Endpoint",
117         "opencast.service.type=org.opencastproject.adminui.ThemesEndpoint",
118         "opencast.service.path=/admin-ng/themes",
119     }
120 )
121 @JaxrsResource
122 public class ThemesEndpoint {
123 
124   /** The logging facility */
125   private static final Logger logger = LoggerFactory.getLogger(ThemesEndpoint.class);
126 
127   /** The themes service database */
128   private ThemesServiceDatabase themesServiceDatabase;
129 
130   /** The security service */
131   private SecurityService securityService;
132 
133   /** The admin UI search index */
134   private ElasticsearchIndex searchIndex;
135 
136   /** The series service */
137   private SeriesService seriesService;
138 
139   /** The static file service */
140   private StaticFileService staticFileService;
141 
142   /** The static file REST service */
143   private StaticFileRestService staticFileRestService;
144 
145   /** OSGi callback for the themes service database. */
146   @Reference
147   public void setThemesServiceDatabase(ThemesServiceDatabase themesServiceDatabase) {
148     this.themesServiceDatabase = themesServiceDatabase;
149   }
150 
151   /** OSGi callback for the security service. */
152   @Reference
153   public void setSecurityService(SecurityService securityService) {
154     this.securityService = securityService;
155   }
156 
157   /** OSGi DI. */
158   @Reference
159   public void setIndex(ElasticsearchIndex index) {
160     this.searchIndex = index;
161   }
162 
163   /** OSGi DI. */
164   @Reference
165   public void setSeriesService(SeriesService seriesService) {
166     this.seriesService = seriesService;
167   }
168 
169   /** OSGi DI. */
170   @Reference
171   public void setStaticFileService(StaticFileService staticFileService) {
172     this.staticFileService = staticFileService;
173   }
174 
175   /** OSGi DI. */
176   @Reference
177   public void setStaticFileRestService(StaticFileRestService staticFileRestService) {
178     this.staticFileRestService = staticFileRestService;
179   }
180 
181   @Activate
182   public void activate(BundleContext bundleContext) {
183     logger.info("Activate themes endpoint");
184   }
185 
186   @GET
187   @Produces({ MediaType.APPLICATION_JSON })
188   @Path("themes.json")
189   @RestQuery(
190       name = "getThemes",
191       description = "Return all of the known themes on the system",
192       restParameters = {
193           @RestParameter(name = "filter", isRequired = false, description = "The filter used for the query. They "
194               + "should be formated like that: 'filter1:value1,filter2:value2'", type = STRING),
195           @RestParameter(defaultValue = "0", description = "The maximum number of items to return per page.",
196               isRequired = false, name = "limit", type = RestParameter.Type.INTEGER),
197           @RestParameter(defaultValue = "0", description = "The page number.", isRequired = false, name = "offset",
198               type = RestParameter.Type.INTEGER),
199           @RestParameter(name = "sort", isRequired = false, description = "The sort order. May include any of the "
200               + "following: NAME, CREATOR.  Add '_DESC' to reverse the sort order (e.g. CREATOR_DESC).", type = STRING)
201       },
202       responses = {
203           @RestResponse(description = "A JSON representation of the themes", responseCode = HttpServletResponse.SC_OK)
204       },
205       returnDescription = "")
206   public Response getThemes(@QueryParam("filter") String filter, @QueryParam("limit") int limit,
207       @QueryParam("offset") int offset, @QueryParam("sort") String sort) {
208     Optional<Integer> optLimit = Optional.ofNullable(limit);
209     Optional<Integer> optOffset = Optional.ofNullable(offset);
210     Optional<String> optSort = Optional.ofNullable(trimToNull(sort));
211 
212     Map<String, String> filters = RestUtils.parseFilter(filter);
213 
214     ArrayList<SortCriterion> sortCriteria;
215     if (optSort.isPresent()) {
216       sortCriteria = RestUtils.parseSortQueryParameter(optSort.get());
217     } else {
218       sortCriteria = new ArrayList<>();
219     }
220     Optional<String> optCreatorFilter = Optional.ofNullable(filters.get(ThemesListQuery.FILTER_CREATOR_NAME));
221     Optional<String> optTextFilter = Optional.ofNullable(filters.get(ThemesListQuery.FILTER_TEXT_NAME));
222 
223     List<Theme> results = null;
224     int total;
225     try {
226       results = themesServiceDatabase.findThemes(
227           optLimit,
228           optOffset,
229           sortCriteria,
230           optCreatorFilter,
231           optTextFilter
232       );
233       total = themesServiceDatabase.countThemes();
234     } catch (Exception e) {
235       logger.error("Could not get themes from the database:", e);
236       return RestUtil.R.serverError();
237     }
238 
239 
240     List<JsonObject> themesJSON = new ArrayList<>();
241     for (Theme theme : results) {
242       themesJSON.add(themeToJSON(theme, false));
243     }
244 
245     return okJsonList(
246         themesJSON,
247         Optional.ofNullable(offset).orElse(0),
248         Optional.ofNullable(limit).orElse(0),
249         total
250     );
251   }
252 
253   @GET
254   @Path("{themeId}.json")
255   @Produces(MediaType.APPLICATION_JSON)
256   @RestQuery(
257       name = "getTheme",
258       description = "Returns the theme by the given id as JSON",
259       returnDescription = "The theme as JSON",
260       pathParameters = {
261           @RestParameter(name = "themeId", description = "The theme id", isRequired = true,
262               type = RestParameter.Type.INTEGER)
263       },
264       responses = {
265           @RestResponse(description = "Returns the theme as JSON", responseCode = HttpServletResponse.SC_OK),
266           @RestResponse(description = "No theme with this identifier was found.",
267               responseCode = HttpServletResponse.SC_NOT_FOUND)
268       })
269   public Response getThemeResponse(@PathParam("themeId") long id) {
270     try {
271       Theme theme = themesServiceDatabase.getTheme(id);
272 
273       return okJson(themeToJSON(theme, true));
274     } catch (ThemesServiceDatabaseException e) {
275       return RestUtil.R.serverError();
276     } catch (NotFoundException e) {
277       return notFound("Cannot find a theme with id '%s'", id);
278     }
279   }
280 
281   @GET
282   @Path("{themeId}/usage.json")
283   @Produces(MediaType.APPLICATION_JSON)
284   @RestQuery(
285       name = "getThemeUsage",
286       description = "Returns the theme usage by the given id as JSON",
287       returnDescription = "The theme usage as JSON",
288       pathParameters = {
289           @RestParameter(name = "themeId", description = "The theme id", isRequired = true,
290               type = RestParameter.Type.INTEGER)
291       },
292       responses = {
293           @RestResponse(description = "Returns the theme usage as JSON", responseCode = HttpServletResponse.SC_OK),
294           @RestResponse(description = "Theme with the given id does not exist",
295               responseCode = HttpServletResponse.SC_NOT_FOUND)
296       })
297   public Response getThemeUsage(@PathParam("themeId") long themeId) {
298     try {
299       themesServiceDatabase.getTheme(themeId);
300 
301       SeriesSearchQuery query = new SeriesSearchQuery(securityService.getOrganization().getId(),
302               securityService.getUser()).withTheme(themeId);
303       SearchResult<Series> results = null;
304       try {
305         results = searchIndex.getByQuery(query);
306       } catch (SearchIndexException e) {
307         logger.error("The admin UI Search Index was not able to get the series with theme '{}':", themeId,
308                 e);
309         return RestUtil.R.serverError();
310       }
311       JsonArray seriesValues = new JsonArray();
312       for (SearchResultItem<Series> item : results.getItems()) {
313         Series series = item.getSource();
314         JsonObject seriesJson = new JsonObject();
315         seriesJson.addProperty("id", series.getIdentifier());
316         seriesJson.addProperty("title", series.getTitle());
317         seriesValues.add(seriesJson);
318       }
319       JsonObject responseJson = new JsonObject();
320       responseJson.add("series", seriesValues);
321 
322       return okJson(responseJson);
323     } catch (ThemesServiceDatabaseException e) {
324       return RestUtil.R.serverError();
325     } catch (NotFoundException e) {
326       return notFound("Cannot find a theme with id {}", themeId);
327     }
328   }
329 
330   @POST
331   @Path("")
332   @RestQuery(
333       name = "createTheme",
334       description = "Add a theme",
335       returnDescription = "Return the created theme",
336       restParameters = {
337           @RestParameter(name = "default", description = "Whether the theme is default",
338               isRequired = true, type = Type.BOOLEAN),
339           @RestParameter(name = "name", description = "The theme name",
340               isRequired = true, type = Type.STRING),
341           @RestParameter(name = "description", description = "The theme description",
342               isRequired = false, type = Type.TEXT),
343           @RestParameter(name = "bumperActive", description = "Whether the theme bumper is active",
344               isRequired = false, type = Type.BOOLEAN),
345           @RestParameter(name = "trailerActive", description = "Whether the theme trailer is active",
346               isRequired = false, type = Type.BOOLEAN),
347           @RestParameter(name = "titleSlideActive", description = "Whether the theme title slide is active",
348               isRequired = false, type = Type.BOOLEAN),
349           @RestParameter(name = "licenseSlideActive", description = "Whether the theme license slide is active",
350               isRequired = false, type = Type.BOOLEAN),
351           @RestParameter(name = "watermarkActive", description = "Whether the theme watermark is active",
352               isRequired = false, type = Type.BOOLEAN),
353           @RestParameter(name = "bumperFile", description = "The theme bumper file",
354               isRequired = false, type = Type.STRING),
355           @RestParameter(name = "trailerFile", description = "The theme trailer file",
356               isRequired = false, type = Type.STRING),
357           @RestParameter(name = "watermarkFile", description = "The theme watermark file",
358               isRequired = false, type = Type.STRING),
359           @RestParameter(name = "titleSlideBackground", description = "The theme title slide background file",
360               isRequired = false, type = Type.STRING),
361           @RestParameter(name = "licenseSlideBackground", description = "The theme license slide background file",
362               isRequired = false, type = Type.STRING),
363           @RestParameter(name = "titleSlideMetadata", description = "The theme title slide metadata",
364               isRequired = false, type = Type.STRING),
365           @RestParameter(name = "licenseSlideDescription", description = "The theme license slide description",
366               isRequired = false, type = Type.STRING),
367           @RestParameter(name = "watermarkPosition", description = "The theme watermark position",
368               isRequired = false, type = Type.STRING),
369       },
370       responses = {
371           @RestResponse(responseCode = SC_OK, description = "Theme created"),
372           @RestResponse(responseCode = SC_BAD_REQUEST, description = "The theme references a non-existing file")
373       })
374   public Response createTheme(@FormParam("default") boolean isDefault, @FormParam("name") String name,
375       @FormParam("description") String description, @FormParam("bumperActive") Boolean bumperActive,
376       @FormParam("trailerActive") Boolean trailerActive, @FormParam("titleSlideActive") Boolean titleSlideActive,
377       @FormParam("licenseSlideActive") Boolean licenseSlideActive,
378       @FormParam("watermarkActive") Boolean watermarkActive, @FormParam("bumperFile") String bumperFile,
379       @FormParam("trailerFile") String trailerFile, @FormParam("watermarkFile") String watermarkFile,
380       @FormParam("titleSlideBackground") String titleSlideBackground,
381       @FormParam("licenseSlideBackground") String licenseSlideBackground,
382       @FormParam("titleSlideMetadata") String titleSlideMetadata,
383       @FormParam("licenseSlideDescription") String licenseSlideDescription,
384       @FormParam("watermarkPosition") String watermarkPosition) {
385     User creator = securityService.getUser();
386 
387     Theme theme = new Theme(Optional.<Long> empty(), new Date(), isDefault, creator, name,
388             StringUtils.trimToNull(description), BooleanUtils.toBoolean(bumperActive),
389             StringUtils.trimToNull(bumperFile), BooleanUtils.toBoolean(trailerActive),
390             StringUtils.trimToNull(trailerFile), BooleanUtils.toBoolean(titleSlideActive),
391             StringUtils.trimToNull(titleSlideMetadata), StringUtils.trimToNull(titleSlideBackground),
392             BooleanUtils.toBoolean(licenseSlideActive), StringUtils.trimToNull(licenseSlideBackground),
393             StringUtils.trimToNull(licenseSlideDescription), BooleanUtils.toBoolean(watermarkActive),
394             StringUtils.trimToNull(watermarkFile), StringUtils.trimToNull(watermarkPosition));
395 
396     try {
397       persistReferencedFiles(theme);
398     } catch (NotFoundException e) {
399       logger.warn("A file that is referenced in theme '{}' was not found: {}", theme, e.getMessage());
400       return R.badRequest("Referenced non-existing file");
401     } catch (IOException e) {
402       logger.warn("Error while persisting file: {}", e.getMessage());
403       return R.serverError();
404     }
405 
406     try {
407       Theme createdTheme = themesServiceDatabase.updateTheme(theme);
408       return RestUtils.okJson(themeToJSON(createdTheme,false));
409     } catch (ThemesServiceDatabaseException e) {
410       logger.error("Unable to create a theme");
411       return RestUtil.R.serverError();
412     }
413   }
414 
415   @PUT
416   @Path("{themeId}")
417   @RestQuery(
418       name = "updateTheme",
419       description = "Updates a theme",
420       returnDescription = "Return the updated theme",
421       pathParameters = {
422           @RestParameter(name = "themeId", description = "The theme identifier", isRequired = true, type = Type.INTEGER)
423       },
424       restParameters = {
425           @RestParameter(name = "default", description = "Whether the theme is default",
426               isRequired = false, type = Type.BOOLEAN),
427           @RestParameter(name = "name", description = "The theme name",
428               isRequired = false, type = Type.STRING),
429           @RestParameter(name = "description", description = "The theme description",
430               isRequired = false, type = Type.TEXT),
431           @RestParameter(name = "bumperActive", description = "Whether the theme bumper is active",
432               isRequired = false, type = Type.BOOLEAN),
433           @RestParameter(name = "trailerActive", description = "Whether the theme trailer is active",
434               isRequired = false, type = Type.BOOLEAN),
435           @RestParameter(name = "titleSlideActive", description = "Whether the theme title slide is active",
436               isRequired = false, type = Type.BOOLEAN),
437           @RestParameter(name = "licenseSlideActive", description = "Whether the theme license slide is active",
438               isRequired = false, type = Type.BOOLEAN),
439           @RestParameter(name = "watermarkActive", description = "Whether the theme watermark is active",
440               isRequired = false, type = Type.BOOLEAN),
441           @RestParameter(name = "bumperFile", description = "The theme bumper file",
442               isRequired = false, type = Type.STRING),
443           @RestParameter(name = "trailerFile", description = "The theme trailer file",
444               isRequired = false, type = Type.STRING),
445           @RestParameter(name = "watermarkFile", description = "The theme watermark file",
446               isRequired = false, type = Type.STRING),
447           @RestParameter(name = "titleSlideBackground", description = "The theme title slide background file",
448               isRequired = false, type = Type.STRING),
449           @RestParameter(name = "licenseSlideBackground", description = "The theme license slide background file",
450               isRequired = false, type = Type.STRING),
451           @RestParameter(name = "titleSlideMetadata", description = "The theme title slide metadata",
452               isRequired = false, type = Type.STRING),
453           @RestParameter(name = "licenseSlideDescription", description = "The theme license slide description",
454               isRequired = false, type = Type.STRING),
455           @RestParameter(name = "watermarkPosition", description = "The theme watermark position",
456               isRequired = false, type = Type.STRING),
457       },
458       responses = {
459           @RestResponse(responseCode = SC_OK, description = "Theme updated"),
460           @RestResponse(responseCode = SC_NOT_FOUND, description = "If the theme has not been found."),
461       })
462   public Response updateTheme(@PathParam("themeId") long themeId, @FormParam("default") Boolean isDefault,
463       @FormParam("name") String name, @FormParam("description") String description,
464       @FormParam("bumperActive") Boolean bumperActive, @FormParam("trailerActive") Boolean trailerActive,
465       @FormParam("titleSlideActive") Boolean titleSlideActive,
466       @FormParam("licenseSlideActive") Boolean licenseSlideActive,
467       @FormParam("watermarkActive") Boolean watermarkActive, @FormParam("bumperFile") String bumperFile,
468       @FormParam("trailerFile") String trailerFile, @FormParam("watermarkFile") String watermarkFile,
469       @FormParam("titleSlideBackground") String titleSlideBackground,
470       @FormParam("licenseSlideBackground") String licenseSlideBackground,
471       @FormParam("titleSlideMetadata") String titleSlideMetadata,
472       @FormParam("licenseSlideDescription") String licenseSlideDescription,
473       @FormParam("watermarkPosition") String watermarkPosition) throws NotFoundException {
474     try {
475       Theme origTheme = themesServiceDatabase.getTheme(themeId);
476 
477       if (isDefault == null) {
478         isDefault = origTheme.isDefault();
479       }
480       if (StringUtils.isBlank(name)) {
481         name = origTheme.getName();
482       }
483       if (StringUtils.isEmpty(description)) {
484         description = origTheme.getDescription();
485       }
486       if (bumperActive == null) {
487         bumperActive = origTheme.isBumperActive();
488       }
489       if (StringUtils.isEmpty(bumperFile)) {
490         bumperFile = origTheme.getBumperFile();
491       }
492       if (trailerActive == null) {
493         trailerActive = origTheme.isTrailerActive();
494       }
495       if (StringUtils.isEmpty(trailerFile)) {
496         trailerFile = origTheme.getTrailerFile();
497       }
498       if (titleSlideActive == null) {
499         titleSlideActive = origTheme.isTitleSlideActive();
500       }
501       if (StringUtils.isEmpty(titleSlideMetadata)) {
502         titleSlideMetadata = origTheme.getTitleSlideMetadata();
503       }
504       if (StringUtils.isEmpty(titleSlideBackground)) {
505         titleSlideBackground = origTheme.getTitleSlideBackground();
506       }
507       if (licenseSlideActive == null) {
508         licenseSlideActive = origTheme.isLicenseSlideActive();
509       }
510       if (StringUtils.isEmpty(licenseSlideBackground)) {
511         licenseSlideBackground = origTheme.getLicenseSlideBackground();
512       }
513       if (StringUtils.isEmpty(licenseSlideDescription)) {
514         licenseSlideDescription = origTheme.getLicenseSlideDescription();
515       }
516       if (watermarkActive == null) {
517         watermarkActive = origTheme.isWatermarkActive();
518       }
519       if (StringUtils.isEmpty(watermarkFile)) {
520         watermarkFile = origTheme.getWatermarkFile();
521       }
522       if (StringUtils.isEmpty(watermarkPosition)) {
523         watermarkPosition = origTheme.getWatermarkPosition();
524       }
525 
526       Theme theme = new Theme(origTheme.getId(), origTheme.getCreationDate(), isDefault, origTheme.getCreator(), name,
527           StringUtils.trimToNull(description), BooleanUtils.toBoolean(bumperActive),
528           StringUtils.trimToNull(bumperFile), BooleanUtils.toBoolean(trailerActive),
529           StringUtils.trimToNull(trailerFile), BooleanUtils.toBoolean(titleSlideActive),
530           StringUtils.trimToNull(titleSlideMetadata), StringUtils.trimToNull(titleSlideBackground),
531           BooleanUtils.toBoolean(licenseSlideActive), StringUtils.trimToNull(licenseSlideBackground),
532           StringUtils.trimToNull(licenseSlideDescription), BooleanUtils.toBoolean(watermarkActive),
533           StringUtils.trimToNull(watermarkFile), StringUtils.trimToNull(watermarkPosition));
534 
535       try {
536         updateReferencedFiles(origTheme, theme);
537       } catch (IOException e) {
538         logger.warn("Error while persisting file: {}", e.getMessage());
539         return R.serverError();
540       } catch (NotFoundException e) {
541         logger.warn("A file that is referenced in theme '{}' was not found: {}", theme, e.getMessage());
542         return R.badRequest("Referenced non-existing file");
543       }
544 
545       Theme updatedTheme = themesServiceDatabase.updateTheme(theme);
546       return RestUtils.okJson(themeToJSON(updatedTheme, false));
547     } catch (ThemesServiceDatabaseException e) {
548       logger.error("Unable to update theme {}", themeId, e);
549       return RestUtil.R.serverError();
550     }
551   }
552 
553   @DELETE
554   @Path("{themeId}")
555   @RestQuery(
556       name = "deleteTheme",
557       description = "Deletes a theme",
558       returnDescription = "The method doesn't return any content",
559       pathParameters = {
560           @RestParameter(name = "themeId", isRequired = true, description = "The theme identifier",
561               type = RestParameter.Type.INTEGER)
562       },
563       responses = {
564           @RestResponse(responseCode = SC_NOT_FOUND, description = "If the theme has not been found."),
565           @RestResponse(responseCode = SC_NO_CONTENT, description = "The method does not return any content"),
566           @RestResponse(responseCode = SC_UNAUTHORIZED, description = "If the current user is not authorized to "
567               + "perform this action")
568       })
569   public Response deleteTheme(@PathParam("themeId") long themeId) throws NotFoundException, UnauthorizedException {
570     try {
571       Theme theme = themesServiceDatabase.getTheme(themeId);
572       try {
573         deleteReferencedFiles(theme);
574       } catch (IOException e) {
575         logger.warn("Error while deleting referenced file: {}", e.getMessage());
576         return R.serverError();
577       }
578 
579       themesServiceDatabase.deleteTheme(themeId);
580       deleteThemeOnSeries(themeId);
581 
582       return RestUtil.R.noContent();
583     } catch (NotFoundException e) {
584       logger.warn("Unable to find a theme with id " + themeId);
585       throw e;
586     } catch (ThemesServiceDatabaseException e) {
587       logger.error("Error getting theme {} during delete operation because:", themeId,
588               e);
589       return RestUtil.R.serverError();
590     }
591   }
592 
593   /**
594    * Deletes all related series theme entries
595    *
596    * @param themeId
597    *          the theme id
598    */
599   private void deleteThemeOnSeries(long themeId) throws UnauthorizedException {
600     SeriesSearchQuery query = new SeriesSearchQuery(securityService.getOrganization().getId(),
601             securityService.getUser()).withTheme(themeId);
602     SearchResult<Series> results = null;
603     try {
604       results = searchIndex.getByQuery(query);
605     } catch (SearchIndexException e) {
606       logger.error("The admin UI Search Index was not able to get the series with theme '{}':", themeId, e);
607       throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
608     }
609     for (SearchResultItem<Series> item : results.getItems()) {
610       String seriesId = item.getSource().getIdentifier();
611       try {
612         seriesService.deleteSeriesProperty(seriesId, SeriesEndpoint.THEME_KEY);
613       } catch (NotFoundException e) {
614         logger.warn("Theme {} already deleted on series {}", themeId, seriesId);
615       } catch (SeriesException e) {
616         logger.error("Unable to remove theme from series {}", seriesId, e);
617         throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
618       }
619     }
620   }
621 
622   private void extendStaticFileInfo(String fieldName, String staticFileId, JsonObject json) {
623     if (StringUtils.isNotBlank(staticFileId)) {
624       try {
625         String fileName = staticFileService.getFileName(staticFileId);
626         String fileUrl = staticFileRestService.getStaticFileURL(staticFileId).toString();
627 
628         json.addProperty(fieldName + "Name", fileName);
629         json.addProperty(fieldName + "Url", safeString(fileUrl));
630       } catch (IllegalStateException | NotFoundException e) {
631         logger.error("Error retrieving static file '{}'", staticFileId, e);
632       }
633     }
634   }
635 
636   /**
637    * Returns the JSON representation of this theme.
638    *
639    * @param theme
640    *          the theme
641    * @param editResponse
642    *          whether the returning representation should contain edit information
643    * @return the JSON representation of this theme.
644    */
645   private JsonObject themeToJSON(Theme theme, boolean editResponse) {
646     String creator = StringUtils.isNotBlank(theme.getCreator().getName())
647         ? theme.getCreator().getName()
648         : theme.getCreator().getUsername();
649 
650     JsonObject json = new JsonObject();
651 
652     json.addProperty("id",  theme.getId().orElse(-1L));
653     json.addProperty("creationDate", DateTimeSupport.toUTC(theme.getCreationDate().getTime()));
654     json.addProperty("default", theme.isDefault());
655     json.addProperty("name", theme.getName());
656     json.addProperty("creator", creator);
657     json.addProperty("description", safeString(theme.getDescription()));
658     json.addProperty("bumperActive", theme.isBumperActive());
659     json.addProperty("bumperFile", safeString(theme.getBumperFile()));
660     json.addProperty("trailerActive", theme.isTrailerActive());
661     json.addProperty("trailerFile", safeString(theme.getTrailerFile()));
662     json.addProperty("titleSlideActive", theme.isTitleSlideActive());
663     json.addProperty("titleSlideMetadata", safeString(theme.getTitleSlideMetadata()));
664     json.addProperty("titleSlideBackground", safeString(theme.getTitleSlideBackground()));
665     json.addProperty("licenseSlideActive", theme.isLicenseSlideActive());
666     json.addProperty("licenseSlideDescription", safeString(theme.getLicenseSlideDescription()));
667     json.addProperty("licenseSlideBackground", safeString(theme.getLicenseSlideBackground()));
668     json.addProperty("watermarkActive", theme.isWatermarkActive());
669     json.addProperty("watermarkFile", theme.getWatermarkFile() != null ? theme.getWatermarkFile() : "");
670     json.addProperty("watermarkPosition", theme.getWatermarkPosition() != null ? theme.getWatermarkPosition() : "");
671 
672     if (editResponse) {
673       extendStaticFileInfo("bumperFile", theme.getBumperFile(), json);
674       extendStaticFileInfo("trailerFile", theme.getTrailerFile(), json);
675       extendStaticFileInfo("titleSlideBackground", theme.getTitleSlideBackground(), json);
676       extendStaticFileInfo("licenseSlideBackground", theme.getLicenseSlideBackground(), json);
677       extendStaticFileInfo("watermarkFile", theme.getWatermarkFile(), json);
678     }
679 
680     return json;
681   }
682 
683   /**
684    * Persist all files that are referenced in the theme.
685    *
686    * @param theme
687    *          The theme
688    * @throws NotFoundException
689    *           If a referenced file is not found.
690    * @throws IOException
691    *           If there was an error while persisting the file.
692    */
693   private void persistReferencedFiles(Theme theme) throws NotFoundException, IOException {
694     if (isNotBlank(theme.getBumperFile())) {
695       staticFileService.persistFile(theme.getBumperFile());
696     }
697     if (isNotBlank(theme.getLicenseSlideBackground())) {
698       staticFileService.persistFile(theme.getLicenseSlideBackground());
699     }
700     if (isNotBlank(theme.getTitleSlideBackground())) {
701       staticFileService.persistFile(theme.getTitleSlideBackground());
702     }
703     if (isNotBlank(theme.getTrailerFile())) {
704       staticFileService.persistFile(theme.getTrailerFile());
705     }
706     if (isNotBlank(theme.getWatermarkFile())) {
707       staticFileService.persistFile(theme.getWatermarkFile());
708     }
709   }
710 
711   /**
712    * Delete all files that are referenced in the theme.
713    *
714    * @param theme
715    *          The theme
716    * @throws NotFoundException
717    *           If a referenced file is not found.
718    * @throws IOException
719    *           If there was an error while persisting the file.
720    */
721   private void deleteReferencedFiles(Theme theme) throws NotFoundException, IOException {
722     if (isNotBlank(theme.getBumperFile())) {
723       staticFileService.deleteFile(theme.getBumperFile());
724     }
725     if (isNotBlank(theme.getLicenseSlideBackground())) {
726       staticFileService.deleteFile(theme.getLicenseSlideBackground());
727     }
728     if (isNotBlank(theme.getTitleSlideBackground())) {
729       staticFileService.deleteFile(theme.getTitleSlideBackground());
730     }
731     if (isNotBlank(theme.getTrailerFile())) {
732       staticFileService.deleteFile(theme.getTrailerFile());
733     }
734     if (isNotBlank(theme.getWatermarkFile())) {
735       staticFileService.deleteFile(theme.getWatermarkFile());
736     }
737   }
738 
739   /**
740    * Update all files that have changed between {@code original} and {@code updated}.
741    *
742    * @param original
743    *          The original theme
744    * @param updated
745    *          The updated theme
746    * @throws NotFoundException
747    *           If one of the referenced files could not be found.
748    * @throws IOException
749    *           If there was an error while updating the referenced files.
750    */
751   private void updateReferencedFiles(Theme original, Theme updated) throws NotFoundException, IOException {
752     updateReferencedFile(original.getBumperFile(), updated.getBumperFile());
753     updateReferencedFile(original.getLicenseSlideBackground(), updated.getLicenseSlideBackground());
754     updateReferencedFile(original.getTitleSlideBackground(), updated.getTitleSlideBackground());
755     updateReferencedFile(original.getTrailerFile(), updated.getTrailerFile());
756     updateReferencedFile(original.getWatermarkFile(), updated.getWatermarkFile());
757   }
758 
759   /**
760    * If the file resource has changed between {@code original} and {@code updated}, the original file is deleted and the
761    * updated one persisted.
762    *
763    * @param original
764    *          The UUID of the original file
765    * @param updated
766    *          The UUID of the updated file
767    * @throws NotFoundException
768    *           If the file could not be found
769    * @throws IOException
770    *           If there was an error while persisting or deleting one of the files.
771    */
772   private void updateReferencedFile(String original, String updated) throws NotFoundException, IOException {
773     if (EqualsUtil.ne(original, updated)) {
774       if (isNotBlank(original)) {
775         staticFileService.deleteFile(original);
776       }
777       if (isNotBlank(updated)) {
778         staticFileService.persistFile(updated);
779       }
780     }
781   }
782 
783 }