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