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.rename;
23  
24  import org.opencastproject.job.api.JobContext;
25  import org.opencastproject.mediapackage.MediaPackage;
26  import org.opencastproject.mediapackage.MediaPackageElementFlavor;
27  import org.opencastproject.mediapackage.MediaPackageElements;
28  import org.opencastproject.mediapackage.Track;
29  import org.opencastproject.mediapackage.VideoStream;
30  import org.opencastproject.mediapackage.selector.TrackSelector;
31  import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
32  import org.opencastproject.metadata.dublincore.DublinCoreUtil;
33  import org.opencastproject.util.NotFoundException;
34  import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
35  import org.opencastproject.workflow.api.WorkflowInstance;
36  import org.opencastproject.workflow.api.WorkflowOperationException;
37  import org.opencastproject.workflow.api.WorkflowOperationHandler;
38  import org.opencastproject.workflow.api.WorkflowOperationResult;
39  import org.opencastproject.workflow.api.WorkflowOperationResult.Action;
40  import org.opencastproject.workspace.api.Workspace;
41  
42  import org.apache.commons.io.FilenameUtils;
43  import org.osgi.service.component.ComponentContext;
44  import org.osgi.service.component.annotations.Activate;
45  import org.osgi.service.component.annotations.Component;
46  import org.osgi.service.component.annotations.Reference;
47  import org.slf4j.Logger;
48  import org.slf4j.LoggerFactory;
49  
50  import java.io.IOException;
51  import java.util.Arrays;
52  import java.util.HashMap;
53  import java.util.List;
54  import java.util.Map;
55  import java.util.UUID;
56  
57  /**
58   * The <code>RenameFilesWorkflowOperationHandler</code> will rename files referenced in tracks based on metadata
59   * contained in the media package.
60   */
61  @Component(
62      property = {
63          "service.description=Rename Files Workflow Operation Handler",
64          "workflow.operation=rename-files"
65      },
66      immediate = true,
67      service = WorkflowOperationHandler.class
68  )
69  public class RenameFilesWorkflowOperationHandler extends AbstractWorkflowOperationHandler {
70  
71    /** The logging facility */
72    private static final Logger logger = LoggerFactory.getLogger(RenameFilesWorkflowOperationHandler.class);
73  
74    /** The local workspace */
75    private Workspace workspace = null;
76  
77    /**
78     * Callback for declarative services configuration that will introduce us to the local workspace service.
79     * Implementation assumes that the reference is configured as being static.
80     *
81     * @param workspace
82     *          an instance of the workspace
83     */
84    @Reference
85    public void setWorkspace(Workspace workspace) {
86      this.workspace = workspace;
87    }
88  
89    /**
90     * {@inheritDoc}
91     *
92     * @see org.opencastproject.workflow.api.WorkflowOperationHandler#start(
93     *      org.opencastproject.workflow.api.WorkflowInstance, JobContext)
94     */
95    @Override
96    public WorkflowOperationResult start(WorkflowInstance workflowInstance, JobContext context)
97            throws WorkflowOperationException {
98  
99      final var operation = workflowInstance.getCurrentOperation();
100     final var mediaPackage = workflowInstance.getMediaPackage();
101     final var mediaPackageId = mediaPackage.getIdentifier().toString();
102 
103     logger.info("Running rename files workflow operation on workflow {}", workflowInstance.getId());
104 
105     // Read configuration
106     // source-flavors source-tags
107     String pattern = operation.getConfiguration("name-pattern");
108     if (pattern == null) {
109       throw new WorkflowOperationException("name-pattern must be configured");
110     }
111     logger.debug("name-pattern {}", pattern);
112 
113 
114     var tagsAndFlavors = getTagsAndFlavors(
115             workflowInstance,
116             Configuration.none,  // source-tags
117             Configuration.many,  // source-flavors
118             Configuration.none,  // target-tags
119             Configuration.none); // target-flavors
120 
121     // Select tracks by evaluating source tags and flavors
122     List<MediaPackageElementFlavor> sourceFlavors = tagsAndFlavors.getSrcFlavors();
123 
124     TrackSelector trackSelector = new TrackSelector();
125     for (MediaPackageElementFlavor sourceFlavor: sourceFlavors) {
126       trackSelector.addFlavor(sourceFlavor);
127     }
128 
129     for (var track: trackSelector.select(mediaPackage, false)) {
130       var uri = track.getURI();
131       var extension = FilenameUtils.getExtension(uri.toString());
132       var newElementId = UUID.randomUUID().toString();
133 
134       // Prepare placeholders and filename
135       var filename = pattern;
136       for (var entry: placeholders(mediaPackage, track).entrySet()) {
137         filename = filename.replace(entry.getKey(), entry.getValue());
138       }
139       filename = filename.replaceAll("#\\{[a-z.]*}", "_");
140 
141       // Put updated filename in working file repository and update the track.
142       // Make sure it has a new identifier to prevent conflicts with the old files.
143       try (var in = workspace.read(uri)) {
144         var newUri = workspace.put(mediaPackageId, newElementId, filename, in);
145         logger.info("Renaming {} to {}", uri, newUri);
146         track.setIdentifier(newElementId);
147         track.setURI(newUri);
148       } catch (NotFoundException | IOException e) {
149         throw new WorkflowOperationException("Failed moving track file", e);
150       }
151 
152       // Delete the old files from the working file repository and workspace if they were in there
153       logger.debug("Removing old track file {}", uri);
154       try {
155         workspace.delete(uri);
156       } catch (NotFoundException | IOException e) {
157         logger.debug("Could not remove track from workspace. Could be it was never there.");
158       }
159 
160     }
161 
162     return createResult(mediaPackage, Action.CONTINUE);
163   }
164 
165   /**
166    * Generate map or placeholders.
167    *
168    * @param element
169    *          Current media package element
170    * @param mediaPackage
171    *          Current media package
172    * @return Map of placeholders.
173    */
174   private Map<String, String> placeholders(MediaPackage mediaPackage, Track element) {
175 
176     var placeholders = new HashMap<String, String>();
177 
178     var width = Arrays.stream(element.getStreams())
179         .filter(s -> s instanceof VideoStream)
180         .map(s -> (VideoStream) s)
181         .findFirst()
182         .map(VideoStream::getFrameWidth)
183         .map(Object::toString)
184         .orElse("");
185     // Placeholder resolution width
186     placeholders.put("#{video.width}", width);
187 
188     var height = Arrays.stream(element.getStreams())
189             .filter(h -> h instanceof VideoStream)
190             .map(h -> (VideoStream) h)
191             .findFirst()
192             .map(VideoStream::getFrameHeight)
193             .map(Object::toString)
194             .orElse("");
195     //Placeholder resolution height
196     placeholders.put("#{video.height}", height);
197 
198     // file placeholders
199     placeholders.put("#{file.extension}", FilenameUtils.getExtension(element.getURI().toString()));
200     placeholders.put("#{file.basename}", FilenameUtils.getBaseName(element.getURI().toString()));
201 
202     // flavor placeholders
203     placeholders.put("#{flavor.type}", element.getFlavor().getType());
204     placeholders.put("#{flavor.subtype}", element.getFlavor().getSubtype());
205 
206     // metadata placeholders
207     for (var flavor: Arrays.asList(MediaPackageElements.EPISODE, MediaPackageElements.SERIES)) {
208       // Get metadata catalogs
209       for (var catalog : mediaPackage.getCatalogs(flavor)) {
210         DublinCoreCatalog dc = DublinCoreUtil.loadDublinCore(workspace, catalog);
211         for (var entry : dc.getValues().entrySet()) {
212           var key = String.format("#{%s.%s}", flavor.getSubtype(), entry.getKey().getLocalName());
213           var value = entry.getValue().get(0).getValue();
214           placeholders.put(key, value);
215         }
216       }
217     }
218 
219     logger.debug("Placeholders to use for renaming: {}", placeholders);
220     return placeholders;
221   }
222 
223   @Activate
224   @Override
225   protected void activate(ComponentContext cc) {
226     super.activate(cc);
227   }
228 }