/** * 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.assetmanager.impl; import static com.entwinemedia.fn.Prelude.chuck; import static com.entwinemedia.fn.Stream.$; import static java.lang.String.format; import static org.opencastproject.mediapackage.MediaPackageSupport.Filters.hasNoChecksum; import static org.opencastproject.mediapackage.MediaPackageSupport.Filters.isNotPublication; import static org.opencastproject.mediapackage.MediaPackageSupport.getFileName; import static org.opencastproject.mediapackage.MediaPackageSupport.getMediaPackageElementId; import org.opencastproject.assetmanager.api.Asset; import org.opencastproject.assetmanager.api.AssetId; import org.opencastproject.assetmanager.api.AssetManager; import org.opencastproject.assetmanager.api.AssetManagerException; import org.opencastproject.assetmanager.api.Availability; import org.opencastproject.assetmanager.api.Property; import org.opencastproject.assetmanager.api.Snapshot; import org.opencastproject.assetmanager.api.Version; import org.opencastproject.assetmanager.api.query.AQueryBuilder; import org.opencastproject.assetmanager.impl.persistence.AssetDtos; import org.opencastproject.assetmanager.impl.persistence.Database; import org.opencastproject.assetmanager.impl.persistence.SnapshotDto; import org.opencastproject.assetmanager.impl.query.AQueryBuilderImpl; import org.opencastproject.assetmanager.impl.storage.AssetStore; import org.opencastproject.assetmanager.impl.storage.Source; import org.opencastproject.assetmanager.impl.storage.StoragePath; import org.opencastproject.mediapackage.MediaPackage; import org.opencastproject.mediapackage.MediaPackageElement; import org.opencastproject.mediapackage.MediaPackageParser; import org.opencastproject.mediapackage.MediaPackageSupport; import org.opencastproject.util.Checksum; import org.opencastproject.util.ChecksumType; import org.opencastproject.util.MimeTypes; import org.opencastproject.util.NotFoundException; import org.opencastproject.util.RequireUtil; import org.opencastproject.workspace.api.Workspace; import com.entwinemedia.fn.Fn; import com.entwinemedia.fn.Fx; import com.entwinemedia.fn.P1; import com.entwinemedia.fn.P1Lazy; import com.entwinemedia.fn.Pred; import com.entwinemedia.fn.data.Opt; import com.entwinemedia.fn.fns.Booleans; import com.entwinemedia.fn.fns.Strings; 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.net.URISyntaxException; import java.util.Date; /** * Core implementation of the asset manager interface. * <p> * This implementation features only basic functionality and does not * cover security or messaging aspects. */ public abstract class AbstractAssetManager implements AssetManager { /** Log facility */ private static final Logger logger = LoggerFactory.getLogger(AbstractAssetManager.class); /* ------------------------------------------------------------------------------------------------------------------ */ // // Dependencies // public abstract Database getDb(); public abstract HttpAssetProvider getHttpAssetProvider(); public abstract AssetStore getAssetStore(); /** The workspace is used to download assets from their URIs. */ protected abstract Workspace getWorkspace(); /** Return the organization ID of the currently executing thread. */ protected abstract String getCurrentOrgId(); /* ------------------------------------------------------------------------------------------------------------------ */ @Override public Snapshot takeSnapshot(final String owner, final MediaPackage mp) { RequireUtil.notEmpty(owner, "owner"); return handleException(new P1Lazy<Snapshot>() { @Override public Snapshot get1() { try { final Snapshot archived = addInternal(owner, MediaPackageSupport.copy(mp)).toSnapshot(); return getHttpAssetProvider().prepareForDelivery(archived); } catch (Exception e) { return chuck(e); } } }); } @Override public void setAvailability(Version version, String mpId, Availability availability) { getDb().setAvailability(RuntimeTypes.convert(version), mpId, availability); } @Override public boolean setProperty(Property property) { return getDb().saveProperty(property); } @Override public AQueryBuilder createQuery() { return new AQueryBuilderImpl(this); } @Override public Opt<Asset> getAsset(Version version, final String mpId, final String mpeId) { // try to fetch the asset for (final AssetDtos.Medium asset : getDb().getAsset(RuntimeTypes.convert(version), mpId, mpeId)) { for (final InputStream assetStream : getAssetStore().get(StoragePath.mk(asset.getOrganizationId(), mpId, version, mpeId))) { final Asset a = new AssetImpl( AssetId.mk(version, mpId, mpeId), assetStream, asset.getAssetDto().getMimeType(), asset.getAssetDto().getSize(), asset.getAvailability()); return Opt.some(a); } } return Opt.none(); } @Override public Opt<Version> toVersion(String version) { try { return Opt.<Version>some(VersionImpl.mk(Long.parseLong(version))); } catch (NumberFormatException e) { return Opt.none(); } } /* ------------------------------------------------------------------------------------------------------------------ */ /** * Make sure each of the elements has a checksum. */ void calcChecksumsForMediaPackageElements(PartialMediaPackage pmp) { pmp.getElements().filter(hasNoChecksum.toFn()).each(addChecksum).run(); } /** Mutates mp and its elements, so make sure to work on a copy. */ private SnapshotDto addInternal(String owner, final MediaPackage mp) throws Exception { final Date now = new Date(); // claim a new version for the media package final String mpId = mp.getIdentifier().toString(); final VersionImpl version = getDb().claimVersion(mpId); logger.info("Creating new version {} of media package {}", version, mp); final PartialMediaPackage pmp = assetsOnly(mp); // make sure they have a checksum calcChecksumsForMediaPackageElements(pmp); // download and archive elements storeAssets(pmp, version); // store mediapackage in db final SnapshotDto snapshotDto; try { rewriteUrisForArchival(pmp, version); snapshotDto = getDb().saveSnapshot(getCurrentOrgId(), pmp, now, version, Availability.ONLINE, owner); } catch (AssetManagerException e) { logger.error("Could not take snapshot {}: {}", mpId, e); throw new AssetManagerException(e); } // save manifest to element store // this is done at the end after the media package element ids have been rewritten to neutral URNs storeManifest(pmp, version); return snapshotDto; } private final Fx<MediaPackageElement> addChecksum = new Fx<MediaPackageElement>() { @Override public void apply(MediaPackageElement mpe) { try { logger.trace("Calculate checksum for " + mpe.getURI()); mpe.setChecksum(Checksum.create(ChecksumType.DEFAULT_TYPE, getFileFromWorkspace(mpe.getURI()))); } catch (IOException e) { throw new AssetManagerException(format("Cannot calculate checksum for media package element %s", mpe.getURI()), e); } } }; /** * Store all elements of <code>pmp</code> under the given version. */ private void storeAssets(final PartialMediaPackage pmp, final Version version) throws Exception { final String mpId = pmp.getMediaPackage().getIdentifier().toString(); final String orgId = getCurrentOrgId(); for (final MediaPackageElement e : pmp.getElements()) { logger.debug(format("Archiving %s %s %s", e.getFlavor(), e.getMimeType(), e.getURI())); final StoragePath storagePath = StoragePath.mk(orgId, mpId, version, e.getIdentifier()); final Opt<StoragePath> existingAssetOpt = findAssetInVersions(e.getChecksum().toString()); if (existingAssetOpt.isSome()) { final StoragePath existingAsset = existingAssetOpt.get(); logger.debug("Content of asset {} with checksum {} has been archived before", existingAsset.getMediaPackageElementId(), e.getChecksum()); if (!getAssetStore().copy(existingAsset, storagePath)) { throw new AssetManagerException( format("An asset with checksum %s has already been archived but trying to copy or link asset %s to it failed", e.getChecksum(), existingAsset)); } } else { final Opt<Long> size = e.getSize() > 0 ? Opt.some(e.getSize()) : Opt.<Long>none(); getAssetStore().put(storagePath, Source.mk(e.getURI(), size, Opt.nul(e.getMimeType()))); } } } /** Check if element <code>e</code> is already part of the history. */ private Opt<StoragePath> findAssetInVersions(final String checksum) throws Exception { return getDb().findAssetByChecksum(checksum).map(new Fn<AssetDtos.Full, StoragePath>() { @Override public StoragePath apply(AssetDtos.Full dto) { return StoragePath.mk(dto.getOrganizationId(), dto.getMediaPackageId(), dto.getVersion(), dto.getAssetDto().getMediaPackageElementId()); } }); } /** * Get a file from the workspace. * * @throws AssetManagerException * in case of any error */ private File getFileFromWorkspace(URI uri) { try { return getWorkspace().get(uri); } catch (NotFoundException e) { throw new AssetManagerException(format("Cannot find file at URI %s", uri), e); } catch (IOException e) { throw new AssetManagerException(format("Cannot access file at URI %s", uri), e); } } private void storeManifest(final PartialMediaPackage pmp, final Version version) throws Exception { final String mpId = pmp.getMediaPackage().getIdentifier().toString(); final String orgId = getCurrentOrgId(); // store the manifest.xml // TODO make use of checksums logger.debug(format("Archiving manifest of media package %s", mpId)); // temporarily save the manifest XML into the workspace to final String manifestFileName = format("manifest_%s.xml", pmp.getMediaPackage().getIdentifier().toString()); final URI manifestTmpUri = getWorkspace().putInCollection( "archive", manifestFileName, IOUtils.toInputStream(MediaPackageParser.getAsXml(pmp.getMediaPackage()), "UTF-8")); try { getAssetStore().put( StoragePath.mk(orgId, mpId, version, manifestAssetId(pmp, "manifest")), Source.mk(manifestTmpUri, Opt.<Long>none(), Opt.some(MimeTypes.XML))); } finally { // make sure to clean up the temporary file getWorkspace().deleteFromCollection("archive", manifestFileName); } } /** * Create a unique id for the manifest xml. This is to avoid an id collision in the rare case that the media package * contains an XML element with the id used for the manifest. A UUID could also be used but this is far less readable. * * @param seedId * the id to start with */ private String manifestAssetId(PartialMediaPackage pmp, String seedId) { if ($(pmp.getElements()).map(getMediaPackageElementId.toFn()).exists(Booleans.eq(seedId))) { return manifestAssetId(pmp, seedId + "_"); } else { return seedId; } } /* ---------------------------------------------------------------------------------------------------------------- */ /** * Unify exception handling by wrapping any occurring exception in an * {@link AssetManagerException}. */ static <A> A handleException(final P1<A> p) throws AssetManagerException { try { return p.get1(); } catch (Exception e) { logger.error("An error occurred", e); throw unwrapExceptionUntil(AssetManagerException.class, e).getOr(new AssetManagerException(e)); } } /** * Walk up the stacktrace to find a cause of type <code>type</code>. Return none if no such * type can be found. */ static <A extends Throwable> Opt<A> unwrapExceptionUntil(Class<A> type, Throwable e) { if (e == null) { return Opt.none(); } else if (type.isAssignableFrom(e.getClass())) { return Opt.some((A) e); } else { return unwrapExceptionUntil(type, e.getCause()); } } /** * Return a partial media package filtering assets. Assets are elements the archive is going to manager, i.e. all * non-publication elements. */ static PartialMediaPackage assetsOnly(MediaPackage mp) { return PartialMediaPackage.mk(mp, isAsset); } static final Pred<MediaPackageElement> isAsset = Pred.mk(isNotPublication.toFn()); /** * Create a URN for a media package element of a certain version. * Use this URN for the archived media package. */ static Fn<MediaPackageElement, URI> createUrn(final String mpId, final Version version) { return new Fn<MediaPackageElement, URI>() { @Override public URI apply(MediaPackageElement mpe) { try { String fileName = getFileName(mpe).getOr("unknown"); return new URI("urn", "matterhorn:" + mpId + ":" + version + ":" + mpe.getIdentifier() + ":" + fileName, null); } catch (URISyntaxException e) { throw new AssetManagerException(e); } } }; } /** * Extract the file name from a media package elements URN. * * @return the file name or none if it could not be determined */ public static Opt<String> getFileNameFromUrn(MediaPackageElement mpe) { Opt<URI> uri = Opt.nul(mpe.getURI()); if (uri.isSome() && "urn".equals(uri.get().getScheme())) return uri.toStream().map(toString).bind(Strings.split(":")).drop(1).reverse().head(); return Opt.none(); } private static final Fn<URI, String> toString = new Fn<URI, String>() { @Override public String apply(URI uri) { return uri.toString(); } }; /** * Rewrite URIs of assets of media package elements. Please note that this method modifies the given media package. */ static void rewriteUrisForArchival(PartialMediaPackage pmp, Version version) { rewriteUris(pmp, createUrn(pmp.getMediaPackage().getIdentifier().toString(), version)); } /** * Rewrite URIs of all asset elements of a media package. * Please note that this method modifies the given media package. */ static void rewriteUris(PartialMediaPackage pmp, Fn<MediaPackageElement, URI> uriCreator) { for (MediaPackageElement mpe : pmp.getElements()) { mpe.setURI(uriCreator.apply(mpe)); } } /* ------------------------------------------------------------------------------------------------------------------ */ /** * Rewrite URIs of all asset elements of a snapshot's media package. * This method does not mutate anything. */ public static Snapshot rewriteUris(Snapshot snapshot, Fn<MediaPackageElement, URI> uriCreator) { final MediaPackage mpCopy = MediaPackageSupport.copy(snapshot.getMediaPackage()); for (final MediaPackageElement mpe : assetsOnly(mpCopy).getElements()) { mpe.setURI(uriCreator.apply(mpe)); } return new SnapshotImpl( snapshot.getVersion(), snapshot.getOrganizationId(), snapshot.getArchivalDate(), snapshot.getAvailability(), snapshot.getOwner(), mpCopy); } }