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