/** * 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 com.entwinemedia.fn.data.json.Jsons.arr; import static com.entwinemedia.fn.data.json.Jsons.f; import static com.entwinemedia.fn.data.json.Jsons.obj; import static com.entwinemedia.fn.data.json.Jsons.v; import static org.apache.commons.lang3.exception.ExceptionUtils.getMessage; import static org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace; import com.entwinemedia.fn.Fn; import com.entwinemedia.fn.data.Opt; import com.entwinemedia.fn.data.json.Field; import com.entwinemedia.fn.data.json.JObject; import com.entwinemedia.fn.data.json.JValue; import com.entwinemedia.fn.data.json.Jsons; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.time.DurationFormatUtils; import org.json.simple.JSONArray; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TimeZone; /** * This is a generic and very abstract view of a certain field/property in a metadata catalog. The main purpose of this * class is to have a generic access to the variety of information stored in metadata catalogs. * * @param <A> * Defines the type of the metadata value */ public class MetadataField<A> { private static final Logger logger = LoggerFactory.getLogger(MetadataField.class); public static final String PATTERN_DURATION = "HH:mm:ss"; /** Keys for the different values in the configuration file */ public static final String CONFIG_COLLECTION_ID_KEY = "collectionID"; public static final String CONFIG_PATTERN_KEY = "pattern"; public static final String CONFIG_END_DATE_OUTPUT_KEY = "endDateOutputID"; public static final String CONFIG_END_TIME_OUTPUT_KEY = "endTimeOutputID"; public static final String CONFIG_INPUT_ID_KEY = "inputID"; public static final String CONFIG_LABEL_KEY = "label"; public static final String CONFIG_LIST_PROVIDER_KEY = "listprovider"; public static final String CONFIG_NAMESPACE_KEY = "namespace"; public static final String CONFIG_ORDER_KEY = "order"; public static final String CONFIG_OUTPUT_ID_KEY = "outputID"; public static final String CONFIG_PROPERTY_PREFIX = "property"; public static final String CONFIG_READ_ONLY_KEY = "readOnly"; public static final String CONFIG_REQUIRED_KEY = "required"; public static final String CONFIG_START_DATE_OUTPUT_KEY = "startDateOutputID"; public static final String CONFIG_START_TIME_OUTPUT_KEY = "startTimeOutputID"; public static final String CONFIG_TYPE_KEY = "type"; /* Keys for the different properties of the metadata JSON Object */ protected static final String JSON_KEY_ID = "id"; protected static final String JSON_KEY_LABEL = "label"; protected static final String JSON_KEY_READONLY = "readOnly"; protected static final String JSON_KEY_REQUIRED = "required"; protected static final String JSON_KEY_TYPE = "type"; protected static final String JSON_KEY_VALUE = "value"; protected static final String JSON_KEY_COLLECTION = "collection"; /** Labels for the temporal date fields */ private static final String LABEL_METADATA_PREFIX = "EVENTS.EVENTS.DETAILS.METADATA."; private static final String LABEL_METADATA_END_DATE = LABEL_METADATA_PREFIX + "END_DATE"; private static final String LABEL_METADATA_END_TIME = LABEL_METADATA_PREFIX + "END_TIME"; private static final String LABEL_METADATA_DURATION = LABEL_METADATA_PREFIX + "DURATION"; private static final String LABEL_METADATA_START_DATE = LABEL_METADATA_PREFIX + "START_DATE"; private static final String LABEL_METADATA_START_TIME = LABEL_METADATA_PREFIX + "START_TIME"; /** * Possible types for the metadata field. The types are used in the frontend and backend to know how the metadata * fields should be formatted (if needed). */ public enum Type { BOOLEAN, DATE, DURATION, ITERABLE_TEXT, MIXED_TEXT, LONG, START_DATE, START_TIME, TEXT, TEXT_LONG } public enum JsonType { BOOLEAN, DATE, NUMBER, TEXT, MIXED_TEXT, TEXT_LONG, TIME } /** A parser for handling JSON values that are strings. **/ public static final JSONParser parser = new JSONParser(); /** The id of a collection to validate values against. */ private Opt<String> collectionID = Opt.none(); /** The format to use for temporal date properties. */ private Opt<String> pattern = Opt.none(); /** The id of the field used to identify it in the dublin core. */ private String inputID; /** The i18n id for the label to show the property. */ private String label; /** The provider to populate the property with. */ private Opt<String> listprovider = Opt.none(); /** The optional namespace of the field used if a field can be found in more than one namespace */ private Opt<String> namespace = Opt.some(DublinCore.TERMS_NS_URI); /** * In the order of properties where this property should be oriented in the UI i.e. 0 means the property should come * first, 1 means it should come second etc. */ private Opt<Integer> order = Opt.none(); /** The optional id of the field used to output for the ui, if not present will assume the same as the inputID. */ private Opt<String> outputID = Opt.none(); /** Whether the property should not be edited. */ private boolean readOnly; /** Whether the property is required to update the metadata. */ private boolean required; /** The type of the metadata for example text, date etc. */ private Type type; /** The type of the metadata for the json to use example text, date, time, number etc. */ private JsonType jsonType; private Opt<A> value = Opt.none(); private boolean updated = false; private Opt<Map<String, String>> collection = Opt.none(); private Fn<Opt<A>, JValue> valueToJSON; private Fn<Object, A> jsonToValue; private Opt<String> durationOutputID = Opt.none(); public MetadataField() { } /** * Metadata field constructor * * @param inputID * The identifier of the new metadata field * @param label * the label of the field. The string displayed next to the field value on the frontend. This is usually be a * translation key * @param readOnly * Define if the new metadata field can be or not edited * @param required * Define if the new metadata field is or not required * @param value * The metadata field value * @param type * The metadata field type @ EventMetadata.Type} * @param collection * If the field has a limited list of possible value, the option should contain this one. Otherwise it should * be none. This is also possible to use the collectionId parameter for that. * @param collectionID * The id of the limit list of possible value that should be get through the resource endpoint. * @param valueToJSON * Function to format the metadata field value to a JSON value. * @param jsonToValue * Function to parse the JSON value of the metadata field. * @throws IllegalArgumentException * if the id, label, type, valueToJSON or/and jsonToValue parameters is/are null */ private MetadataField(String inputID, Opt<String> outputID, String label, boolean readOnly, boolean required, A value, Type type, JsonType jsonType, Opt<Map<String, String>> collection, Opt<String> collectionID, Fn<Opt<A>, JValue> valueToJSON, Fn<Object, A> jsonToValue, Opt<Integer> order, Opt<String> namespace) throws IllegalArgumentException { if (valueToJSON == null) throw new IllegalArgumentException("The function 'valueToJSON' must not be null."); if (jsonToValue == null) throw new IllegalArgumentException("The function 'jsonToValue' must not be null."); if (StringUtils.isBlank(inputID)) throw new IllegalArgumentException("The metadata input id must not be null."); if (StringUtils.isBlank(label)) throw new IllegalArgumentException("The metadata label must not be null."); if (type == null) throw new IllegalArgumentException("The metadata type must not be null."); this.inputID = inputID; this.outputID = outputID; this.label = label; this.readOnly = readOnly; this.required = required; if (value == null) this.value = Opt.none(); else this.value = Opt.some(value); this.type = type; this.jsonType = jsonType; this.collection = collection; this.collectionID = collectionID; this.valueToJSON = valueToJSON; this.jsonToValue = jsonToValue; this.order = order; this.namespace = namespace; } /** * Set the option of a limited list of possible values. * * @param collection * The option of a limited list of possible values */ public void setCollection(Opt<Map<String, String>> collection) { if (collection == null) this.collection = Opt.none(); else { this.collection = collection; } } public JObject toJSON() { Map<String, Field> values = new HashMap<>(); values.put(JSON_KEY_ID, f(JSON_KEY_ID, v(getOutputID(), Jsons.BLANK))); values.put(JSON_KEY_LABEL, f(JSON_KEY_LABEL, v(label, Jsons.BLANK))); values.put(JSON_KEY_VALUE, f(JSON_KEY_VALUE, valueToJSON.apply(value))); values.put(JSON_KEY_TYPE, f(JSON_KEY_TYPE, v(jsonType.toString().toLowerCase(), Jsons.BLANK))); values.put(JSON_KEY_READONLY, f(JSON_KEY_READONLY, v(readOnly))); values.put(JSON_KEY_REQUIRED, f(JSON_KEY_REQUIRED, v(required))); if (collection.isSome()) values.put(JSON_KEY_COLLECTION, f(JSON_KEY_COLLECTION, mapToJSON(collection.get()))); else if (collectionID.isSome()) values.put(JSON_KEY_COLLECTION, f(JSON_KEY_COLLECTION, v(collectionID.get()))); return obj(values); } public void fromJSON(Object json) { this.setValue(jsonToValue.apply(json)); } public Opt<Map<String, String>> getCollection() { return collection; } public Opt<A> getValue() { return value; } public boolean isUpdated() { return updated; } public void setValue(A value) { if (value == null) this.value = Opt.none(); else { this.value = Opt.some(value); this.updated = true; } } public static SimpleDateFormat getSimpleDateFormatter(String pattern) { final SimpleDateFormat dateFormat; if (StringUtils.isNotBlank(pattern)) { dateFormat = new SimpleDateFormat(pattern); } else { dateFormat = new SimpleDateFormat(); } dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); return dateFormat; } /** * Create a metadata field based on a {@link Boolean}. * * @param inputID * The identifier of the new metadata field * @param label * The label of the new metadata field * @param readOnly * Define if the new metadata is or not a readonly field * @param required * Define if the new metadata field is or not required * @param order * The ui order for the new field, 0 at the top and progressively down from there. * @return The new metadata field * */ public static MetadataField<Boolean> createBooleanMetadata(String inputID, Opt<String> outputID, String label, boolean readOnly, boolean required, Opt<Integer> order, Opt<String> namespace) { Fn<Opt<Boolean>, JValue> booleanToJson = new Fn<Opt<Boolean>, JValue>() { @Override public JValue apply(Opt<Boolean> value) { if (value.isNone()) return Jsons.BLANK; else { return v(value.get(), Jsons.BLANK); } } }; Fn<Object, Boolean> jsonToBoolean = new Fn<Object, Boolean>() { @Override public Boolean apply(Object value) { if (value instanceof Boolean) { return (Boolean) value; } String stringValue = value.toString(); if (StringUtils.isBlank(stringValue)) { return null; } return Boolean.parseBoolean(stringValue); } }; return new MetadataField<>(inputID, outputID, label, readOnly, required, null, Type.BOOLEAN, JsonType.BOOLEAN, Opt.<Map<String, String>> none(), Opt.<String> none(), booleanToJson, jsonToBoolean, order, namespace); } /** * Creates a copy of a {@link MetadataField} and sets the value based upon a string. * * @param oldField * The field whose other values such as ids, label etc. will be copied. * @param value * The value that will be interpreted as being from a JSON value. * @return A new {@link MetadataField} with the value set */ public static MetadataField<?> copyMetadataFieldWithValue(MetadataField<?> oldField, String value) { MetadataField<?> newField = null; switch (oldField.getType()) { case BOOLEAN: MetadataField<Boolean> booleanField = MetadataField.createBooleanMetadata(oldField.getInputID(), Opt.some(oldField.getOutputID()), oldField.getLabel(), oldField.isReadOnly(), oldField.isRequired(), oldField.getOrder(), oldField.getNamespace()); booleanField.fromJSON(value); return booleanField; case DATE: MetadataField<Date> dateField = MetadataField.createDateMetadata(oldField.getInputID(), Opt.some(oldField.getOutputID()), oldField.getLabel(), oldField.isReadOnly(), oldField.isRequired(), oldField.getPattern().get(), oldField.getOrder(), oldField.getNamespace()); dateField.fromJSON(value); return dateField; case DURATION: MetadataField<String> durationField = MetadataField.createDurationMetadataField(oldField.getInputID(), Opt.some(oldField.getOutputID()), oldField.getLabel(), oldField.isReadOnly(), oldField.isRequired(), oldField.getOrder(), oldField.getNamespace()); durationField.fromJSON(value); return durationField; case ITERABLE_TEXT: MetadataField<Iterable<String>> iterableTextField = MetadataField.createIterableStringMetadataField( oldField.getInputID(), Opt.some(oldField.getOutputID()), oldField.getLabel(), oldField.isReadOnly(), oldField.isRequired(), oldField.getCollection(), oldField.getCollectionID(), oldField.getOrder(), oldField.getNamespace()); iterableTextField.fromJSON(value); return iterableTextField; case LONG: MetadataField<Long> longField = MetadataField.createLongMetadataField(oldField.getInputID(), Opt.some(oldField.getOutputID()), oldField.getLabel(), oldField.isReadOnly(), oldField.isRequired(), oldField.getCollection(), oldField.getCollectionID(), oldField.getOrder(), oldField.getNamespace()); longField.fromJSON(value); return longField; case MIXED_TEXT: MetadataField<Iterable<String>> mixedField = MetadataField.createMixedIterableStringMetadataField( oldField.getInputID(), Opt.some(oldField.getOutputID()), oldField.getLabel(), oldField.isReadOnly(), oldField.isRequired(), oldField.getCollection(), oldField.getCollectionID(), oldField.getOrder(), oldField.getNamespace()); mixedField.fromJSON(value); return mixedField; case START_DATE: MetadataField<String> startDateField = MetadataField.createTemporalStartDateMetadata(oldField.getInputID(), Opt.some(oldField.getOutputID()), oldField.getLabel(), oldField.isReadOnly(), oldField.isRequired(), oldField.getPattern().get(), oldField.getOrder(), oldField.getNamespace()); startDateField.fromJSON(value); return startDateField; case START_TIME: MetadataField<String> startTimeField = MetadataField.createTemporalStartTimeMetadata(oldField.getInputID(), Opt.some(oldField.getOutputID()), oldField.getLabel(), oldField.isReadOnly(), oldField.isRequired(), oldField.getPattern().get(), oldField.getOrder(), oldField.getNamespace()); startTimeField.fromJSON(value); return startTimeField; case TEXT: MetadataField<String> textField = MetadataField.createTextMetadataField(oldField.getInputID(), Opt.some(oldField.getOutputID()), oldField.getLabel(), oldField.isReadOnly(), oldField.isRequired(), oldField.getCollection(), oldField.getCollectionID(), oldField.getOrder(), oldField.getNamespace()); textField.fromJSON(value); return textField; case TEXT_LONG: MetadataField<String> textLongField = MetadataField.createTextLongMetadataField(oldField.getInputID(), Opt.some(oldField.getOutputID()), oldField.getLabel(), oldField.isReadOnly(), oldField.isRequired(), oldField.getCollection(), oldField.getCollectionID(), oldField.getOrder(), oldField.getNamespace()); textLongField.fromJSON(value); return textLongField; default: return newField; } } /** * Create a metadata field based on a {@link Date}. * * @param inputID * The identifier of the new metadata field * @param label * The label of the new metadata field * @param readOnly * Define if the new metadata is or not a readonly field * @param required * Define if the new metadata field is or not required * @param pattern * The date pattern for {@link SimpleDateFormat}. * @param order * The ui order for the new field, 0 at the top and progressively down from there. * @return The new metadata field * */ public static MetadataField<Date> createDateMetadata(String inputID, Opt<String> outputID, String label, boolean readOnly, boolean required, final String pattern, Opt<Integer> order, Opt<String> namespace) { final SimpleDateFormat dateFormat = getSimpleDateFormatter(pattern); Fn<Opt<Date>, JValue> dateToJSON = new Fn<Opt<Date>, JValue>() { @Override public JValue apply(Opt<Date> date) { if (date.isNone()) return Jsons.BLANK; else { return v(dateFormat.format(date.get()), Jsons.BLANK); } } }; Fn<Object, Date> jsonToDate = new Fn<Object, Date>() { @Override public Date apply(Object value) { try { String date = (String) value; if (StringUtils.isBlank(date)) return null; return dateFormat.parse(date); } catch (java.text.ParseException e) { logger.error("Not able to parse date {}: {}", value, e.getMessage()); return null; } } }; MetadataField<Date> dateField = new MetadataField<>(inputID, outputID, label, readOnly, required, null, Type.DATE, JsonType.DATE, Opt.<Map<String, String>> none(), Opt.<String> none(), dateToJSON, jsonToDate, order, namespace); if (StringUtils.isNotBlank(pattern)) { dateField.setPattern(Opt.some(pattern)); } return dateField; } public static MetadataField<String> createDurationMetadataField(String inputID, Opt<String> outputID, String label, boolean readOnly, boolean required, Opt<Integer> order, Opt<String> namespace) { return createDurationMetadataField(inputID, outputID, label, readOnly, required, Opt.<Map<String, String>> none(), Opt.<String> none(), order, namespace); } public static MetadataField<String> createDurationMetadataField(String inputID, Opt<String> outputID, String label, boolean readOnly, boolean required, Opt<Map<String, String>> collection, Opt<String> collectionId, Opt<Integer> order, Opt<String> namespace) { Fn<Opt<String>, JValue> periodToJSON = new Fn<Opt<String>, JValue>() { @Override public JValue apply(Opt<String> value) { Long returnValue = 0L; DCMIPeriod period = EncodingSchemeUtils.decodePeriod(value.get()); if (period != null && period.hasStart() && period.hasEnd()) { returnValue = period.getEnd().getTime() - period.getStart().getTime(); } else { try { returnValue = Long.parseLong(value.get()); } catch (NumberFormatException e) { logger.debug("Unable to parse duration '{}' as either period or millisecond duration.", value.get()); } } return v(DurationFormatUtils.formatDuration(returnValue, PATTERN_DURATION)); } }; Fn<Object, String> jsonToPeriod = new Fn<Object, String>() { @Override public String apply(Object value) { if (!(value instanceof String)) { logger.warn("The given value for duration can not be parsed."); return ""; } String duration = (String) value; String[] durationParts = duration.split(":"); if (durationParts.length < 3) return null; Integer hours = Integer.parseInt(durationParts[0]); Integer minutes = Integer.parseInt(durationParts[1]); Integer seconds = Integer.parseInt(durationParts[2]); Long returnValue = ((hours.longValue() * 60 + minutes.longValue()) * 60 + seconds.longValue()) * 1000; return returnValue.toString(); } }; return new MetadataField<>(inputID, outputID, label, readOnly, required, "", Type.DURATION, JsonType.TEXT, collection, collectionId, periodToJSON, jsonToPeriod, order, namespace); } /** * Create a metadata field of type mixed iterable String * * @param inputID * The identifier of the new metadata field * @param label * The label of the new metadata field * @param readOnly * Define if the new metadata field can be or not edited * @param required * Define if the new metadata field is or not required * @param collection * If the field has a limited list of possible value, the option should contain this one. Otherwise it should * be none. * @param order * The ui order for the new field, 0 at the top and progressively down from there. * @return the new metadata field */ public static MetadataField<Iterable<String>> createMixedIterableStringMetadataField(String inputID, Opt<String> outputID, String label, boolean readOnly, boolean required, Opt<Map<String, String>> collection, Opt<String> collectionId, Opt<Integer> order, Opt<String> namespace) { Fn<Opt<Iterable<String>>, JValue> iterableToJSON = new Fn<Opt<Iterable<String>>, JValue>() { @Override public JValue apply(Opt<Iterable<String>> value) { if (value.isNone()) return arr(); Object val = value.get(); List<JValue> list = new ArrayList<>(); if (val instanceof String) { // The value is a string so we need to split it. String stringVal = (String) val; for (String entry : stringVal.split(",")) { if (StringUtils.isNotBlank(entry)) list.add(v(entry, Jsons.BLANK)); } } else { // The current value is just an iterable string. for (Object v : value.get()) { list.add(v(v, Jsons.BLANK)); } } return arr(list); } }; Fn<Object, Iterable<String>> jsonToIterable = new Fn<Object, Iterable<String>>() { @Override public Iterable<String> apply(Object arrayIn) { JSONArray array; if (arrayIn instanceof String) { try { array = (JSONArray) parser.parse((String) arrayIn); } catch (ParseException e) { throw new IllegalArgumentException("Unable to parse Mixed Iterable value into a JSONArray: {}", e); } } else { array = (JSONArray) arrayIn; } if (array == null) return new ArrayList<>(); String[] arrayOut = new String[array.size()]; for (int i = 0; i < array.size(); i++) { arrayOut[i] = (String) array.get(i); } return Arrays.asList(arrayOut); } }; return new MetadataField<>(inputID, outputID, label, readOnly, required, new ArrayList<String>(), Type.MIXED_TEXT, JsonType.MIXED_TEXT, collection, collectionId, iterableToJSON, jsonToIterable, order, namespace); } /** * Create a metadata field of type iterable String * * @param inputID * The identifier of the new metadata field * @param label * The label of the new metadata field * @param readOnly * Define if the new metadata field can be or not edited * @param required * Define if the new metadata field is or not required * @param collection * If the field has a limited list of possible value, the option should contain this one. Otherwise it should * be none. * @param order * The ui order for the new field, 0 at the top and progressively down from there. * @return the new metadata field */ public static MetadataField<Iterable<String>> createIterableStringMetadataField(String inputID, Opt<String> outputID, String label, boolean readOnly, boolean required, Opt<Map<String, String>> collection, Opt<String> collectionId, Opt<Integer> order, Opt<String> namespace) { Fn<Opt<Iterable<String>>, JValue> iterableToJSON = new Fn<Opt<Iterable<String>>, JValue>() { @Override public JValue apply(Opt<Iterable<String>> value) { if (value.isNone()) return arr(); Object val = value.get(); List<JValue> list = new ArrayList<>(); if (val instanceof String) { // The value is a string so we need to split it. String stringVal = (String) val; for (String entry : stringVal.split(",")) { list.add(v(entry, Jsons.BLANK)); } } else { // The current value is just an iterable string. for (Object v : value.get()) { list.add(v(v, Jsons.BLANK)); } } return arr(list); } }; Fn<Object, Iterable<String>> jsonToIterable = new Fn<Object, Iterable<String>>() { @Override public Iterable<String> apply(Object arrayIn) { JSONArray array = (JSONArray) arrayIn; if (array == null) return null; String[] arrayOut = new String[array.size()]; for (int i = 0; i < array.size(); i++) { arrayOut[i] = (String) array.get(i); } return Arrays.asList(arrayOut); } }; return new MetadataField<>(inputID, outputID, label, readOnly, required, new ArrayList<String>(), Type.ITERABLE_TEXT, JsonType.TEXT, collection, collectionId, iterableToJSON, jsonToIterable, order, namespace); } public static MetadataField<Long> createLongMetadataField(String inputID, Opt<String> outputID, String label, boolean readOnly, boolean required, Opt<Map<String, String>> collection, Opt<String> collectionId, Opt<Integer> order, Opt<String> namespace) { Fn<Opt<Long>, JValue> longToJSON = new Fn<Opt<Long>, JValue>() { @Override public JValue apply(Opt<Long> value) { if (value.isNone()) return Jsons.BLANK; else return v(value.get().toString()); } }; Fn<Object, Long> jsonToLong = new Fn<Object, Long>() { @Override public Long apply(Object value) { if (!(value instanceof String)) { logger.warn("The given value for Long can not be parsed."); return 0L; } String longString = (String) value; return Long.parseLong(longString); } }; return new MetadataField<>(inputID, outputID, label, readOnly, required, 0L, Type.TEXT, JsonType.NUMBER, collection, collectionId, longToJSON, jsonToLong, order, namespace); } protected void setDurationOutputID(Opt<String> durationOutputID) { this.durationOutputID = durationOutputID; } protected Opt<String> getDurationOutputID() { return durationOutputID; } private static MetadataField<String> createTemporalMetadata(String inputID, Opt<String> outputID, String label, boolean readOnly, boolean required, final String pattern, final Type type, final JsonType jsonType, Opt<Integer> order, Opt<String> namespace) { if (StringUtils.isBlank(pattern)) { throw new IllegalArgumentException( "For temporal metadata field " + inputID + " of type " + type + " there needs to be a pattern."); } final SimpleDateFormat dateFormat = getSimpleDateFormatter(pattern); Fn<Object, String> jsonToDateString = new Fn<Object, String>() { @Override public String apply(Object value) { String date = (String) value; if (StringUtils.isBlank(date)) return ""; try { dateFormat.parse(date); } catch (java.text.ParseException e) { logger.error("Not able to parse date string {}: {}", value, getMessage(e)); return null; } return date; } }; Fn<Opt<String>, JValue> dateToJSON = new Fn<Opt<String>, JValue>() { @Override public JValue apply(Opt<String> periodEncodedString) { if (periodEncodedString.isNone() || StringUtils.isBlank(periodEncodedString.get())) { return Jsons.BLANK; } // Try to parse the metadata as DCIM metadata. DCMIPeriod p = EncodingSchemeUtils.decodePeriod(periodEncodedString.get()); if (p != null) { return v(dateFormat.format(p.getStart()), Jsons.BLANK); } // Not DCIM metadata so it might already be formatted (given from the front and is being returned there try { dateFormat.parse(periodEncodedString.get()); return v(periodEncodedString.get(), Jsons.BLANK); } catch (Exception e) { logger.error( "Unable to parse temporal metadata '{}' as either DCIM data or a formatted date using pattern {} because: {}", new Object[] { periodEncodedString.get(), pattern, getStackTrace(e) }); throw new IllegalArgumentException(e); } } }; MetadataField<String> temporalStart = new MetadataField<>(inputID, outputID, label, readOnly, required, null, type, jsonType, Opt.<Map<String, String>> none(), Opt.<String> none(), dateToJSON, jsonToDateString, order, namespace); temporalStart.setPattern(Opt.some(pattern)); return temporalStart; } public static MetadataField<String> createTemporalStartDateMetadata(String inputID, Opt<String> outputID, String label, boolean readOnly, boolean required, final String pattern, Opt<Integer> order, Opt<String> namespace) { return createTemporalMetadata(inputID, outputID, label, readOnly, required, pattern, Type.START_DATE, JsonType.DATE, order, namespace); } public static MetadataField<String> createTemporalStartTimeMetadata(String inputID, Opt<String> outputID, String label, boolean readOnly, boolean required, final String pattern, Opt<Integer> order, Opt<String> namespace) { return createTemporalMetadata(inputID, outputID, label, readOnly, required, pattern, Type.START_TIME, JsonType.TIME, order, namespace); } /** * Add a temporal format {@link Date} field to the metadata * * @param metadataField * The form of the field * @param label * @param p * The data to put into the field * @param outputID * The id to use for the new field. * @param pattern * The {@link SimpleDateFormat} to format the {@link Date} field. * @param isStart * Whether this field is a start or end value of the DCMIPeriod * @param order * The ui order for the new field, 0 at the top and progressively down from there. */ public static Opt<MetadataField<Date>> createTemporalDateMetadataField(MetadataField<?> metadataField, String label, Opt<DCMIPeriod> p, Opt<String> outputID, Opt<String> pattern, boolean isStart, Opt<Integer> order) { if (outputID.isNone()) { logger.debug("Skipping temporal property with label {} because its output id was not defined.", label); return Opt.none(); } if (pattern.isNone()) { logger.warn("Skipping temporal JSON property with id {} because the date or time pattern was not defined for it.", outputID.get()); return Opt.none(); } MetadataField<Date> dateField = MetadataField.createDateMetadata(metadataField.getInputID(), outputID, label, metadataField.isReadOnly(), metadataField.isRequired(), pattern.get(), metadataField.getOrder(), metadataField.getNamespace()); if (p.isSome()) { Date date; if (isStart) { date = p.get().getStart(); } else { date = p.get().getEnd(); } dateField.setValue(date); } return Opt.some(dateField); } public static Opt<MetadataField<String>> createTemporalDurationMetadataField(MetadataField<?> metadataField, String label, Opt<DCMIPeriod> p, Opt<String> outputID, Opt<Integer> order) { if (outputID.isNone()) { logger.debug("Skipping temporal property with label {} because its output id was not defined.", label); return Opt.none(); } MetadataField<String> durationField = MetadataField.createDurationMetadataField(metadataField.getInputID(), outputID, label, metadataField.isReadOnly(), metadataField.isRequired(), metadataField.getOrder(), metadataField.getNamespace()); Long value = p.get().getEnd().getTime() - p.get().getStart().getTime(); if (p.get().getEnd().before(p.get().getStart())) { throw new IllegalArgumentException("The start date cannot be before the end date. Start: " + p.get().getStart() + " End: " + p.get().getEnd()); } durationField.setValue(value.toString()); return Opt.some(durationField); } /** * Create a metadata field of type String with a single line in the front end. * * @param inputID * The identifier of the new metadata field * @param label * The label of the new metadata field * @param readOnly * Define if the new metadata field can be or not edited * @param required * Define if the new metadata field is or not required * @param collection * If the field has a limited list of possible value, the option should contain this one. Otherwise it should * be none. * @param order * The ui order for the new field, 0 at the top and progressively down from there. * @return the new metadata field */ public static MetadataField<String> createTextMetadataField(String inputID, Opt<String> outputID, String label, boolean readOnly, boolean required, Opt<Map<String, String>> collection, Opt<String> collectionId, Opt<Integer> order, Opt<String> namespace) { return createTextLongMetadataField(inputID, outputID, label, readOnly, required, collection, collectionId, order, JsonType.TEXT, namespace); } /** * Create a metadata field of type String with many lines in the front end. * * @param inputID * The identifier of the new metadata field * @param label * The label of the new metadata field * @param readOnly * Define if the new metadata field can be or not edited * @param required * Define if the new metadata field is or not required * @param collection * If the field has a limited list of possible value, the option should contain this one. Otherwise it should * be none. * @param order * The ui order for the new field, 0 at the top and progressively down from there. * @return the new metadata field */ public static MetadataField<String> createTextLongMetadataField(String inputID, Opt<String> outputID, String label, boolean readOnly, boolean required, Opt<Map<String, String>> collection, Opt<String> collectionId, Opt<Integer> order, Opt<String> namespace) { return createTextLongMetadataField(inputID, outputID, label, readOnly, required, collection, collectionId, order, JsonType.TEXT_LONG, namespace); } /** * Create a metadata field of type String specifying the type for the front end. * * @param id * The identifier of the new metadata field * @param label * The label of the new metadata field * @param readOnly * Define if the new metadata field can be or not edited * @param required * Define if the new metadata field is or not required * @param collection * If the field has a limited list of possible value, the option should contain this one. Otherwise it should * be none. * @param order * The ui order for the new field, 0 at the top and progressively down from there. * @return the new metadata field */ private static MetadataField<String> createTextLongMetadataField(String inputID, Opt<String> outputID, String label, boolean readOnly, boolean required, Opt<Map<String, String>> collection, Opt<String> collectionId, Opt<Integer> order, JsonType jsonType, Opt<String> namespace) { Fn<Opt<String>, JValue> stringToJSON = new Fn<Opt<String>, JValue>() { @Override public JValue apply(Opt<String> value) { return v(value.getOr("")); } }; Fn<Object, String> jsonToString = new Fn<Object, String>() { @Override public String apply(Object jsonValue) { if (jsonValue == null) return ""; if (!(jsonValue instanceof String)) { logger.warn("Value cannot be parsed as String."); return null; } return (String) jsonValue; } }; return new MetadataField<>(inputID, outputID, label, readOnly, required, "", Type.TEXT, jsonType, collection, collectionId, stringToJSON, jsonToString, order, namespace); } /** * Turn a map into a {@link JObject} object * * @param map * the source map * @return a new {@link JObject} generated with the map values */ public static JObject mapToJSON(Map<String, String> map) { if (map == null) { throw new IllegalArgumentException("Map must not be null!"); } List<Field> fields = new ArrayList<>(); for (Entry<String, String> item : map.entrySet()) { fields.add(f(item.getKey(), v(item.getValue(), Jsons.BLANK))); } return obj(fields); } /** * A convenience function for converting properties values into their java equivalents * * @param key * The key for the metadata property. * @param value * The value for the metadata property. */ public void setValue(String key, String value) { switch (key) { case CONFIG_COLLECTION_ID_KEY: this.collectionID = Opt.some(value); break; case CONFIG_PATTERN_KEY: this.pattern = Opt.some(value); break; case CONFIG_INPUT_ID_KEY: this.inputID = value; break; case CONFIG_LABEL_KEY: this.label = value; break; case CONFIG_LIST_PROVIDER_KEY: this.listprovider = Opt.some(value); break; case CONFIG_NAMESPACE_KEY: this.namespace = Opt.some(value); break; case CONFIG_ORDER_KEY: try { Integer orderValue = Integer.parseInt(value); this.order = Opt.some(orderValue); } catch (NumberFormatException e) { logger.warn("Unable to parse order value {} of metadata field {} because:{}", new Object[] { value, this.getInputID(), ExceptionUtils.getStackTrace(e) }); this.order = Opt.none(); } break; case CONFIG_OUTPUT_ID_KEY: this.outputID = Opt.some(value); break; case CONFIG_READ_ONLY_KEY: this.readOnly = Boolean.valueOf(value); break; case CONFIG_REQUIRED_KEY: this.required = Boolean.valueOf(value); break; case CONFIG_TYPE_KEY: this.type = Type.valueOf(value.toUpperCase()); break; default: throw new IllegalArgumentException("Unknown Dublin Core Property Key " + key); } } public Opt<String> getCollectionID() { return collectionID; } public void setCollectionID(Opt<String> collectionID) { this.collectionID = collectionID; } public String getInputID() { return inputID; } public void setInputId(String inputID) { this.inputID = inputID; } public String getLabel() { return label; } public void setLabel(String label) { this.label = label; } public Opt<String> getListprovider() { return listprovider; } public void setListprovider(Opt<String> listprovider) { this.listprovider = listprovider; } public Opt<String> getNamespace() { return namespace; } public void setNamespace(Opt<String> namespace) { this.namespace = namespace; } public Opt<Integer> getOrder() { return order; } public void setOrder(Opt<Integer> order) { this.order = order; } /** * @return The outputID if available, inputID if it is missing. */ public String getOutputID() { if (outputID.isSome()) { return outputID.get(); } else { return inputID; } } public void setOutputID(Opt<String> outputID) { this.outputID = outputID; } public Opt<String> getPattern() { return pattern; } public void setPattern(Opt<String> pattern) { this.pattern = pattern; } public void setReadOnly(boolean readOnly) { this.readOnly = readOnly; } public boolean isReadOnly() { return readOnly; } public boolean isRequired() { return required; } public void setRequired(boolean required) { this.required = required; } public Type getType() { return type; } public void setType(Type type) { this.type = type; } public JsonType getJsonType() { return jsonType; } public void setJsonType(JsonType jsonType) { this.jsonType = jsonType; } public Fn<Object, A> getJsonToValue() { return jsonToValue; } public void setJsonToValue(Fn<Object, A> jsonToValue) { this.jsonToValue = jsonToValue; } public Fn<Opt<A>, JValue> getValueToJSON() { return valueToJSON; } public void setValueToJSON(Fn<Opt<A>, JValue> valueToJSON) { this.valueToJSON = valueToJSON; } }