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 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
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
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
114 private static final String THEME_PROPERTY_NAME = "theme";
115
116
117 private static final Logger logger = LoggerFactory.getLogger(ThemeWorkflowOperationHandler.class);
118
119 private static final MediaPackageElementBuilderFactory elementBuilderFactory = MediaPackageElementBuilderFactory
120 .newInstance();
121
122
123 private SeriesService seriesService;
124
125
126 private ThemesServiceDatabase themesServiceDatabase;
127
128
129 private StaticFileService staticFileService;
130
131
132 private Workspace workspace;
133
134
135 @Reference
136 public void setSeriesService(SeriesService seriesService) {
137 this.seriesService = seriesService;
138 }
139
140
141 @Reference
142 public void setThemesServiceDatabase(ThemesServiceDatabase themesServiceDatabase) {
143 this.themesServiceDatabase = themesServiceDatabase;
144 }
145
146
147 @Reference
148 public void setStaticFileService(StaticFileService staticFileService) {
149 this.staticFileService = staticFileService;
150 }
151
152
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
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
267
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
281 }
282
283
284
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 }