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