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  package org.opencastproject.workflow.handler.timelinepreviews;
22  
23  import org.opencastproject.job.api.Job;
24  import org.opencastproject.job.api.JobContext;
25  import org.opencastproject.mediapackage.MediaPackage;
26  import org.opencastproject.mediapackage.MediaPackageElement;
27  import org.opencastproject.mediapackage.MediaPackageElementFlavor;
28  import org.opencastproject.mediapackage.MediaPackageElementParser;
29  import org.opencastproject.mediapackage.MediaPackageException;
30  import org.opencastproject.mediapackage.Track;
31  import org.opencastproject.mediapackage.selector.TrackSelector;
32  import org.opencastproject.serviceregistry.api.ServiceRegistry;
33  import org.opencastproject.timelinepreviews.api.TimelinePreviewsException;
34  import org.opencastproject.timelinepreviews.api.TimelinePreviewsService;
35  import org.opencastproject.util.IoSupport;
36  import org.opencastproject.util.NotFoundException;
37  import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
38  import org.opencastproject.workflow.api.ConfiguredTagsAndFlavors;
39  import org.opencastproject.workflow.api.WorkflowInstance;
40  import org.opencastproject.workflow.api.WorkflowOperationException;
41  import org.opencastproject.workflow.api.WorkflowOperationHandler;
42  import org.opencastproject.workflow.api.WorkflowOperationResult;
43  import org.opencastproject.workspace.api.Workspace;
44  
45  import org.apache.commons.io.FilenameUtils;
46  import org.apache.commons.lang3.BooleanUtils;
47  import org.apache.commons.lang3.StringUtils;
48  import org.osgi.service.component.ComponentContext;
49  import org.osgi.service.component.annotations.Activate;
50  import org.osgi.service.component.annotations.Component;
51  import org.osgi.service.component.annotations.Reference;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  
55  import java.io.File;
56  import java.io.FileInputStream;
57  import java.io.FileNotFoundException;
58  import java.io.IOException;
59  import java.net.URI;
60  import java.util.ArrayList;
61  import java.util.Collection;
62  import java.util.List;
63  
64  /**
65   * Workflow operation for the timeline previews service.
66   */
67  @Component(
68      immediate = true,
69      service = WorkflowOperationHandler.class,
70      property = {
71          "service.description=Timeline Preview Images Workflow Operation Handler",
72          "workflow.operation=timelinepreviews"
73      }
74  )
75  public class TimelinePreviewsWorkflowOperationHandler extends AbstractWorkflowOperationHandler {
76  
77    /** The logging facility */
78    private static final Logger logger = LoggerFactory.getLogger(TimelinePreviewsWorkflowOperationHandler.class);
79  
80    /** Source flavor configuration property name. */
81    private static final String SOURCE_FLAVOR_PROPERTY = "source-flavor";
82  
83    /** Source tags configuration property name. */
84    private static final String SOURCE_TAGS_PROPERTY = "source-tags";
85  
86    /** Target flavor configuration property name. */
87    private static final String TARGET_FLAVOR_PROPERTY = "target-flavor";
88  
89    /** Target tags configuration property name. */
90    private static final String TARGET_TAGS_PROPERTY = "target-tags";
91  
92    /** Process first match only */
93    private static final String PROCCESS_FIRST_MATCH = "process-first-match-only";
94  
95    /** Image size configuration property name. */
96    private static final String IMAGE_SIZE_PROPERTY = "image-count";
97  
98    /** Default value for image size. */
99    private static final int DEFAULT_IMAGE_SIZE = 10;
100 
101   /** The timeline previews service. */
102   private TimelinePreviewsService timelinePreviewsService = null;
103 
104   /** The workspace service. */
105   private Workspace workspace = null;
106 
107   @Override
108   @Activate
109   public void activate(ComponentContext cc) {
110     super.activate(cc);
111     logger.info("Registering timeline previews workflow operation handler");
112   }
113 
114   @Override
115   public WorkflowOperationResult start(WorkflowInstance workflowInstance, JobContext context)
116           throws WorkflowOperationException {
117     MediaPackage mediaPackage = workflowInstance.getMediaPackage();
118     logger.info("Start timeline previews workflow operation for mediapackage {}",
119         mediaPackage.getIdentifier().toString());
120 
121     ConfiguredTagsAndFlavors tagsAndFlavors = getTagsAndFlavors(workflowInstance,
122         Configuration.many, Configuration.many, Configuration.many, Configuration.one);
123     List<MediaPackageElementFlavor> sourceFlavorProperty = tagsAndFlavors.getSrcFlavors();
124     List<String> sourceTagsProperty = tagsAndFlavors.getSrcTags();
125     if (sourceFlavorProperty.isEmpty() && sourceTagsProperty.isEmpty()) {
126       throw new WorkflowOperationException(String.format("Required property %s or %s not set",
127               SOURCE_FLAVOR_PROPERTY, SOURCE_TAGS_PROPERTY));
128     }
129     MediaPackageElementFlavor targetFlavor = tagsAndFlavors.getSingleTargetFlavor();
130     List<String> targetTagsProperty = tagsAndFlavors.getTargetTags();
131 
132     String imageSizeArg = StringUtils.trimToNull(
133             workflowInstance.getCurrentOperation().getConfiguration(IMAGE_SIZE_PROPERTY));
134     int imageSize;
135     if (imageSizeArg != null) {
136       try {
137         imageSize = Integer.parseInt(imageSizeArg);
138       } catch (NumberFormatException e) {
139         imageSize = DEFAULT_IMAGE_SIZE;
140         logger.info("No valid integer given for property {}, using default value: {}",
141                 IMAGE_SIZE_PROPERTY, DEFAULT_IMAGE_SIZE);
142       }
143     } else {
144       imageSize = DEFAULT_IMAGE_SIZE;
145       logger.info("Property {} not set, using default value: {}", IMAGE_SIZE_PROPERTY, DEFAULT_IMAGE_SIZE);
146     }
147 
148     boolean processOnlyOne = BooleanUtils.toBoolean(StringUtils.trimToNull(
149             workflowInstance.getCurrentOperation().getConfiguration(PROCCESS_FIRST_MATCH)));
150 
151     TrackSelector trackSelector = new TrackSelector();
152     for (MediaPackageElementFlavor flavor : sourceFlavorProperty) {
153       trackSelector.addFlavor(flavor);
154     }
155     for (String tag : sourceTagsProperty) {
156       trackSelector.addTag(tag);
157     }
158     Collection<Track> sourceTracks = trackSelector.select(mediaPackage, true);
159     if (sourceTracks.isEmpty()) {
160       logger.info("No tracks found in mediapackage {} with specified {} {}", mediaPackage.getIdentifier().toString(),
161               SOURCE_FLAVOR_PROPERTY,
162               sourceFlavorProperty);
163       createResult(mediaPackage, WorkflowOperationResult.Action.SKIP);
164     }
165 
166     List<Job> timelinepreviewsJobs = new ArrayList<Job>(sourceTracks.size());
167     for (Track sourceTrack : sourceTracks) {
168       try {
169         // generate timeline preview images
170         logger.info("Create timeline previews job for track '{}' in mediapackage '{}'",
171                 sourceTrack.getIdentifier(), mediaPackage.getIdentifier().toString());
172 
173         Job timelinepreviewsJob = timelinePreviewsService.createTimelinePreviewImages(sourceTrack, imageSize);
174         timelinepreviewsJobs.add(timelinepreviewsJob);
175 
176         if (processOnlyOne) {
177           break;
178         }
179 
180       } catch (MediaPackageException | TimelinePreviewsException ex) {
181         logger.error("Creating timeline previews job for track '{}' in media package '{}' failed with error {}",
182                 sourceTrack.getIdentifier(), mediaPackage.getIdentifier().toString(), ex.getMessage());
183       }
184     }
185 
186     logger.info("Wait for timeline previews jobs for media package {}", mediaPackage.getIdentifier().toString());
187     if (!waitForStatus(timelinepreviewsJobs.toArray(new Job[timelinepreviewsJobs.size()])).isSuccess()) {
188       cleanupWorkspace(timelinepreviewsJobs);
189       throw new WorkflowOperationException(
190               String.format("Timeline previews jobs for media package '%s' have not completed successfully",
191                       mediaPackage.getIdentifier().toString()));
192     }
193 
194 
195     try {
196       // copy timeline previews attachments into workspace and add them to the media package
197       for (Job job : timelinepreviewsJobs) {
198         String jobPayload = job.getPayload();
199         if (StringUtils.isNotEmpty(jobPayload)) {
200           MediaPackageElement timelinePreviewsMpe = null;
201           File timelinePreviewsFile = null;
202           try {
203             timelinePreviewsMpe = MediaPackageElementParser.getFromXml(jobPayload);
204             timelinePreviewsFile = workspace.get(timelinePreviewsMpe.getURI());
205           } catch (MediaPackageException ex) {
206             // unexpected job payload
207             throw new WorkflowOperationException("Can't parse timeline previews attachment from job " + job.getId());
208           } catch (NotFoundException ex) {
209             throw new WorkflowOperationException("Timeline preview images file '" + timelinePreviewsMpe.getURI()
210                     + "' not found", ex);
211           } catch (IOException ex) {
212             throw new WorkflowOperationException("Can't get workflow image file '" + timelinePreviewsMpe.getURI()
213                     + "' from workspace");
214           }
215 
216           FileInputStream timelinePreviewsInputStream = null;
217           logger.info("Put timeline preview images file {} from media package {} to the media package work space",
218                   timelinePreviewsMpe.getURI(), mediaPackage.getIdentifier().toString());
219 
220           try {
221             timelinePreviewsInputStream = new FileInputStream(timelinePreviewsFile);
222             String fileName = FilenameUtils.getName(timelinePreviewsMpe.getURI().getPath());
223             URI timelinePreviewsWfrUri = workspace.put(mediaPackage.getIdentifier().toString(),
224                     timelinePreviewsMpe.getIdentifier(), fileName, timelinePreviewsInputStream);
225             timelinePreviewsMpe.setURI(timelinePreviewsWfrUri);
226           } catch (FileNotFoundException ex) {
227             throw new WorkflowOperationException("Timeline preview images file " + timelinePreviewsFile.getPath()
228                     + " not found", ex);
229           } catch (IOException ex) {
230             throw new WorkflowOperationException("Can't read just created timeline preview images file "
231                     + timelinePreviewsFile.getPath(), ex);
232           } catch (IllegalArgumentException ex) {
233             throw new WorkflowOperationException(ex);
234           } finally {
235             IoSupport.closeQuietly(timelinePreviewsInputStream);
236           }
237 
238           // set the timeline previews attachment flavor and add it to the mediapackage
239           if ("*".equals(targetFlavor.getType())) {
240             targetFlavor = new MediaPackageElementFlavor(
241                 timelinePreviewsMpe.getFlavor().getType(), targetFlavor.getSubtype());
242           }
243           if ("*".equals(targetFlavor.getSubtype())) {
244             targetFlavor = new MediaPackageElementFlavor(
245                 targetFlavor.getType(), timelinePreviewsMpe.getFlavor().getSubtype());
246           }
247           timelinePreviewsMpe.setFlavor(targetFlavor);
248           if (!targetTagsProperty.isEmpty()) {
249             for (String tag : targetTagsProperty) {
250               timelinePreviewsMpe.addTag(tag);
251             }
252           }
253 
254           mediaPackage.add(timelinePreviewsMpe);
255         }
256       }
257     } finally {
258       cleanupWorkspace(timelinepreviewsJobs);
259     }
260 
261 
262     logger.info("Timeline previews workflow operation for mediapackage {} completed",
263         mediaPackage.getIdentifier().toString());
264     return createResult(mediaPackage, WorkflowOperationResult.Action.CONTINUE);
265   }
266 
267   /**
268    * Remove all files created by the given jobs
269    * @param jobs
270    */
271   private void cleanupWorkspace(List<Job> jobs) {
272     for (Job job : jobs) {
273       String jobPayload = job.getPayload();
274       if (StringUtils.isNotEmpty(jobPayload)) {
275         try {
276           MediaPackageElement timelinepreviewsMpe = MediaPackageElementParser.getFromXml(jobPayload);
277           URI timelinepreviewsUri = timelinepreviewsMpe.getURI();
278           workspace.delete(timelinepreviewsUri);
279         } catch (MediaPackageException ex) {
280             // unexpected job payload
281           logger.error("Can't parse timeline previews attachment from job {}", job.getId());
282         } catch (NotFoundException ex) {
283             // this is ok, because we want delete the file
284         } catch (IOException ex) {
285           logger.warn("Deleting timeline previews image file from workspace failed: {}", ex.getMessage());
286             // this is ok, because workspace cleaner will remove old files if they exist
287         }
288       }
289     }
290   }
291 
292   @Reference
293   public void setTimelinePreviewsService(TimelinePreviewsService timelinePreviewsService) {
294     this.timelinePreviewsService = timelinePreviewsService;
295   }
296 
297   @Reference
298   public void setWorkspace(Workspace workspace) {
299     this.workspace = workspace;
300   }
301 
302   @Reference
303   @Override
304   public void setServiceRegistry(ServiceRegistry serviceRegistry) {
305     super.setServiceRegistry(serviceRegistry);
306   }
307 
308 }