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.Effect;
32  import org.opencastproject.util.data.Function;
33  import org.opencastproject.util.data.Option;
34  
35  import com.entwinemedia.fn.data.Opt;
36  import com.entwinemedia.fn.fns.Strings;
37  
38  import org.apache.commons.io.FilenameUtils;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  import java.io.InputStream;
43  import java.net.URI;
44  import java.util.Collections;
45  import java.util.List;
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   public static Effect<MediaPackageElement> updateElement(final MediaPackage mp) {
165     return new Effect<MediaPackageElement>() {
166       @Override
167       protected void run(MediaPackageElement e) {
168         updateElement(mp, e);
169       }
170     };
171   }
172 
173   public static final Function<MediaPackageElement, String> getMediaPackageElementId = new Function<MediaPackageElement, String>() {
174     @Override
175     public String apply(MediaPackageElement mediaPackageElement) {
176       return mediaPackageElement.getIdentifier();
177     }
178   };
179 
180   /** Filters and predicates to work with media package element collections. */
181   public static final class Filters {
182     private Filters() {
183     }
184 
185     // functions implemented for monadic bind in order to cast types
186 
187     public static <A extends MediaPackageElement> Function<MediaPackageElement, List<A>> byType(final Class<A> type) {
188       return new Function<MediaPackageElement, List<A>>() {
189         @Override
190         public List<A> apply(MediaPackageElement mpe) {
191           return type.isAssignableFrom(mpe.getClass()) ? list((A) mpe) : (List<A>) NIL;
192         }
193       };
194     }
195 
196     public static Function<MediaPackageElement, List<MediaPackageElement>> byFlavor(
197             final MediaPackageElementFlavor flavor) {
198       return new Function<MediaPackageElement, List<MediaPackageElement>>() {
199         @Override
200         public List<MediaPackageElement> apply(MediaPackageElement mpe) {
201           // match is commutative
202           return flavor.matches(mpe.getFlavor()) ? Collections.singletonList(mpe) : Collections.emptyList();
203         }
204       };
205     }
206 
207     public static <A extends MediaPackageElement> Function<MediaPackageElement, Boolean> ofType(final Class<A> type) {
208       return new Function<MediaPackageElement, Boolean>() {
209         @Override
210         public Boolean apply(MediaPackageElement mpe) {
211           return type.isAssignableFrom(mpe.getClass());
212         }
213       };
214     }
215 
216     public static final Function<MediaPackageElement, List<Publication>> presentations = byType(Publication.class);
217 
218     public static final Function<MediaPackageElement, List<Attachment>> attachments = byType(Attachment.class);
219 
220     public static final Function<MediaPackageElement, List<Track>> tracks = byType(Track.class);
221 
222     public static final Function<MediaPackageElement, List<Catalog>> catalogs = byType(Catalog.class);
223 
224     public static final Function<MediaPackageElement, Boolean> isPublication = ofType(Publication.class);
225 
226     public static final Function<MediaPackageElement, Boolean> isNotPublication = not(isPublication);
227 
228     public static final Function<MediaPackageElement, Boolean> hasChecksum = new Function<MediaPackageElement, Boolean>() {
229       @Override
230       public Boolean apply(MediaPackageElement e) {
231         return e.getChecksum() != null;
232       }
233     };
234 
235     public static final Function<MediaPackageElement, Boolean> hasNoChecksum = not(hasChecksum);
236 
237     public static final Function<Track, Boolean> hasVideo = new Function<Track, Boolean>() {
238       @Override
239       public Boolean apply(Track track) {
240         return track.hasVideo();
241       }
242     };
243 
244     public static final Function<Track, Boolean> hasAudio = new Function<Track, Boolean>() {
245       @Override
246       public Boolean apply(Track track) {
247         return track.hasAudio();
248       }
249     };
250 
251     public static final Function<Track, Boolean> hasNoVideo = not(hasVideo);
252 
253     public static final Function<Track, Boolean> hasNoAudio = not(hasAudio);
254 
255     /** Filters publications to channel <code>channelId</code>. */
256     public static Function<Publication, Boolean> ofChannel(final String channelId) {
257       return new Function<Publication, Boolean>() {
258         @Override
259         public Boolean apply(Publication p) {
260           return p.getChannel().equals(channelId);
261         }
262       };
263     }
264 
265     /** Check if mediapackage element has any of the given tags. */
266     public static Function<MediaPackageElement, Boolean> hasTagAny(final List<String> tags) {
267       return new Function<MediaPackageElement, Boolean>() {
268         @Override
269         public Boolean apply(MediaPackageElement mpe) {
270           return mpe.containsTag(tags);
271         }
272       };
273     }
274 
275     /**
276      * Return true if the element has a flavor that matches <code>flavor</code>.
277      *
278      * @see MediaPackageElementFlavor#matches(MediaPackageElementFlavor)
279      */
280     public static Function<MediaPackageElement, Boolean> matchesFlavor(final MediaPackageElementFlavor flavor) {
281       return new Function<MediaPackageElement, Boolean>() {
282         @Override
283         public Boolean apply(MediaPackageElement mpe) {
284           // match is commutative
285           return flavor.matches(mpe.getFlavor());
286         }
287       };
288     }
289 
290     public static final Function<MediaPackageElementFlavor, Function<MediaPackageElement, Boolean>> matchesFlavor = new Function<MediaPackageElementFlavor, Function<MediaPackageElement, Boolean>>() {
291       @Override
292       public Function<MediaPackageElement, Boolean> apply(final MediaPackageElementFlavor flavor) {
293         return matchesFlavor(flavor);
294       }
295     };
296 
297     public static final Function<MediaPackageElement, Boolean> isEpisodeDublinCore = new Function<MediaPackageElement, Boolean>() {
298       @Override
299       public Boolean apply(MediaPackageElement mpe) {
300         // match is commutative
301         return MediaPackageElements.EPISODE.matches(mpe.getFlavor());
302       }
303     };
304 
305     public static final Function<MediaPackageElement, Boolean> isSmilCatalog = new Function<MediaPackageElement, Boolean>() {
306       @Override
307       public Boolean apply(MediaPackageElement mpe) {
308         // match is commutative
309         return MediaPackageElements.SMIL.matches(mpe.getFlavor());
310       }
311     };
312   }
313 
314   /**
315    * Basic sanity checking for media packages.
316    *
317    * <pre>
318    * // media package is ok
319    * sanityCheck(mp).isNone()
320    * </pre>
321    *
322    * @return none if the media package is a healthy condition, some([error_msgs]) otherwise
323    */
324   public static Option<List<String>> sanityCheck(MediaPackage mp) {
325     final Option<List<String>> errors = sequenceOpt(list(toOption(mp.getIdentifier() != null, "no ID"),
326             toOption(mp.getIdentifier() != null && isNotBlank(mp.getIdentifier().toString()), "blank ID")));
327     return errors.getOrElse(NIL).size() == 0 ? Option.<List<String>> none() : errors;
328   }
329 
330   /** To be used in unit tests. */
331   public static MediaPackage loadFromClassPath(String path) {
332     return withResource(MediaPackageSupport.class.getResourceAsStream(path),
333             new Function.X<InputStream, MediaPackage>() {
334               @Override
335               public MediaPackage xapply(InputStream is) throws MediaPackageException {
336                 return MediaPackageBuilderFactory.newInstance().newMediaPackageBuilder().loadFromXml(is);
337               }
338             });
339   }
340 
341   /**
342    * Function to extract the ID of a media package.
343    */
344   @Deprecated
345   public static final Function<MediaPackage, String> getId = new Function<MediaPackage, String>() {
346     @Override
347     public String apply(MediaPackage mp) {
348       return mp.getIdentifier().toString();
349     }
350   };
351 
352   /** Functions on media packages. */
353   public static final class Fn {
354     private Fn() {
355     }
356 
357   }
358 }