/**
* 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.metadata.dublincore;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_ACCESS_RIGHTS;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_AVAILABLE;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_CONTRIBUTOR;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_CREATED;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_CREATOR;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_DESCRIPTION;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_EXTENT;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_IDENTIFIER;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_IS_PART_OF;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_LANGUAGE;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_LICENSE;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_PUBLISHER;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_REPLACES;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_RIGHTS_HOLDER;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_SPATIAL;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_SUBJECT;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_TEMPORAL;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_TITLE;
import static org.opencastproject.metadata.dublincore.DublinCore.PROPERTY_TYPE;
import static org.opencastproject.util.data.Collections.head;
import static org.opencastproject.util.data.Collections.list;
import static org.opencastproject.util.data.Monadics.mlist;
import static org.opencastproject.util.data.Option.option;
import static org.opencastproject.util.data.Option.some;
import org.opencastproject.mediapackage.Catalog;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.MediaPackageElements;
import org.opencastproject.metadata.api.MetadataValue;
import org.opencastproject.metadata.api.StaticMetadata;
import org.opencastproject.metadata.api.StaticMetadataService;
import org.opencastproject.metadata.api.util.Interval;
import org.opencastproject.util.data.Function;
import org.opencastproject.util.data.NonEmptyList;
import org.opencastproject.util.data.Option;
import org.opencastproject.util.data.Predicate;
import org.opencastproject.util.data.functions.Misc;
import org.opencastproject.workspace.api.Workspace;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* This service provides {@link org.opencastproject.metadata.api.StaticMetadata} for a given mediapackage,
* based on a contained dublin core catalog describing the episode.
*/
public class StaticMetadataServiceDublinCoreImpl implements StaticMetadataService {
private static final Logger logger = LoggerFactory.getLogger(StaticMetadataServiceDublinCoreImpl.class);
// Catalog loader function
private Function<Catalog, Option<DublinCoreCatalog>> loader = new Function<Catalog, Option<DublinCoreCatalog>>() {
@Override
public Option<DublinCoreCatalog> apply(Catalog catalog) {
return load(catalog);
}
};
protected int priority = 0;
protected Workspace workspace = null;
public void setWorkspace(Workspace workspace) {
this.workspace = workspace;
}
public void activate(@SuppressWarnings("rawtypes") Map properties) {
logger.debug("activate()");
if (properties != null) {
String priorityString = (String) properties.get(PRIORITY_KEY);
if (priorityString != null) {
try {
priority = Integer.parseInt(priorityString);
} catch (NumberFormatException e) {
logger.warn("Unable to set priority to {}", priorityString);
throw e;
}
}
}
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.metadata.api.MetadataService#getMetadata(org.opencastproject.mediapackage.MediaPackage)
*/
@Override
public StaticMetadata getMetadata(final MediaPackage mp) {
return mlist(list(mp.getCatalogs(DublinCoreCatalog.ANY_DUBLINCORE)))
.find(flavorPredicate(MediaPackageElements.EPISODE))
.flatMap(loader)
.map(new Function<DublinCoreCatalog, StaticMetadata>() {
@Override
public StaticMetadata apply(DublinCoreCatalog episode) {
return newStaticMetadataFromEpisode(episode);
}
})
.getOrElse((StaticMetadata) null);
}
private static StaticMetadata newStaticMetadataFromEpisode(DublinCoreCatalog episode) {
// Ensure that the mandatory properties are present
final Option<String> id = option(episode.getFirst(PROPERTY_IDENTIFIER));
final Option<Date> created = option(episode.getFirst(PROPERTY_CREATED)).map(new Function<String, Date>() {
@Override
public Date apply(String a) {
final Date date = EncodingSchemeUtils.decodeDate(a);
return date != null ? date : Misc.<Date>chuck(new RuntimeException(a + " does not conform to W3C-DTF encoding scheme."));
}
});
final Option temporalOpt = option(episode.getFirstVal(PROPERTY_TEMPORAL)).map(dc2temporalValueOption());
final Option<Date> start;
if (episode.getFirst(PROPERTY_TEMPORAL) != null) {
DCMIPeriod period = EncodingSchemeUtils
.decodeMandatoryPeriod(episode.getFirst(PROPERTY_TEMPORAL));
start = option(period.getStart());
} else {
start = created;
}
final Option<String> language = option(episode.getFirst(PROPERTY_LANGUAGE));
final Option<Long> extent = head(episode.get(PROPERTY_EXTENT)).map(new Function<DublinCoreValue, Long>() {
@Override
public Long apply(DublinCoreValue a) {
final Long extent = EncodingSchemeUtils.decodeDuration(a);
return extent != null ? extent : Misc.<Long>chuck(new RuntimeException(a + " does not conform to ISO8601 encoding scheme for durations."));
}
});
final Option<String> type = option(episode.getFirst(PROPERTY_TYPE));
final Option<String> isPartOf = option(episode.getFirst(PROPERTY_IS_PART_OF));
final Option<String> replaces = option(episode.getFirst(PROPERTY_REPLACES));
final Option<Interval> available = head(episode.get(PROPERTY_AVAILABLE)).flatMap(
new Function<DublinCoreValue, Option<Interval>>() {
@Override
public Option<Interval> apply(DublinCoreValue v) {
final DCMIPeriod p = EncodingSchemeUtils.decodePeriod(v);
return p != null
? some(Interval.fromValues(p.getStart(), p.getEnd()))
: Misc.<Option<Interval>>chuck(new RuntimeException(v + " does not conform to W3C-DTF encoding scheme for periods"));
}
});
final NonEmptyList<MetadataValue<String>> titles = new NonEmptyList<MetadataValue<String>>(
mlist(episode.get(PROPERTY_TITLE)).map(dc2mvString(PROPERTY_TITLE.getLocalName())).value());
final List<MetadataValue<String>> subjects =
mlist(episode.get(PROPERTY_SUBJECT)).map(dc2mvString(PROPERTY_SUBJECT.getLocalName())).value();
final List<MetadataValue<String>> creators =
mlist(episode.get(PROPERTY_CREATOR)).map(dc2mvString(PROPERTY_CREATOR.getLocalName())).value();
final List<MetadataValue<String>> publishers =
mlist(episode.get(PROPERTY_PUBLISHER)).map(dc2mvString(PROPERTY_PUBLISHER.getLocalName())).value();
final List<MetadataValue<String>> contributors =
mlist(episode.get(PROPERTY_CONTRIBUTOR)).map(dc2mvString(PROPERTY_CONTRIBUTOR.getLocalName())).value();
final List<MetadataValue<String>> description =
mlist(episode.get(PROPERTY_DESCRIPTION)).map(dc2mvString(PROPERTY_DESCRIPTION.getLocalName())).value();
final List<MetadataValue<String>> rightsHolders =
mlist(episode.get(PROPERTY_RIGHTS_HOLDER)).map(dc2mvString(PROPERTY_RIGHTS_HOLDER.getLocalName())).value();
final List<MetadataValue<String>> spatials =
mlist(episode.get(PROPERTY_SPATIAL)).map(dc2mvString(PROPERTY_SPATIAL.getLocalName())).value();
final List<MetadataValue<String>> accessRights =
mlist(episode.get(PROPERTY_ACCESS_RIGHTS)).map(dc2mvString(PROPERTY_ACCESS_RIGHTS.getLocalName())).value();
final List<MetadataValue<String>> licenses =
mlist(episode.get(PROPERTY_LICENSE)).map(dc2mvString(PROPERTY_LICENSE.getLocalName())).value();
return new StaticMetadata() {
@Override
public Option<String> getId() {
return id;
}
@Override
public Option<Date> getCreated() {
// Compatibility patch with SOLR search service, where DC_CREATED stores the time the recording has been recorded
// in Admin UI and external API this is stored in DC_TEMPORAL as a DC Period. DC_CREATED is the date on which the
// event was created there.
// Admin UI and External UI do not use this Class, that only seems to be used by the old SOLR modules,
// so data will be kept correctly there, and only be "exported" to DC_CREATED for compatibility reasons here.
return start;
}
@Override
public Option<Date[]> getTemporalPeriod() {
if (temporalOpt.isSome()) {
if (temporalOpt.get() instanceof DCMIPeriod) {
DCMIPeriod p = (DCMIPeriod) temporalOpt.get();
return option(new Date[] { p.getStart(), p.getEnd() });
}
}
return Option.none();
}
@Override
public Option<Date> getTemporalInstant() {
if (temporalOpt.isSome()) {
if (temporalOpt.get() instanceof Date) {
return temporalOpt;
}
}
return Option.none();
}
@Override
public Option<Long> getTemporalDuration() {
if (temporalOpt.isSome()) {
if (temporalOpt.get() instanceof Long) {
return temporalOpt;
}
}
return Option.none();
}
@Override
public Option<Long> getExtent() {
return extent;
}
@Override
public Option<String> getLanguage() {
return language;
}
@Override
public Option<String> getIsPartOf() {
return isPartOf;
}
@Override
public Option<String> getReplaces() {
return replaces;
}
@Override
public Option<String> getType() {
return type;
}
@Override
public Option<Interval> getAvailable() {
return available;
}
@Override
public NonEmptyList<MetadataValue<String>> getTitles() {
return titles;
}
@Override
public List<MetadataValue<String>> getSubjects() {
return subjects;
}
@Override
public List<MetadataValue<String>> getCreators() {
return creators;
}
@Override
public List<MetadataValue<String>> getPublishers() {
return publishers;
}
@Override
public List<MetadataValue<String>> getContributors() {
return contributors;
}
@Override
public List<MetadataValue<String>> getDescription() {
return description;
}
@Override
public List<MetadataValue<String>> getRightsHolders() {
return rightsHolders;
}
@Override
public List<MetadataValue<String>> getSpatials() {
return spatials;
}
@Override
public List<MetadataValue<String>> getAccessRights() {
return accessRights;
}
@Override
public List<MetadataValue<String>> getLicenses() {
return licenses;
}
};
}
/**
*
* {@inheritDoc}
*
* @see org.opencastproject.metadata.api.MetadataService#getPriority()
*/
@Override
public int getPriority() {
return priority;
}
/**
* Return a function that creates a Option with the value of temporal from a DublinCoreValue.
*/
private static Function<DublinCoreValue, Object> dc2temporalValueOption() {
return new Function<DublinCoreValue, Object>() {
@Override
public Object apply(DublinCoreValue dcv) {
Temporal temporal = EncodingSchemeUtils.decodeTemporal(dcv);
if (temporal != null) {
return temporal.fold(new Temporal.Match<Object>() {
@Override
public Object period(DCMIPeriod period) {
return period;
}
@Override
public Object instant(Date instant) {
return instant;
}
@Override
public Object duration(long duration) {
return duration;
}
});
}
return Misc.<Object>chuck(new RuntimeException(dcv
+ " does not conform to ISO8601 encoding scheme for temporal."));
}
};
}
/**
* Return a function that creates a MetadataValue[String] from a DublinCoreValue setting its name to <code>name</code>.
*/
private static Function<DublinCoreValue, MetadataValue<String>> dc2mvString(final String name) {
return new Function<DublinCoreValue, MetadataValue<String>>() {
@Override
public MetadataValue<String> apply(DublinCoreValue dcv) {
return new MetadataValue<String>(dcv.getValue(), name, dcv.getLanguage());
}
};
}
private static Predicate<Catalog> flavorPredicate(final MediaPackageElementFlavor flavor) {
return new Predicate<Catalog>() {
@Override
public Boolean apply(Catalog catalog) {
return flavor.equals(catalog.getFlavor());
}
};
}
private Option<DublinCoreCatalog> load(Catalog catalog) {
InputStream in = null;
try {
File f = workspace.get(catalog.getURI());
in = new FileInputStream(f);
return some((DublinCoreCatalog) DublinCores.read(in));
} catch (Exception e) {
logger.warn("Unable to load metadata from catalog '{}'", catalog);
return Option.none();
} finally {
IOUtils.closeQuietly(in);
}
}
}