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.workflow.handler.themes;
23  
24  import static java.lang.String.format;
25  import static org.opencastproject.composer.layout.Offset.offset;
26  
27  import org.opencastproject.composer.layout.AbsolutePositionLayoutSpec;
28  import org.opencastproject.composer.layout.AnchorOffset;
29  import org.opencastproject.composer.layout.Anchors;
30  import org.opencastproject.composer.layout.Serializer;
31  import org.opencastproject.job.api.JobContext;
32  import org.opencastproject.mediapackage.MediaPackage;
33  import org.opencastproject.mediapackage.MediaPackageElement;
34  import org.opencastproject.mediapackage.MediaPackageElement.Type;
35  import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
36  import org.opencastproject.mediapackage.MediaPackageElementFlavor;
37  import org.opencastproject.security.api.UnauthorizedException;
38  import org.opencastproject.series.api.SeriesException;
39  import org.opencastproject.series.api.SeriesService;
40  import org.opencastproject.serviceregistry.api.ServiceRegistry;
41  import org.opencastproject.staticfiles.api.StaticFileService;
42  import org.opencastproject.themes.Theme;
43  import org.opencastproject.themes.ThemesServiceDatabase;
44  import org.opencastproject.themes.persistence.ThemesServiceDatabaseException;
45  import org.opencastproject.util.MimeType;
46  import org.opencastproject.util.MimeTypes;
47  import org.opencastproject.util.NotFoundException;
48  import org.opencastproject.util.UnknownFileTypeException;
49  import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
50  import org.opencastproject.workflow.api.WorkflowInstance;
51  import org.opencastproject.workflow.api.WorkflowOperationException;
52  import org.opencastproject.workflow.api.WorkflowOperationHandler;
53  import org.opencastproject.workflow.api.WorkflowOperationResult;
54  import org.opencastproject.workflow.api.WorkflowOperationResult.Action;
55  import org.opencastproject.workspace.api.Workspace;
56  
57  import com.entwinemedia.fn.Fn;
58  import com.entwinemedia.fn.Stream;
59  import com.entwinemedia.fn.data.Opt;
60  import com.entwinemedia.fn.fns.Strings;
61  
62  import org.apache.commons.lang3.StringUtils;
63  import org.osgi.service.component.annotations.Component;
64  import org.osgi.service.component.annotations.Reference;
65  import org.slf4j.Logger;
66  import org.slf4j.LoggerFactory;
67  
68  import java.io.IOException;
69  import java.io.InputStream;
70  import java.net.URI;
71  import java.util.ArrayList;
72  import java.util.List;
73  import java.util.UUID;
74  
75  /**
76   * The workflow definition for handling "theme" operations
77   */
78  @Component(
79      immediate = true,
80      service = WorkflowOperationHandler.class,
81      property = {
82          "service.description=Theme Workflow Operation Handler",
83          "workflow.operation=theme"
84      }
85  )
86  public class ThemeWorkflowOperationHandler extends AbstractWorkflowOperationHandler {
87  
88    private static final String BUMPER_FLAVOR = "bumper-flavor";
89    private static final String BUMPER_TAGS = "bumper-tags";
90  
91    private static final String TRAILER_FLAVOR = "trailer-flavor";
92    private static final String TRAILER_TAGS = "trailer-tags";
93  
94    private static final String TITLE_SLIDE_FLAVOR = "title-slide-flavor";
95    private static final String TITLE_SLIDE_TAGS = "title-slide-tags";
96  
97    private static final String LICENSE_SLIDE_FLAVOR = "license-slide-flavor";
98    private static final String LICENSE_SLIDE_TAGS = "license-slide-tags";
99  
100   private static final String WATERMARK_FLAVOR = "watermark-flavor";
101   private static final String WATERMARK_TAGS = "watermark-tags";
102   private static final String WATERMARK_LAYOUT = "watermark-layout";
103   private static final String WATERMARK_LAYOUT_VARIABLE = "watermark-layout-variable";
104 
105   /** Workflow property names */
106   private static final String THEME_ACTIVE = "theme_active";
107   private static final String THEME_BUMPER_ACTIVE = "theme_bumper_active";
108   private static final String THEME_TRAILER_ACTIVE = "theme_trailer_active";
109   private static final String THEME_TITLE_SLIDE_ACTIVE = "theme_title_slide_active";
110   private static final String THEME_TITLE_SLIDE_UPLOADED = "theme_title_slide_uploaded";
111   private static final String THEME_WATERMARK_ACTIVE = "theme_watermark_active";
112 
113   /** The series theme property name */
114   private static final String THEME_PROPERTY_NAME = "theme";
115 
116   /** The logging facility */
117   private static final Logger logger = LoggerFactory.getLogger(ThemeWorkflowOperationHandler.class);
118 
119   private static final MediaPackageElementBuilderFactory elementBuilderFactory = MediaPackageElementBuilderFactory
120           .newInstance();
121 
122   /** The series service */
123   private SeriesService seriesService;
124 
125   /** The themes database service */
126   private ThemesServiceDatabase themesServiceDatabase;
127 
128   /** The static file service */
129   private StaticFileService staticFileService;
130 
131   /** The workspace */
132   private Workspace workspace;
133 
134   /** OSGi callback for the series service. */
135   @Reference
136   public void setSeriesService(SeriesService seriesService) {
137     this.seriesService = seriesService;
138   }
139 
140   /** OSGi callback for the themes database service. */
141   @Reference
142   public void setThemesServiceDatabase(ThemesServiceDatabase themesServiceDatabase) {
143     this.themesServiceDatabase = themesServiceDatabase;
144   }
145 
146   /** OSGi callback for the static file service. */
147   @Reference
148   public void setStaticFileService(StaticFileService staticFileService) {
149     this.staticFileService = staticFileService;
150   }
151 
152   /** OSGi callback for the workspace. */
153   @Reference
154   public void setWorkspace(Workspace workspace) {
155     this.workspace = workspace;
156   }
157 
158   @Reference
159   @Override
160   public void setServiceRegistry(ServiceRegistry serviceRegistry) {
161     super.setServiceRegistry(serviceRegistry);
162   }
163 
164   @Override
165   public WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context)
166           throws WorkflowOperationException {
167     logger.debug("Running theme workflow operation on workflow {}", workflowInstance.getId());
168 
169     final MediaPackageElementFlavor bumperFlavor = getOptConfig(workflowInstance, BUMPER_FLAVOR).map(
170             toMediaPackageElementFlavor).getOr(new MediaPackageElementFlavor("branding", "bumper"));
171     final MediaPackageElementFlavor trailerFlavor = getOptConfig(workflowInstance, TRAILER_FLAVOR).map(
172             toMediaPackageElementFlavor).getOr(new MediaPackageElementFlavor("branding", "trailer"));
173     final MediaPackageElementFlavor titleSlideFlavor = getOptConfig(workflowInstance, TITLE_SLIDE_FLAVOR).map(
174             toMediaPackageElementFlavor).getOr(new MediaPackageElementFlavor("branding", "title-slide"));
175     final MediaPackageElementFlavor licenseSlideFlavor = getOptConfig(workflowInstance, LICENSE_SLIDE_FLAVOR).map(
176             toMediaPackageElementFlavor).getOr(new MediaPackageElementFlavor("branding", "license-slide"));
177     final MediaPackageElementFlavor watermarkFlavor = getOptConfig(workflowInstance, WATERMARK_FLAVOR).map(
178             toMediaPackageElementFlavor).getOr(new MediaPackageElementFlavor("branding", "watermark"));
179     final List<String> bumperTags = asList(workflowInstance.getConfiguration(BUMPER_TAGS));
180     final List<String> trailerTags = asList(workflowInstance.getConfiguration(TRAILER_TAGS));
181     final List<String> titleSlideTags = asList(workflowInstance.getConfiguration(TITLE_SLIDE_TAGS));
182     final List<String> licenseSlideTags = asList(workflowInstance.getConfiguration(LICENSE_SLIDE_TAGS));
183     final List<String> watermarkTags = asList(workflowInstance.getConfiguration(WATERMARK_TAGS));
184 
185     Opt<String> layoutStringOpt = getOptConfig(workflowInstance, WATERMARK_LAYOUT);
186     Opt<String> watermarkLayoutVariable = getOptConfig(workflowInstance, WATERMARK_LAYOUT_VARIABLE);
187 
188     List<String> layoutList = new ArrayList<>(Stream.$(layoutStringOpt).bind(Strings.split(";")).toList());
189 
190     try {
191       MediaPackage mediaPackage = workflowInstance.getMediaPackage();
192       String series = mediaPackage.getSeries();
193       if (series == null) {
194         logger.info("Skipping theme workflow operation, no series assigned to mediapackage {}",
195                 mediaPackage.getIdentifier());
196         return createResult(Action.SKIP);
197       }
198 
199       Long themeId;
200       try {
201         themeId = Long.parseLong(seriesService.getSeriesProperty(series, THEME_PROPERTY_NAME));
202       } catch (NotFoundException e) {
203         logger.info("Skipping theme workflow operation, no theme assigned to series {} on mediapackage {}.", series,
204                 mediaPackage.getIdentifier());
205         return createResult(Action.SKIP);
206       } catch (UnauthorizedException e) {
207         logger.warn("Skipping theme workflow operation, user not authorized to perform operation:", e);
208         return createResult(Action.SKIP);
209       }
210 
211       Theme theme;
212       try {
213         theme = themesServiceDatabase.getTheme(themeId);
214       } catch (NotFoundException e) {
215         logger.warn("Skipping theme workflow operation, no theme with id {} found.", themeId);
216         return createResult(Action.SKIP);
217       }
218 
219       logger.info("Applying theme {} to mediapackage {}", themeId, mediaPackage.getIdentifier());
220 
221       /* Make theme settings available to workflow instance */
222       workflowInstance.setConfiguration(THEME_ACTIVE, Boolean.toString(
223                  theme.isBumperActive()
224               || theme.isTrailerActive()
225               || theme.isTitleSlideActive()
226               || theme.isWatermarkActive()
227           )
228       );
229       workflowInstance.setConfiguration(THEME_BUMPER_ACTIVE, Boolean.toString(theme.isBumperActive()));
230       workflowInstance.setConfiguration(THEME_TRAILER_ACTIVE, Boolean.toString(theme.isTrailerActive()));
231       workflowInstance.setConfiguration(THEME_TITLE_SLIDE_ACTIVE, Boolean.toString(theme.isTitleSlideActive()));
232       workflowInstance.setConfiguration(
233           THEME_TITLE_SLIDE_UPLOADED,
234           Boolean.toString(StringUtils.isNotBlank(theme.getTitleSlideBackground())));
235       workflowInstance.setConfiguration(THEME_WATERMARK_ACTIVE, Boolean.toString(theme.isWatermarkActive()));
236 
237       if (theme.isBumperActive() && StringUtils.isNotBlank(theme.getBumperFile())) {
238         try (InputStream bumper = staticFileService.getFile(theme.getBumperFile())) {
239           addElement(mediaPackage, bumperFlavor, bumperTags, bumper,
240                   staticFileService.getFileName(theme.getBumperFile()), Type.Track);
241         } catch (NotFoundException e) {
242           logger.warn("Bumper file {} not found in static file service, skip applying it", theme.getBumperFile());
243         }
244       }
245 
246       if (theme.isTrailerActive() && StringUtils.isNotBlank(theme.getTrailerFile())) {
247         try (InputStream trailer = staticFileService.getFile(theme.getTrailerFile())) {
248           addElement(mediaPackage, trailerFlavor, trailerTags, trailer,
249                   staticFileService.getFileName(theme.getTrailerFile()), Type.Track);
250         } catch (NotFoundException e) {
251           logger.warn("Trailer file {} not found in static file service, skip applying it", theme.getTrailerFile());
252         }
253       }
254 
255       if (theme.isTitleSlideActive()) {
256         if (StringUtils.isNotBlank(theme.getTitleSlideBackground())) {
257           try (InputStream titleSlideBackground = staticFileService.getFile(theme.getTitleSlideBackground())) {
258             addElement(mediaPackage, titleSlideFlavor, titleSlideTags, titleSlideBackground,
259                     staticFileService.getFileName(theme.getTitleSlideBackground()), Type.Attachment);
260           } catch (NotFoundException e) {
261             logger.warn("Title slide file {} not found in static file service, skip applying it",
262                     theme.getTitleSlideBackground());
263           }
264         }
265 
266         // TODO add the title slide metadata to the workflow properties to be used by the cover-image WOH
267         // String titleSlideMetadata = theme.getTitleSlideMetadata();
268       }
269 
270       if (theme.isLicenseSlideActive()) {
271         if (StringUtils.isNotBlank(theme.getLicenseSlideBackground())) {
272           try (InputStream licenseSlideBackground = staticFileService.getFile(theme.getLicenseSlideBackground())) {
273             addElement(mediaPackage, licenseSlideFlavor, licenseSlideTags, licenseSlideBackground,
274                     staticFileService.getFileName(theme.getLicenseSlideBackground()), Type.Attachment);
275           } catch (NotFoundException e) {
276             logger.warn("License slide file {} not found in static file service, skip applying it",
277                     theme.getLicenseSlideBackground());
278           }
279         } else {
280           // TODO define what to do here (maybe extract image as background)
281         }
282 
283         // TODO add the license slide description to the workflow properties to be used by the cover-image WOH
284         // String licenseSlideDescription = theme.getLicenseSlideDescription();
285       }
286 
287       if (theme.isWatermarkActive() && StringUtils.isNotBlank(theme.getWatermarkFile())) {
288         try (InputStream watermark = staticFileService.getFile(theme.getWatermarkFile())) {
289           addElement(mediaPackage, watermarkFlavor, watermarkTags, watermark,
290                   staticFileService.getFileName(theme.getWatermarkFile()), Type.Attachment);
291         } catch (NotFoundException e) {
292           logger.warn("Watermark file {} not found in static file service, skip applying it", theme.getWatermarkFile());
293         }
294 
295         if (layoutStringOpt.isNone() || watermarkLayoutVariable.isNone()) {
296           throw new WorkflowOperationException(format("Configuration key '%s' or '%s' is either missing or empty",
297                   WATERMARK_LAYOUT, WATERMARK_LAYOUT_VARIABLE));
298         }
299 
300         AbsolutePositionLayoutSpec watermarkLayout = parseLayout(theme.getWatermarkPosition());
301         layoutList.set(layoutList.size() - 1, Serializer.json(watermarkLayout).toJson());
302         layoutStringOpt = Opt.some(Stream.$(layoutList).mkString(";"));
303       }
304 
305       if (watermarkLayoutVariable.isSome() && layoutStringOpt.isSome()) {
306         workflowInstance.setConfiguration(watermarkLayoutVariable.get(), layoutStringOpt.get());
307       }
308 
309       return createResult(mediaPackage, Action.CONTINUE);
310     } catch (SeriesException | ThemesServiceDatabaseException | IllegalStateException | IllegalArgumentException
311             | IOException e) {
312       throw new WorkflowOperationException(e);
313     }
314   }
315 
316   private AbsolutePositionLayoutSpec parseLayout(String watermarkPosition) {
317     switch (watermarkPosition) {
318       case "topLeft":
319         return new AbsolutePositionLayoutSpec(new AnchorOffset(Anchors.TOP_LEFT, Anchors.TOP_LEFT, offset(20, 20)));
320       case "topRight":
321         return new AbsolutePositionLayoutSpec(new AnchorOffset(Anchors.TOP_RIGHT, Anchors.TOP_RIGHT, offset(-20, 20)));
322       case "bottomLeft":
323         return new AbsolutePositionLayoutSpec(new AnchorOffset(Anchors.BOTTOM_LEFT, Anchors.BOTTOM_LEFT,
324                 offset(20, -20)));
325       case "bottomRight":
326         return new AbsolutePositionLayoutSpec(new AnchorOffset(Anchors.BOTTOM_RIGHT, Anchors.BOTTOM_RIGHT, offset(-20,
327                 -20)));
328       default:
329         throw new IllegalStateException("Unknown watermark position: " + watermarkPosition);
330     }
331   }
332 
333   private void addElement(MediaPackage mediaPackage, final MediaPackageElementFlavor flavor, final List<String> tags,
334           InputStream file, String filename, Type type) throws IOException {
335     MediaPackageElement element = elementBuilderFactory.newElementBuilder().newElement(type, flavor);
336     element.setIdentifier(UUID.randomUUID().toString());
337     for (String tag : tags) {
338       element.addTag(tag);
339     }
340     URI uri = workspace.put(mediaPackage.getIdentifier().toString(), element.getIdentifier(), filename, file);
341     element.setURI(uri);
342     try {
343       MimeType mimeType = MimeTypes.fromString(filename);
344       element.setMimeType(mimeType);
345     } catch (UnknownFileTypeException e) {
346       logger.warn("Unable to detect the mime type of file {}", filename);
347     }
348     mediaPackage.add(element);
349   }
350 
351   private static Fn<String, MediaPackageElementFlavor> toMediaPackageElementFlavor
352       = new Fn<String, MediaPackageElementFlavor>() {
353         @Override
354         public MediaPackageElementFlavor apply(String flavorString) {
355           return MediaPackageElementFlavor.parseFlavor(flavorString);
356         }
357       };
358 
359 }