/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License * at: * * http://opensource.org/licenses/ecl2.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package org.opencastproject.mediapackage; import static com.entwinemedia.fn.Prelude.chuck; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.opencastproject.util.IoSupport.withResource; import static org.opencastproject.util.data.Collections.list; import static org.opencastproject.util.data.Option.option; import static org.opencastproject.util.data.functions.Booleans.not; import static org.opencastproject.util.data.functions.Options.sequenceOpt; import static org.opencastproject.util.data.functions.Options.toOption; import org.opencastproject.fun.juc.Immutables; import org.opencastproject.util.PathSupport; import org.opencastproject.util.data.Effect; import org.opencastproject.util.data.Function; import org.opencastproject.util.data.Option; import com.entwinemedia.fn.data.Opt; import com.entwinemedia.fn.fns.Strings; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.List; /** Utility class used for media package handling. */ public final class MediaPackageSupport { /** Disable construction of this utility class */ private MediaPackageSupport() { } private static final List NIL = java.util.Collections.EMPTY_LIST; /** * Mode used when merging media packages. * <p> * <ul> * <li><code>Merge</code> assigns a new identifier in case of conflicts</li> * <li><code>Replace</code> replaces elements in the target media package with matching identifier</li> * <li><code>Skip</code> skips elements from the source media package with matching identifer</li> * <li><code>Fail</code> fail in case of conflicting identifier</li> * </ul> */ enum MergeMode { Merge, Replace, Skip, Fail } /** the logging facility provided by log4j */ private static final Logger logger = LoggerFactory.getLogger(MediaPackageSupport.class.getName()); /** * Merges the contents of media package located at <code>sourceDir</code> into the media package located at * <code>targetDir</code>. * <p> * When choosing to move the media package element into the new place instead of copying them, the source media * package folder will be removed afterwards. * </p> * * @param dest * the target media package directory * @param src * the source media package directory * @param mode * conflict resolution strategy in case of identical element identifier * @throws MediaPackageException * if an error occurs either accessing one of the two media packages or merging them */ public static MediaPackage merge(MediaPackage dest, MediaPackage src, MergeMode mode) throws MediaPackageException { try { for (MediaPackageElement e : src.elements()) { if (dest.getElementById(e.getIdentifier()) == null) dest.add(e); else { if (MergeMode.Replace == mode) { logger.debug("Replacing element " + e.getIdentifier() + " while merging " + dest + " with " + src); dest.remove(dest.getElementById(e.getIdentifier())); dest.add(e); } else if (MergeMode.Skip == mode) { logger.debug("Skipping element " + e.getIdentifier() + " while merging " + dest + " with " + src); continue; } else if (MergeMode.Merge == mode) { logger.debug("Renaming element " + e.getIdentifier() + " while merging " + dest + " with " + src); e.setIdentifier(null); dest.add(e); } else if (MergeMode.Fail == mode) { throw new MediaPackageException("Target media package " + dest + " already contains element with id " + e.getIdentifier()); } } } } catch (UnsupportedElementException e) { throw new MediaPackageException(e); } return dest; } /** * Returns <code>true</code> if the media package contains an element with the specified identifier. * * @param identifier * the identifier * @return <code>true</code> if the media package contains an element with this identifier */ public static boolean contains(String identifier, MediaPackage mp) { for (MediaPackageElement element : mp.getElements()) { if (element.getIdentifier().equals(identifier)) return true; } return false; } /** * Extract the file name from a media package elements URI. * * @return the file name or none if it could not be determined */ public static Opt<String> getFileName(MediaPackageElement mpe) { final URI uri = mpe.getURI(); if (uri != null) { return Opt.nul(FilenameUtils.getName(uri.toString())).bind(Strings.blankToNone); } else { return Opt.none(); } } public static InputStream getJsonInputStream(MediaPackage mp) { return getInputStream(MediaPackageParser.getAsJSON(mp)); } public static InputStream getXmlInputStream(MediaPackage mp) { return getInputStream(MediaPackageParser.getAsXml(mp)); } public static InputStream getJsonInputStream(XMLCatalog catalog) { try { return getInputStream(catalog.toJson()); } catch (IOException e) { return chuck(e); } } public static InputStream getXmlInputStream(XMLCatalog catalog) { try { return getInputStream(catalog.toXmlString()); } catch (IOException e) { return chuck(e); } } /** * Get a UTF-8 encoded input stream. */ private static InputStream getInputStream(String s) { try { return IOUtils.toInputStream(s, "UTF-8"); } catch (IOException e) { return chuck(e); } } /** * Creates a unique filename inside the root folder, based on the parameter <code>filename</code>. * * @param root * the root folder * @param filename * the original filename * @return the new and unique filename */ public static File createElementFilename(File root, String filename) { String baseName = PathSupport.removeFileExtension(filename); String extension = PathSupport.getFileExtension(filename); int count = 1; StringBuffer name = null; File f = new File(root, filename); while (f.exists()) { name = new StringBuffer(baseName).append("-").append(count).append(".").append(extension); f = new File(root, name.toString()); count++; } return f; } /** Immutable modification of a media package. */ public static MediaPackage modify(MediaPackage mp, Effect<MediaPackage> e) { final MediaPackage clone = (MediaPackage) mp.clone(); e.apply(clone); return clone; } /** * Immutable modification of a media package element. Attention: The returned element loses its media package * membership (see {@link org.opencastproject.mediapackage.AbstractMediaPackageElement#clone()}) */ public static <A extends MediaPackageElement> A modify(A mpe, Effect<A> e) { final A clone = (A) mpe.clone(); e.apply(clone); return clone; } /** * Create a copy of the given media package. * <p> * ATTENTION: Copying changes the type of the media package elements, e.g. an element of * type <code>DublinCoreCatalog</code> will become a <code>CatalogImpl</code>. */ public static MediaPackage copy(MediaPackage mp) { return (MediaPackage) mp.clone(); } /** Create a copy of the given media package element. */ public static MediaPackageElement copy(MediaPackageElement mpe) { return (MediaPackageElement) mpe.clone(); } /** Update a mediapackage element of a mediapackage. Mutates <code>mp</code>. */ public static void updateElement(MediaPackage mp, MediaPackageElement e) { mp.removeElementById(e.getIdentifier()); mp.add(e); } /** {@link #updateElement(MediaPackage, MediaPackageElement)} as en effect. */ public static Effect<MediaPackageElement> updateElement(final MediaPackage mp) { return new Effect<MediaPackageElement>() { @Override protected void run(MediaPackageElement e) { updateElement(mp, e); } }; } public static void removeElements(List<MediaPackageElement> es, MediaPackage mp) { for (MediaPackageElement e : es) { mp.remove(e); } } public static Effect<MediaPackage> removeElements(final List<MediaPackageElement> es) { return new Effect<MediaPackage>() { @Override protected void run(MediaPackage mp) { removeElements(es, mp); } }; } /** Replaces all elements of <code>mp</code> with <code>es</code>. Mutates <code>mp</code>. */ public static void replaceElements(MediaPackage mp, List<MediaPackageElement> es) { for (MediaPackageElement e : mp.getElements()) mp.remove(e); for (MediaPackageElement e : es) mp.add(e); } public static final Function<MediaPackageElement, String> getMediaPackageElementId = new Function<MediaPackageElement, String>() { @Override public String apply(MediaPackageElement mediaPackageElement) { return mediaPackageElement.getIdentifier(); } }; public static final Function<MediaPackageElement, Option<String>> getMediaPackageElementReferenceId = new Function<MediaPackageElement, Option<String>>() { @Override public Option<String> apply(MediaPackageElement mediaPackageElement) { return option(mediaPackageElement.getReference()).map(getReferenceId); } }; public static final Function<MediaPackageReference, String> getReferenceId = new Function<MediaPackageReference, String>() { @Override public String apply(MediaPackageReference mediaPackageReference) { return mediaPackageReference.getIdentifier(); } }; /** Get the checksum from a media package element. */ public static final Function<MediaPackageElement, Option<String>> getChecksum = new Function<MediaPackageElement, Option<String>>() { @Override public Option<String> apply(MediaPackageElement mpe) { return option(mpe.getChecksum().getValue()); } }; /** Filters and predicates to work with media package element collections. */ public static final class Filters { private Filters() { } // functions implemented for monadic bind in order to cast types public static <A extends MediaPackageElement> Function<MediaPackageElement, List<A>> byType(final Class<A> type) { return new Function<MediaPackageElement, List<A>>() { @Override public List<A> apply(MediaPackageElement mpe) { return type.isAssignableFrom(mpe.getClass()) ? list((A) mpe) : (List<A>) NIL; } }; } public static Function<MediaPackageElement, List<MediaPackageElement>> byFlavor( final MediaPackageElementFlavor flavor) { return new Function<MediaPackageElement, List<MediaPackageElement>>() { @Override public List<MediaPackageElement> apply(MediaPackageElement mpe) { // match is commutative return flavor.matches(mpe.getFlavor()) ? list(mpe) : Immutables.<MediaPackageElement> nil(); } }; } public static Function<MediaPackageElement, List<MediaPackageElement>> byTags(final List<String> tags) { return new Function<MediaPackageElement, List<MediaPackageElement>>() { @Override public List<MediaPackageElement> apply(MediaPackageElement mpe) { return mpe.containsTag(tags) ? list(mpe) : Immutables.<MediaPackageElement> nil(); } }; } /** {@link MediaPackageElement#containsTag(java.util.Collection)} as a function. */ public static Function<MediaPackageElement, Boolean> ofTags(final List<String> tags) { return new Function<MediaPackageElement, Boolean>() { @Override public Boolean apply(MediaPackageElement mpe) { return mpe.containsTag(tags); } }; } public static <A extends MediaPackageElement> Function<MediaPackageElement, Boolean> ofType(final Class<A> type) { return new Function<MediaPackageElement, Boolean>() { @Override public Boolean apply(MediaPackageElement mpe) { return type.isAssignableFrom(mpe.getClass()); } }; } public static final Function<MediaPackageElement, List<Publication>> presentations = byType(Publication.class); public static final Function<MediaPackageElement, List<Attachment>> attachments = byType(Attachment.class); public static final Function<MediaPackageElement, List<Track>> tracks = byType(Track.class); public static final Function<MediaPackageElement, List<Catalog>> catalogs = byType(Catalog.class); public static final Function<MediaPackageElement, Boolean> isPublication = ofType(Publication.class); public static final Function<MediaPackageElement, Boolean> isNotPublication = not(isPublication); public static final Function<MediaPackageElement, Boolean> hasChecksum = new Function<MediaPackageElement, Boolean>() { @Override public Boolean apply(MediaPackageElement e) { return e.getChecksum() != null; } }; public static final Function<MediaPackageElement, Boolean> hasNoChecksum = not(hasChecksum); public static final Function<Track, Boolean> hasVideo = new Function<Track, Boolean>() { @Override public Boolean apply(Track track) { return track.hasVideo(); } }; public static final Function<Track, Boolean> hasAudio = new Function<Track, Boolean>() { @Override public Boolean apply(Track track) { return track.hasAudio(); } }; public static final Function<Track, Boolean> hasNoVideo = not(hasVideo); public static final Function<Track, Boolean> hasNoAudio = not(hasAudio); /** Filters publications to channel <code>channelId</code>. */ public static Function<Publication, Boolean> ofChannel(final String channelId) { return new Function<Publication, Boolean>() { @Override public Boolean apply(Publication p) { return p.getChannel().equals(channelId); } }; } /** Check if mediapackage element has any of the given tags. */ public static Function<MediaPackageElement, Boolean> hasTagAny(final List<String> tags) { return new Function<MediaPackageElement, Boolean>() { @Override public Boolean apply(MediaPackageElement mpe) { return mpe.containsTag(tags); } }; } public static Function<MediaPackageElement, Boolean> hasTag(final String tag) { return new Function<MediaPackageElement, Boolean>() { @Override public Boolean apply(MediaPackageElement mpe) { return mpe.containsTag(tag); } }; } /** * Return true if the element has a flavor that matches <code>flavor</code>. * * @see MediaPackageElementFlavor#matches(MediaPackageElementFlavor) */ public static Function<MediaPackageElement, Boolean> matchesFlavor(final MediaPackageElementFlavor flavor) { return new Function<MediaPackageElement, Boolean>() { @Override public Boolean apply(MediaPackageElement mpe) { // match is commutative return flavor.matches(mpe.getFlavor()); } }; } /** * Return true if the element has a flavor that matches any of the <code>flavors</code>. * * @see MediaPackageElementFlavor#matches(MediaPackageElementFlavor) */ public static Function<MediaPackageElement, Boolean> matchesFlavorAny(final List<MediaPackageElementFlavor> flavors) { return new Function<MediaPackageElement, Boolean>() { @Override public Boolean apply(MediaPackageElement mpe) { for (MediaPackageElementFlavor f : flavors) { if (f.matches(mpe.getFlavor())) { return true; } } return false; } }; } public static final Function<MediaPackageElementFlavor, Function<MediaPackageElement, Boolean>> matchesFlavor = new Function<MediaPackageElementFlavor, Function<MediaPackageElement, Boolean>>() { @Override public Function<MediaPackageElement, Boolean> apply(final MediaPackageElementFlavor flavor) { return matchesFlavor(flavor); } }; /** {@link MediaPackageElementFlavor#matches(MediaPackageElementFlavor)} as a function. */ public static Function<MediaPackageElementFlavor, Boolean> matches(final MediaPackageElementFlavor flavor) { return new Function<MediaPackageElementFlavor, Boolean>() { @Override public Boolean apply(MediaPackageElementFlavor f) { return f.matches(flavor); } }; } public static final Function<MediaPackageElement, Boolean> isXACML = MediaPackageSupport.Filters .matchesFlavorAny(list(MediaPackageElements.XACML_POLICY, MediaPackageElements.XACML_POLICY_EPISODE, MediaPackageElements.XACML_POLICY_SERIES)); public static final Function<MediaPackageElement, Boolean> isEpisodeAcl = new Function<MediaPackageElement, Boolean>() { @Override public Boolean apply(MediaPackageElement mpe) { // match is commutative return MediaPackageElements.XACML_POLICY_EPISODE.matches(mpe.getFlavor()); } }; public static final Function<MediaPackageElement, Boolean> isEpisodeDublinCore = new Function<MediaPackageElement, Boolean>() { @Override public Boolean apply(MediaPackageElement mpe) { // match is commutative return MediaPackageElements.EPISODE.matches(mpe.getFlavor()); } }; public static final Function<MediaPackageElement, Boolean> isSeriesDublinCore = new Function<MediaPackageElement, Boolean>() { @Override public Boolean apply(MediaPackageElement mpe) { // match is commutative return MediaPackageElements.SERIES.matches(mpe.getFlavor()); } }; public static final Function<MediaPackageElement, Boolean> isSmilCatalog = new Function<MediaPackageElement, Boolean>() { @Override public Boolean apply(MediaPackageElement mpe) { // match is commutative return MediaPackageElements.SMIL.matches(mpe.getFlavor()); } }; } /** * Basic sanity checking for media packages. * * <pre> * // media package is ok * sanityCheck(mp).isNone() * </pre> * * @return none if the media package is a healthy condition, some([error_msgs]) otherwise */ public static Option<List<String>> sanityCheck(MediaPackage mp) { final Option<List<String>> errors = sequenceOpt(list(toOption(mp.getIdentifier() != null, "no ID"), toOption(mp.getIdentifier() != null && isNotBlank(mp.getIdentifier().toString()), "blank ID"))); return errors.getOrElse(NIL).size() == 0 ? Option.<List<String>> none() : errors; } /** To be used in unit tests. */ public static MediaPackage loadFromClassPath(String path) { return withResource(MediaPackageSupport.class.getResourceAsStream(path), new Function.X<InputStream, MediaPackage>() { @Override public MediaPackage xapply(InputStream is) throws MediaPackageException { return MediaPackageBuilderFactory.newInstance().newMediaPackageBuilder().loadFromXml(is); } }); } /** * Function to extract the ID of a media package. * * @deprecated use {@link Fn#getId} */ @Deprecated public static final Function<MediaPackage, String> getId = new Function<MediaPackage, String>() { @Override public String apply(MediaPackage mp) { return mp.getIdentifier().toString(); } }; /** Functions on media packages. */ public static final class Fn { private Fn() { } /** Function to extract the ID of a media package. */ public static final Function<MediaPackage, String> getId = new Function<MediaPackage, String>() { @Override public String apply(MediaPackage mp) { return mp.getIdentifier().toString(); } }; public static final Function<MediaPackage, List<MediaPackageElement>> getElements = new Function<MediaPackage, List<MediaPackageElement>>() { @Override public List<MediaPackageElement> apply(MediaPackage a) { return Immutables.list(a.getElements()); } }; public static final Function<MediaPackage, List<Track>> getTracks = new Function<MediaPackage, List<Track>>() { @Override public List<Track> apply(MediaPackage a) { return Immutables.list(a.getTracks()); } }; public static final Function<MediaPackage, List<Publication>> getPublications = new Function<MediaPackage, List<Publication>>() { @Override public List<Publication> apply(MediaPackage a) { return Immutables.list(a.getPublications()); } }; } }