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.mediapackage;
23  
24  import static org.apache.commons.lang3.StringUtils.isNotBlank;
25  import static org.opencastproject.util.IoSupport.withResource;
26  import static org.opencastproject.util.data.Collections.list;
27  import static org.opencastproject.util.data.functions.Booleans.not;
28  import static org.opencastproject.util.data.functions.Options.sequenceOpt;
29  import static org.opencastproject.util.data.functions.Options.toOption;
30  
31  import org.opencastproject.util.data.Function;
32  import org.opencastproject.util.data.Option;
33  
34  import com.entwinemedia.fn.data.Opt;
35  import com.entwinemedia.fn.fns.Strings;
36  
37  import org.apache.commons.io.FilenameUtils;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  import java.io.InputStream;
42  import java.net.URI;
43  import java.util.Collections;
44  import java.util.List;
45  import java.util.function.Consumer;
46  
47  /** Utility class used for media package handling. */
48  public final class MediaPackageSupport {
49    /** Disable construction of this utility class */
50    private MediaPackageSupport() {
51    }
52  
53    private static final List NIL = java.util.Collections.EMPTY_LIST;
54  
55    /**
56     * Mode used when merging media packages.
57     * <p>
58     * <ul>
59     * <li><code>Merge</code> assigns a new identifier in case of conflicts</li>
60     * <li><code>Replace</code> replaces elements in the target media package with matching identifier</li>
61     * <li><code>Skip</code> skips elements from the source media package with matching identifer</li>
62     * <li><code>Fail</code> fail in case of conflicting identifier</li>
63     * </ul>
64     */
65    public enum MergeMode {
66      Merge, Replace, Skip, Fail
67    }
68  
69    /** the logging facility provided by log4j */
70    private static final Logger logger = LoggerFactory.getLogger(MediaPackageSupport.class.getName());
71  
72    /**
73     * Merges the contents of media package located at <code>sourceDir</code> into the media package located at
74     * <code>targetDir</code>.
75     * <p>
76     * When choosing to move the media package element into the new place instead of copying them, the source media
77     * package folder will be removed afterwards.
78     * </p>
79     *
80     * @param dest
81     *          the target media package directory
82     * @param src
83     *          the source media package directory
84     * @param mode
85     *          conflict resolution strategy in case of identical element identifier
86     * @throws MediaPackageException
87     *           if an error occurs either accessing one of the two media packages or merging them
88     */
89    public static MediaPackage merge(MediaPackage dest, MediaPackage src, MergeMode mode) throws MediaPackageException {
90      try {
91        for (MediaPackageElement e : src.elements()) {
92          if (dest.getElementById(e.getIdentifier()) == null)
93            dest.add(e);
94          else {
95            if (MergeMode.Replace == mode) {
96              logger.debug("Replacing element " + e.getIdentifier() + " while merging " + dest + " with " + src);
97              dest.remove(dest.getElementById(e.getIdentifier()));
98              dest.add(e);
99            } else if (MergeMode.Skip == mode) {
100             logger.debug("Skipping element " + e.getIdentifier() + " while merging " + dest + " with " + src);
101             continue;
102           } else if (MergeMode.Merge == mode) {
103             logger.debug("Renaming element " + e.getIdentifier() + " while merging " + dest + " with " + src);
104             e.setIdentifier(null);
105             dest.add(e);
106           } else if (MergeMode.Fail == mode) {
107             throw new MediaPackageException("Target media package " + dest + " already contains element with id "
108                     + e.getIdentifier());
109           }
110         }
111       }
112     } catch (UnsupportedElementException e) {
113       throw new MediaPackageException(e);
114     }
115     return dest;
116   }
117 
118   /**
119    * Returns <code>true</code> if the media package contains an element with the specified identifier.
120    *
121    * @param identifier
122    *          the identifier
123    * @return <code>true</code> if the media package contains an element with this identifier
124    */
125   public static boolean contains(String identifier, MediaPackage mp) {
126     for (MediaPackageElement element : mp.getElements()) {
127       if (element.getIdentifier().equals(identifier))
128         return true;
129     }
130     return false;
131   }
132 
133   /**
134    * Extract the file name from a media package elements URI.
135    *
136    * @return the file name or none if it could not be determined
137    */
138   public static Opt<String> getFileName(MediaPackageElement mpe) {
139     final URI uri = mpe.getURI();
140     if (uri != null) {
141       return Opt.nul(FilenameUtils.getName(uri.toString())).bind(Strings.blankToNone);
142     } else {
143       return Opt.none();
144     }
145   }
146 
147   /**
148    * Create a copy of the given media package.
149    * <p>
150    * ATTENTION: Copying changes the type of the media package elements, e.g. an element of
151    * type <code>DublinCoreCatalog</code> will become a <code>CatalogImpl</code>.
152    */
153   public static MediaPackage copy(MediaPackage mp) {
154     return (MediaPackage) mp.clone();
155   }
156 
157   /** Update a mediapackage element of a mediapackage. Mutates <code>mp</code>. */
158   public static void updateElement(MediaPackage mp, MediaPackageElement e) {
159     mp.removeElementById(e.getIdentifier());
160     mp.add(e);
161   }
162 
163   /** {@link #updateElement(MediaPackage, MediaPackageElement)} as en effect. */
164 
165 
166   public static Consumer<MediaPackageElement> updateElement(final MediaPackage mp) {
167     return e -> updateElement(mp, e);
168   }
169 
170   public static final Function<MediaPackageElement, String> getMediaPackageElementId = new Function<MediaPackageElement, String>() {
171     @Override
172     public String apply(MediaPackageElement mediaPackageElement) {
173       return mediaPackageElement.getIdentifier();
174     }
175   };
176 
177   /** Filters and predicates to work with media package element collections. */
178   public static final class Filters {
179     private Filters() {
180     }
181 
182     // functions implemented for monadic bind in order to cast types
183 
184     public static <A extends MediaPackageElement> Function<MediaPackageElement, List<A>> byType(final Class<A> type) {
185       return new Function<MediaPackageElement, List<A>>() {
186         @Override
187         public List<A> apply(MediaPackageElement mpe) {
188           return type.isAssignableFrom(mpe.getClass()) ? list((A) mpe) : (List<A>) NIL;
189         }
190       };
191     }
192 
193     public static Function<MediaPackageElement, List<MediaPackageElement>> byFlavor(
194             final MediaPackageElementFlavor flavor) {
195       return new Function<MediaPackageElement, List<MediaPackageElement>>() {
196         @Override
197         public List<MediaPackageElement> apply(MediaPackageElement mpe) {
198           // match is commutative
199           return flavor.matches(mpe.getFlavor()) ? Collections.singletonList(mpe) : Collections.emptyList();
200         }
201       };
202     }
203 
204     public static <A extends MediaPackageElement> Function<MediaPackageElement, Boolean> ofType(final Class<A> type) {
205       return new Function<MediaPackageElement, Boolean>() {
206         @Override
207         public Boolean apply(MediaPackageElement mpe) {
208           return type.isAssignableFrom(mpe.getClass());
209         }
210       };
211     }
212 
213     public static final Function<MediaPackageElement, List<Publication>> presentations = byType(Publication.class);
214 
215     public static final Function<MediaPackageElement, List<Attachment>> attachments = byType(Attachment.class);
216 
217     public static final Function<MediaPackageElement, List<Track>> tracks = byType(Track.class);
218 
219     public static final Function<MediaPackageElement, List<Catalog>> catalogs = byType(Catalog.class);
220 
221     public static final Function<MediaPackageElement, Boolean> isPublication = ofType(Publication.class);
222 
223     public static final Function<MediaPackageElement, Boolean> isNotPublication = not(isPublication);
224 
225     public static final Function<MediaPackageElement, Boolean> hasChecksum = new Function<MediaPackageElement, Boolean>() {
226       @Override
227       public Boolean apply(MediaPackageElement e) {
228         return e.getChecksum() != null;
229       }
230     };
231 
232     public static final Function<MediaPackageElement, Boolean> hasNoChecksum = not(hasChecksum);
233 
234     public static final Function<Track, Boolean> hasVideo = new Function<Track, Boolean>() {
235       @Override
236       public Boolean apply(Track track) {
237         return track.hasVideo();
238       }
239     };
240 
241     public static final Function<Track, Boolean> hasAudio = new Function<Track, Boolean>() {
242       @Override
243       public Boolean apply(Track track) {
244         return track.hasAudio();
245       }
246     };
247 
248     public static final Function<Track, Boolean> hasNoVideo = not(hasVideo);
249 
250     public static final Function<Track, Boolean> hasNoAudio = not(hasAudio);
251 
252     /** Filters publications to channel <code>channelId</code>. */
253     public static Function<Publication, Boolean> ofChannel(final String channelId) {
254       return new Function<Publication, Boolean>() {
255         @Override
256         public Boolean apply(Publication p) {
257           return p.getChannel().equals(channelId);
258         }
259       };
260     }
261 
262     /** Check if mediapackage element has any of the given tags. */
263     public static Function<MediaPackageElement, Boolean> hasTagAny(final List<String> tags) {
264       return new Function<MediaPackageElement, Boolean>() {
265         @Override
266         public Boolean apply(MediaPackageElement mpe) {
267           return mpe.containsTag(tags);
268         }
269       };
270     }
271 
272     /**
273      * Return true if the element has a flavor that matches <code>flavor</code>.
274      *
275      * @see MediaPackageElementFlavor#matches(MediaPackageElementFlavor)
276      */
277     public static Function<MediaPackageElement, Boolean> matchesFlavor(final MediaPackageElementFlavor flavor) {
278       return new Function<MediaPackageElement, Boolean>() {
279         @Override
280         public Boolean apply(MediaPackageElement mpe) {
281           // match is commutative
282           return flavor.matches(mpe.getFlavor());
283         }
284       };
285     }
286 
287     public static final Function<MediaPackageElementFlavor, Function<MediaPackageElement, Boolean>> matchesFlavor = new Function<MediaPackageElementFlavor, Function<MediaPackageElement, Boolean>>() {
288       @Override
289       public Function<MediaPackageElement, Boolean> apply(final MediaPackageElementFlavor flavor) {
290         return matchesFlavor(flavor);
291       }
292     };
293 
294     public static final Function<MediaPackageElement, Boolean> isEpisodeDublinCore = new Function<MediaPackageElement, Boolean>() {
295       @Override
296       public Boolean apply(MediaPackageElement mpe) {
297         // match is commutative
298         return MediaPackageElements.EPISODE.matches(mpe.getFlavor());
299       }
300     };
301 
302     public static final Function<MediaPackageElement, Boolean> isSmilCatalog = new Function<MediaPackageElement, Boolean>() {
303       @Override
304       public Boolean apply(MediaPackageElement mpe) {
305         // match is commutative
306         return MediaPackageElements.SMIL.matches(mpe.getFlavor());
307       }
308     };
309   }
310 
311   /**
312    * Basic sanity checking for media packages.
313    *
314    * <pre>
315    * // media package is ok
316    * sanityCheck(mp).isNone()
317    * </pre>
318    *
319    * @return none if the media package is a healthy condition, some([error_msgs]) otherwise
320    */
321   public static Option<List<String>> sanityCheck(MediaPackage mp) {
322     final Option<List<String>> errors = sequenceOpt(list(toOption(mp.getIdentifier() != null, "no ID"),
323             toOption(mp.getIdentifier() != null && isNotBlank(mp.getIdentifier().toString()), "blank ID")));
324     return errors.getOrElse(NIL).size() == 0 ? Option.<List<String>> none() : errors;
325   }
326 
327   /** To be used in unit tests. */
328   public static MediaPackage loadFromClassPath(String path) {
329     return withResource(MediaPackageSupport.class.getResourceAsStream(path),
330             new Function.X<InputStream, MediaPackage>() {
331               @Override
332               public MediaPackage xapply(InputStream is) throws MediaPackageException {
333                 return MediaPackageBuilderFactory.newInstance().newMediaPackageBuilder().loadFromXml(is);
334               }
335             });
336   }
337 
338   /**
339    * Function to extract the ID of a media package.
340    */
341   @Deprecated
342   public static final Function<MediaPackage, String> getId = new Function<MediaPackage, String>() {
343     @Override
344     public String apply(MediaPackage mp) {
345       return mp.getIdentifier().toString();
346     }
347   };
348 
349   /** Functions on media packages. */
350   public static final class Fn {
351     private Fn() {
352     }
353 
354   }
355 }