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.functions.Misc.chuck;
27  
28  import org.apache.commons.io.FilenameUtils;
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  
32  import java.io.InputStream;
33  import java.net.URI;
34  import java.util.List;
35  import java.util.Objects;
36  import java.util.Optional;
37  import java.util.function.Consumer;
38  import java.util.function.Function;
39  import java.util.function.Predicate;
40  import java.util.stream.Collectors;
41  
42  /** Utility class used for media package handling. */
43  public final class MediaPackageSupport {
44    /** Disable construction of this utility class */
45    private MediaPackageSupport() {
46    }
47  
48    private static final List NIL = java.util.Collections.EMPTY_LIST;
49  
50    /**
51     * Mode used when merging media packages.
52     * <p>
53     * <ul>
54     * <li><code>Merge</code> assigns a new identifier in case of conflicts</li>
55     * <li><code>Replace</code> replaces elements in the target media package with matching identifier</li>
56     * <li><code>Skip</code> skips elements from the source media package with matching identifer</li>
57     * <li><code>Fail</code> fail in case of conflicting identifier</li>
58     * </ul>
59     */
60    public enum MergeMode {
61      Merge, Replace, Skip, Fail
62    }
63  
64    /** the logging facility provided by log4j */
65    private static final Logger logger = LoggerFactory.getLogger(MediaPackageSupport.class.getName());
66  
67    /**
68     * Merges the contents of media package located at <code>sourceDir</code> into the media package located at
69     * <code>targetDir</code>.
70     * <p>
71     * When choosing to move the media package element into the new place instead of copying them, the source media
72     * package folder will be removed afterwards.
73     * </p>
74     *
75     * @param dest
76     *          the target media package directory
77     * @param src
78     *          the source media package directory
79     * @param mode
80     *          conflict resolution strategy in case of identical element identifier
81     * @throws MediaPackageException
82     *           if an error occurs either accessing one of the two media packages or merging them
83     */
84    public static MediaPackage merge(MediaPackage dest, MediaPackage src, MergeMode mode) throws MediaPackageException {
85      try {
86        for (MediaPackageElement e : src.elements()) {
87          if (dest.getElementById(e.getIdentifier()) == null)
88            dest.add(e);
89          else {
90            if (MergeMode.Replace == mode) {
91              logger.debug("Replacing element " + e.getIdentifier() + " while merging " + dest + " with " + src);
92              dest.remove(dest.getElementById(e.getIdentifier()));
93              dest.add(e);
94            } else if (MergeMode.Skip == mode) {
95              logger.debug("Skipping element " + e.getIdentifier() + " while merging " + dest + " with " + src);
96              continue;
97            } else if (MergeMode.Merge == mode) {
98              logger.debug("Renaming element " + e.getIdentifier() + " while merging " + dest + " with " + src);
99              e.setIdentifier(null);
100             dest.add(e);
101           } else if (MergeMode.Fail == mode) {
102             throw new MediaPackageException("Target media package " + dest + " already contains element with id "
103                     + e.getIdentifier());
104           }
105         }
106       }
107     } catch (UnsupportedElementException e) {
108       throw new MediaPackageException(e);
109     }
110     return dest;
111   }
112 
113   /**
114    * Returns <code>true</code> if the media package contains an element with the specified identifier.
115    *
116    * @param identifier
117    *          the identifier
118    * @return <code>true</code> if the media package contains an element with this identifier
119    */
120   public static boolean contains(String identifier, MediaPackage mp) {
121     for (MediaPackageElement element : mp.getElements()) {
122       if (element.getIdentifier().equals(identifier))
123         return true;
124     }
125     return false;
126   }
127 
128   /**
129    * Extract the file name from a media package elements URI.
130    *
131    * @return the file name or none if it could not be determined
132    */
133   public static Optional<String> getFileName(MediaPackageElement mpe) {
134     URI uri = mpe.getURI();
135     if (uri == null) {
136       return Optional.empty();
137     }
138     String name = FilenameUtils.getName(uri.toString());
139     if (name == null || name.isBlank()) {
140       return Optional.empty();
141     }
142     return Optional.of(name);
143   }
144 
145   /**
146    * Create a copy of the given media package.
147    * <p>
148    * ATTENTION: Copying changes the type of the media package elements, e.g. an element of
149    * type <code>DublinCoreCatalog</code> will become a <code>CatalogImpl</code>.
150    */
151   public static MediaPackage copy(MediaPackage mp) {
152     return (MediaPackage) mp.clone();
153   }
154 
155   /** Update a mediapackage element of a mediapackage. Mutates <code>mp</code>. */
156   public static void updateElement(MediaPackage mp, MediaPackageElement e) {
157     mp.removeElementById(e.getIdentifier());
158     mp.add(e);
159   }
160 
161   /** {@link #updateElement(MediaPackage, MediaPackageElement)} as en effect. */
162   public static Consumer<MediaPackageElement> updateElement(final MediaPackage mp) {
163     return e -> updateElement(mp, e);
164   }
165 
166   /** Filters and predicates to work with media package element collections. */
167   public static final class Filters {
168     private Filters() {
169     }
170 
171     // functions implemented for monadic bind in order to cast types
172     public static final Predicate<MediaPackageElement> isPublication = Publication.class::isInstance;
173     public static final Predicate<MediaPackageElement> isNotPublication = isPublication.negate();
174 
175     public static final Predicate<MediaPackageElement> hasChecksum = e -> e.getChecksum() != null;
176 
177     public static final Predicate<MediaPackageElement> hasNoChecksum = hasChecksum.negate();
178 
179     /** Filters publications to channel <code>channelId</code>. */
180     public static boolean ofChannel(Publication p, String channelId) {
181       return p.getChannel().equals(channelId);
182     }
183 
184     /** Check if mediapackage element has any of the given tags. */
185     public static boolean hasTagAny(MediaPackageElement mpe, List<String> tags) {
186       return mpe.containsTag(tags);
187     }
188 
189     /**
190      * Return true if the element has a flavor that matches <code>flavor</code>.
191      *
192      * @see MediaPackageElementFlavor#matches(MediaPackageElementFlavor)
193      */
194     public static Function<MediaPackageElement, Boolean> matchesFlavor(final MediaPackageElementFlavor flavor) {
195       return mpe -> flavor.matches(mpe.getFlavor());
196     }
197 
198     public static boolean isEpisodeDublinCore(MediaPackageElement mpe) {
199       // match is commutative
200       return MediaPackageElements.EPISODE.matches(mpe.getFlavor());
201     }
202 
203     public static boolean isSmilCatalog(MediaPackageElement mpe) {
204       // match is commutative
205       return MediaPackageElements.SMIL.matches(mpe.getFlavor());
206     }
207   }
208 
209   /**
210    * Basic sanity checking for media packages.
211    *
212    * <pre>
213    * // media package is ok
214    * sanityCheck(mp).isNone()
215    * </pre>
216    *
217    * @return none if the media package is a healthy condition, some([error_msgs]) otherwise
218    */
219   public static Optional<List<String>> sanityCheck(MediaPackage mp) {
220     List<String> errors = java.util.stream.Stream.of(
221             mp.getIdentifier() == null ? "no ID" : null,
222             (mp.getIdentifier() != null && isNotBlank(mp.getIdentifier().toString())) ? null : "blank ID"
223         )
224         .filter(Objects::nonNull)
225         .collect(Collectors.toList());
226 
227     return errors.isEmpty() ? Optional.empty() : Optional.of(errors);
228   }
229 
230   /** To be used in unit tests. */
231   public static MediaPackage loadFromClassPath(String path) {
232     return withResource(
233         MediaPackageSupport.class.getResourceAsStream(path),
234         (InputStream is) -> {
235           try {
236             return MediaPackageBuilderFactory.newInstance()
237                 .newMediaPackageBuilder()
238                 .loadFromXml(is);
239           } catch (Exception e) {
240             return chuck(e);
241           }
242         }
243     );
244   }
245 }