/**
* 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.util.data.Option.option;
import org.opencastproject.util.data.Function;
import org.opencastproject.util.data.Option;
import com.entwinemedia.fn.data.Opt;
import org.joda.time.Duration;
import org.joda.time.format.ISODateTimeFormat;
import org.joda.time.format.ISOPeriodFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility class to facilitate the work with DCMI encoding schemes.
*/
public final class EncodingSchemeUtils {
private static final Map<Precision, String> formats = new HashMap<Precision, String>();
static {
formats.put(Precision.Year, "yyyy");
formats.put(Precision.Month, "yyyy-MM");
formats.put(Precision.Day, "yyyy-MM-dd");
formats.put(Precision.Minute, "yyyy-MM-dd'T'HH:mm'Z'");
formats.put(Precision.Second, "yyyy-MM-dd'T'HH:mm:ss'Z'");
formats.put(Precision.Fraction, "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
}
/** Disable construction of this utility class */
private EncodingSchemeUtils() {
}
/**
* Encode a date with the given precision into a Dublin Core string value, using the recommended W3C-DTF scheme. The
* UTC timezone is used for all precisions from {@link Precision#Minute} to {@link Precision#Fraction}. For years,
* months and days the local timezone is used instead to ensure that the given date enters the DublinCore as is. If
* UTC was used it may happen that you get the previous or next day, month or year respectively
* <p>
* The language of the returned value is {@link DublinCore#LANGUAGE_UNDEFINED}.
* <p>
* See <a href="http://www.w3.org/TR/NOTE-datetime">http://www.w3.org/TR/NOTE-datetime</a> for more information about
* W3C-DTF.
*
* @param date
* the date to encode
* @param precision
* the precision to use
*/
public static DublinCoreValue encodeDate(Date date, Precision precision) {
if (date == null)
throw new IllegalArgumentException("The date must not be null");
if (precision == null)
throw new IllegalArgumentException("The precision must not be null");
return DublinCoreValue.mk(formatDate(date, precision), DublinCore.LANGUAGE_UNDEFINED, Opt.some(DublinCore.ENC_SCHEME_W3CDTF));
}
public static String formatDate(Date date, Precision precision) {
SimpleDateFormat f = new SimpleDateFormat(formats.get(precision));
if (precision == Precision.Minute || precision == Precision.Second || precision == Precision.Fraction)
f.setTimeZone(TimeZone.getTimeZone("UTC"));
return f.format(date);
}
/**
* Encode a period with the given precision into a Dublin Core string value using the recommended DCMI Period scheme.
* For the usage of the UTC timezone please refer to {@link #encodeDate(Date, Precision)} for further information.
* <p>
* One of the dates may be null to create an open interval.
* <p>
* The language of the returned value is {@link DublinCore#LANGUAGE_UNDEFINED}.
* <p>
* See <a href="http://dublincore.org/documents/dcmi-period/">http://dublincore.org/documents/dcmi-period/</a> for
* more information about DCMI Period.
*
* @param period
* the period
* @param precision
* the precision
*/
public static DublinCoreValue encodePeriod(DCMIPeriod period, Precision precision) {
if (period == null)
throw new IllegalArgumentException("The period must not be null");
if (precision == null)
throw new IllegalArgumentException("The precision must not be null");
StringBuilder b = new StringBuilder();
if (period.hasStart()) {
b.append("start=").append(formatDate(period.getStart(), precision)).append(";");
}
if (period.hasEnd()) {
if (b.length() > 0)
b.append(" ");
b.append("end=").append(formatDate(period.getEnd(), precision)).append(";");
}
if (period.hasName()) {
b.append(" ").append("name=").append(period.getName().replace(";", "")).append(";");
}
b.append(" ").append("scheme=W3C-DTF;");
return DublinCoreValue.mk(b.toString(), DublinCore.LANGUAGE_UNDEFINED, Opt.some(DublinCore.ENC_SCHEME_PERIOD));
}
/**
* Encode a duration measured in milliseconds into a Dublin Core string using the
* {@link DublinCore#ENC_SCHEME_ISO8601} encoding scheme <code>PTnHnMnS</code>.
* <p>
* The language of the returned value is {@link DublinCore#LANGUAGE_UNDEFINED}.
* <p>
* See <a href="http://en.wikipedia.org/wiki/ISO_8601#Durations"> ISO8601 Durations</a> for details.
*
* @param duration
* the duration in milliseconds
*/
public static DublinCoreValue encodeDuration(long duration) {
return DublinCoreValue.mk(ISOPeriodFormat.standard().print(new Duration(duration).toPeriod()),
DublinCore.LANGUAGE_UNDEFINED, Opt.some(DublinCore.ENC_SCHEME_ISO8601));
}
/**
* Decode a string encoded in the ISO8601 encoding scheme.
* <p>
* Also supports the REPLAY legacy format <code>hh:mm:ss</code>.
* <p>
* See <a href="http://en.wikipedia.org/wiki/ISO_8601#Durations"> ISO8601 Durations</a> for details.
*
* @param value
* the ISO encoded string
* @return the duration in milliseconds or null, if the value cannot be parsed
*/
public static Long decodeDuration(String value) {
try {
return ISOPeriodFormat.standard().parsePeriod(value).toStandardDuration().getMillis();
} catch (IllegalArgumentException ignore) {
}
// also support the legacy format hh:mm:ss
String[] parts = value.split(":");
try {
if (parts.length == 1)
return Long.parseLong(parts[0]) * 1000;
if (parts.length == 2)
return Long.parseLong(parts[0]) * 1000 * 60 + Long.parseLong(parts[1]) * 1000;
if (parts.length == 3)
return Long.parseLong(parts[0]) * 1000 * 60 * 60 + Long.parseLong(parts[1]) * 1000 * 60
+ Long.parseLong(parts[2]) * 1000;
} catch (NumberFormatException ignore) {
}
return null;
}
/**
* Decode a string encoded in the ISO8601 encoding scheme.
*
* @param value
* the Dublin Core value
* @return the duration in milliseconds or null, if the value cannot be parsed or is in a different encoding scheme
*/
public static Long decodeDuration(DublinCoreValue value) {
if (!value.hasEncodingScheme() || value.getEncodingScheme().get().equals(DublinCore.ENC_SCHEME_ISO8601)) {
return decodeDuration(value.getValue());
}
return null;
}
public static Long decodeMandatoryDuration(DublinCoreValue value) {
Long l = decodeDuration(value);
if (l == null)
throw new IllegalArgumentException("Cannot decode duration: " + value);
return l;
}
public static Long decodeMandatoryDuration(String value) {
Long l = decodeDuration(value);
if (l == null)
throw new IllegalArgumentException("Cannot decode duration: " + value);
return l;
}
/**
* Tries to decode the given value as a W3C-DTF encoded date. If decoding fails, null is returned.
*
* @return the date or null if decoding fails
*/
public static Date decodeDate(DublinCoreValue value) {
if (!value.hasEncodingScheme() || value.getEncodingScheme().get().equals(DublinCore.ENC_SCHEME_W3CDTF)) {
try {
return parseW3CDTF(value.getValue());
} catch (IllegalArgumentException ignore) {
}
}
// Try unixtime in milliseconds (backwards-compatibility with older mediapackages)
try {
long timestamp = Long.parseLong(value.getValue());
Date decoded = new java.util.Date(timestamp);
return decoded;
} catch (NumberFormatException nfe) {
}
return null;
}
/** {@link #decodeDate(org.opencastproject.metadata.dublincore.DublinCoreValue)} as a function. */
public static final Function<DublinCoreValue, Option<Date>> dcValueToDate = new Function<DublinCoreValue, Option<Date>>() {
@Override public Option<Date> apply(DublinCoreValue dublinCoreValue) {
return option(decodeDate(dublinCoreValue));
}
};
/**
* Tries to decode the given value as a W3C-DTF encoded date. If decoding fails, null is returned.
*
* @return the date or null if decoding fails
*/
public static Date decodeDate(String value) {
try {
return parseW3CDTF(value);
} catch (IllegalArgumentException ignore) {
}
// Try unixtime in milliseconds (backwards-compatibility with older mediapackages)
try {
long timestamp = Long.parseLong(value);
Date decoded = new java.util.Date(timestamp);
return decoded;
} catch (NumberFormatException nfe) {
}
return null;
}
/** {@link #decodeDate(String)} as a function. */
public static final Function<String, Option<Date>> stringToDate = new Function<String, Option<Date>>() {
@Override public Option<Date> apply(String s) {
return option(decodeDate(s));
}
};
/**
* Like {@link #decodeDate(String)}, but throws an {@link IllegalArgumentException} if the value cannot be decoded.
*
* @param value
* the value
* @return the date
* @throws IllegalArgumentException
* if the value cannot be decoded
*/
public static Date decodeMandatoryDate(DublinCoreValue value) {
Date date = decodeDate(value);
if (date == null)
throw new IllegalArgumentException("Cannot decode to Date: " + value);
return date;
}
/**
* Like {@link #decodeDate(String)}, but throws an {@link IllegalArgumentException} if the value cannot be decoded.
*
* @return the date
* @throws IllegalArgumentException
* if the value cannot be decoded
*/
public static Date decodeMandatoryDate(String value) {
Date date = decodeDate(value);
if (date == null)
throw new IllegalArgumentException("Cannot decode to Date: " + value);
return date;
}
private static final Pattern DCMI_PERIOD = Pattern.compile("(start|end|name)\\s*=\\s*(.*?)(?:;|\\s*$)");
private static final Pattern DCMI_PERIOD_SCHEME = Pattern.compile("scheme\\s*=\\s*(.*?)(?:;|\\s*$)");
/**
* Tries to decode a string in the DCMI period format, using W3C-DTF for the encoding of the individual dates. If
* parsing fails at any point, null will be returned.
*
* @return the period or null if decoding fails
*/
public static DCMIPeriod decodePeriod(DublinCoreValue value) {
return decodePeriod(value.getValue());
}
/**
* Tries to decode a string in the DCMI period format, using W3C-DTF for the encoding of the individual dates. If
* parsing fails at any point, null will be returned.
*
* @return the period or null if decoding fails
*/
public static DCMIPeriod decodePeriod(String value) {
// Parse value
Matcher schemeMatcher = DCMI_PERIOD_SCHEME.matcher(value);
boolean mayBeW3CDTFEncoded = true;
if (schemeMatcher.find()) {
String schemeString = schemeMatcher.group(1);
if (!"W3C-DTF".equalsIgnoreCase(schemeString) && !"W3CDTF".equalsIgnoreCase(schemeString)) {
mayBeW3CDTFEncoded = false;
}
}
try {
if (mayBeW3CDTFEncoded) {
// Declare fields
Date start = null;
Date end = null;
String name = null;
// Parse
Matcher m = DCMI_PERIOD.matcher(value);
while (m.find()) {
String field = m.group(1);
String fieldValue = m.group(2);
if ("start".equals(field)) {
if (start != null)
return null;
start = parseW3CDTF(fieldValue);
} else if ("end".equals(field)) {
if (end != null)
return null;
end = parseW3CDTF(fieldValue);
} else if ("name".equals(field)) {
if (name != null)
return null;
name = fieldValue;
}
}
if (start == null && end == null)
return null;
return new DCMIPeriod(start, end, name);
}
} catch (IllegalArgumentException ignore) {
// Parse error
}
return null;
}
/**
* Like {@link #decodePeriod(String)}, but throws an {@link IllegalArgumentException} if the value cannot be decoded.
*
* @return the period
* @throws IllegalArgumentException
* if the value cannot be decoded
*/
public static DCMIPeriod decodeMandatoryPeriod(DublinCoreValue value) {
return decodeMandatoryPeriod(value.getValue());
}
/**
* Like {@link #decodePeriod(DublinCoreValue)}, but throws an {@link IllegalArgumentException} if the value cannot be
* decoded.
*
* @return the period
* @throws IllegalArgumentException
* if the value cannot be decoded
*/
public static DCMIPeriod decodeMandatoryPeriod(String value) {
DCMIPeriod period = decodePeriod(value);
if (period == null)
throw new IllegalArgumentException("Cannot decode to DCMIPeriod: " + value);
return period;
}
/**
* Tries to decode the value to a temporal object. For now, supported types are {@link java.util.Date},
* {@link DCMIPeriod} and Long for a duration.
*
* @param value
* the value to decode
* @return a temporal object of the said types or null if decoding fails
*/
public static Temporal decodeTemporal(DublinCoreValue value) {
// First try Date
Date instant = decodeDate(value);
if (instant != null)
return Temporal.instant(instant);
DCMIPeriod period = decodePeriod(value);
if (period != null)
return Temporal.period(period);
Long duration = decodeDuration(value);
if (duration != null)
return Temporal.duration(duration);
return null;
}
/**
* Like {@link #decodeTemporal(DublinCoreValue)}, but throws an {@link IllegalArgumentException} if the value cannot
* be decoded.
*
* @return the temporal object of type {@link java.util.Date} or {@link DCMIPeriod}
* @throws IllegalArgumentException
* if the value cannot be decoded
*/
public static Temporal decodeMandatoryTemporal(DublinCoreValue value) {
Temporal temporal = decodeTemporal(value);
if (value == null)
throw new IllegalArgumentException("Cannot decode to either Date or DCMIPeriod: " + value);
return temporal;
}
/**
* @throws IllegalArgumentException
* if the value cannot be parsed
*/
private static Date parseW3CDTF(String value) {
return ISODateTimeFormat.dateTimeParser().parseDateTime(value).toDate();
}
}