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