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