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