View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.metadata.dublincore;
23  
24  import static com.entwinemedia.fn.data.json.Jsons.arr;
25  import static com.entwinemedia.fn.data.json.Jsons.f;
26  import static com.entwinemedia.fn.data.json.Jsons.obj;
27  import static com.entwinemedia.fn.data.json.Jsons.v;
28  import static org.apache.commons.lang3.exception.ExceptionUtils.getMessage;
29  
30  import org.opencastproject.mediapackage.MediaPackageElementFlavor;
31  
32  import com.entwinemedia.fn.data.json.Field;
33  import com.entwinemedia.fn.data.json.JObject;
34  import com.entwinemedia.fn.data.json.JValue;
35  import com.entwinemedia.fn.data.json.Jsons;
36  
37  import org.apache.commons.lang3.StringUtils;
38  import org.apache.commons.lang3.time.DurationFormatUtils;
39  import org.json.simple.JSONArray;
40  import org.json.simple.JSONObject;
41  import org.json.simple.parser.JSONParser;
42  import org.json.simple.parser.ParseException;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
45  
46  import java.text.SimpleDateFormat;
47  import java.util.ArrayList;
48  import java.util.Arrays;
49  import java.util.Date;
50  import java.util.HashMap;
51  import java.util.List;
52  import java.util.Map;
53  import java.util.Objects;
54  import java.util.TimeZone;
55  import java.util.stream.Collectors;
56  
57  public final class MetadataJson {
58    private static final Logger logger = LoggerFactory.getLogger(MetadataJson.class);
59  
60    /* Keys for the different properties of the metadata JSON Object */
61    private static final String JSON_KEY_ID = "id";
62    private static final String JSON_KEY_LABEL = "label";
63    private static final String JSON_KEY_READONLY = "readOnly";
64    private static final String JSON_KEY_REQUIRED = "required";
65    private static final String JSON_KEY_TYPE = "type";
66    private static final String JSON_KEY_VALUE = "value";
67    private static final String JSON_KEY_COLLECTION = "collection";
68    private static final String JSON_KEY_TRANSLATABLE = "translatable";
69    private static final String JSON_KEY_DELIMITER = "delimiter";
70    private static final String JSON_KEY_DIFFERENT_VALUES = "differentValues";
71    private static final String KEY_METADATA_TITLE = "title";
72    private static final String KEY_METADATA_FLAVOR = "flavor";
73    private static final String KEY_METADATA_FIELDS = "fields";
74    private static final String KEY_METADATA_LOCKED = "locked";
75  
76    /* Keys for the different properties of the metadata JSON Object */
77    private static final String KEY_METADATA_ID = "id";
78    private static final String KEY_METADATA_VALUE = "value";
79  
80    private static final String PATTERN_DURATION = "HH:mm:ss";
81  
82    /**
83     * Turn a map into a {@link JObject} object
84     *
85     * @param map the source map
86     * @return a new {@link JObject} generated with the map values
87     */
88    private static JObject mapToJson(final Map<String, String> map) {
89      Objects.requireNonNull(map);
90      final List<Field> fields = new ArrayList<>();
91      for (final Map.Entry<String, String> item : map.entrySet()) {
92        fields.add(f(item.getKey(), v(item.getValue(), Jsons.BLANK)));
93      }
94      return obj(fields);
95    }
96  
97    public enum JsonType {
98      BOOLEAN, DATE, NUMBER, TEXT, MIXED_TEXT, ORDERED_TEXT, TEXT_LONG, TIME
99    }
100 
101   private MetadataJson() {
102   }
103 
104   private static SimpleDateFormat getSimpleDateFormatter(final String pattern) {
105     final SimpleDateFormat dateFormat;
106     if (StringUtils.isNotBlank(pattern)) {
107       dateFormat = new SimpleDateFormat(pattern);
108     } else {
109       dateFormat = new SimpleDateFormat();
110     }
111     dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
112     return dateFormat;
113   }
114 
115   private static <T> JValue valueToJson(final T rawValue, final MetadataField.Type type, final String pattern) {
116     switch (type) {
117       case BOOLEAN:
118         if (rawValue == null)
119           return Jsons.BLANK;
120         return v(rawValue, Jsons.BLANK);
121       case DATE: {
122         if (rawValue == null)
123           return Jsons.BLANK;
124         final SimpleDateFormat dateFormat = getSimpleDateFormatter(pattern);
125         return v(dateFormat.format((Date) rawValue), Jsons.BLANK);
126       }
127       case DURATION: {
128         if (rawValue == null)
129           return Jsons.BLANK;
130         long returnValue = 0L;
131         final String value = (String) rawValue;
132         final DCMIPeriod period = EncodingSchemeUtils.decodePeriod(value);
133         if (period != null && period.hasStart() && period.hasEnd()) {
134           returnValue = period.getEnd().getTime() - period.getStart().getTime();
135         } else {
136           try {
137             returnValue = Long.parseLong(value);
138           } catch (final NumberFormatException e) {
139             logger.debug("Unable to parse duration '{}' as either period or millisecond duration.", value);
140           }
141         }
142         return v(DurationFormatUtils.formatDuration(returnValue, PATTERN_DURATION));
143       }
144       case ITERABLE_TEXT:
145       case MIXED_TEXT: {
146         if (rawValue == null)
147           return arr();
148 
149         final List<JValue> list = new ArrayList<>();
150         if (rawValue instanceof String) {
151           // The value is a string so we need to split it.
152           final String stringVal = (String) rawValue;
153           for (final String entry : stringVal.split(",")) {
154             if (StringUtils.isNotBlank(entry))
155               list.add(v(entry, Jsons.BLANK));
156           }
157         } else {
158           // The current value is just an iterable string.
159           for (final Object v : (Iterable<String>)rawValue) {
160             list.add(v(v, Jsons.BLANK));
161           }
162         }
163 
164         return arr(list);
165       }
166       case ORDERED_TEXT:
167       case TEXT_LONG:
168       case TEXT:
169         return v(rawValue == null ? "" : (String)rawValue);
170       case LONG:
171         if (rawValue == null)
172           return Jsons.BLANK;
173         return v(rawValue.toString());
174       case START_DATE: {
175         if (rawValue == null)
176           return Jsons.BLANK;
177 
178         final String value = (String) rawValue;
179 
180         if (StringUtils.isBlank(value))
181           return Jsons.BLANK;
182 
183         // Try to parse the metadata as DCIM metadata.
184         final DCMIPeriod p = EncodingSchemeUtils.decodePeriod(value);
185         final SimpleDateFormat dateFormat = getSimpleDateFormatter(pattern);
186         if (p != null)
187           return v(dateFormat.format(p.getStart()), Jsons.BLANK);
188 
189         // Not DCIM metadata so it might already be formatted (given from the front and is being returned there
190         try {
191           dateFormat.parse(value);
192           return v(value, Jsons.BLANK);
193         } catch (final Exception e) {
194           logger.error(
195                   "Unable to parse temporal metadata '{}' as either DCIM data or a formatted date using pattern {} because:",
196                   value,
197                   pattern,
198                   e);
199           throw new IllegalArgumentException(e);
200         }
201       }
202       case START_TIME: {
203         if (rawValue == null)
204           return Jsons.BLANK;
205 
206         final String value = (String) rawValue;
207 
208         if (StringUtils.isBlank(value))
209           return Jsons.BLANK;
210 
211         // Try to parse the metadata as DCIM metadata.
212         final DCMIPeriod p = EncodingSchemeUtils.decodePeriod(value);
213         if (p != null) {
214           final SimpleDateFormat dateFormat = getSimpleDateFormatter(pattern);
215           return v(dateFormat.format(p.getStart()), Jsons.BLANK);
216         }
217 
218         // Not DCIM metadata so it might already be formatted (given from the front and is being returned there
219         try {
220           final SimpleDateFormat dateFormat = getSimpleDateFormatter(pattern);
221           dateFormat.parse(value);
222           return v(value, Jsons.BLANK);
223         } catch (final Exception e) {
224           logger.error(
225                   "Unable to parse temporal metadata '{}' as either DCIM data or a formatted date using pattern {} because:",
226                   value,
227                   pattern,
228                   e);
229           throw new IllegalArgumentException(e);
230         }
231       }
232       default:
233         throw new IllegalArgumentException("invalid metadata field of type '" + type + "'");
234     }
235   }
236 
237   private static JsonType jsonType(final MetadataField f, final boolean withOrderedText) {
238     switch (f.getType()) {
239       case BOOLEAN:
240         return JsonType.BOOLEAN;
241       case DATE:
242       case START_DATE:
243         return JsonType.DATE;
244       case DURATION:
245       case ITERABLE_TEXT:
246       case TEXT:
247         return JsonType.TEXT;
248       case MIXED_TEXT:
249         return JsonType.MIXED_TEXT;
250       case ORDERED_TEXT:
251         return withOrderedText ? JsonType.ORDERED_TEXT : JsonType.TEXT;
252       case LONG:
253         return JsonType.NUMBER;
254       case START_TIME:
255         return JsonType.TIME;
256       case TEXT_LONG:
257         return JsonType.TEXT_LONG;
258       default:
259         throw new IllegalArgumentException("invalid field type '" + f.getType() + "'");
260     }
261   }
262 
263   private static Object valueFromJson(final Object value, final MetadataField field) {
264     switch (field.getType()) {
265       case BOOLEAN: {
266         if (value instanceof Boolean)
267           return value;
268         final String stringValue = value.toString();
269         if (StringUtils.isBlank(stringValue))
270           return null;
271         return Boolean.parseBoolean(stringValue);
272       }
273       case DATE: {
274         final SimpleDateFormat dateFormat = getSimpleDateFormatter(field.getPattern());
275         try {
276           final String date = (String) value;
277 
278           if (StringUtils.isBlank(date))
279             return null;
280 
281           return dateFormat.parse(date);
282         } catch (final java.text.ParseException e) {
283           logger.error("Not able to parse date {}: {}", value, e.getMessage());
284           return null;
285         }
286       }
287       case DURATION: {
288         if (!(value instanceof String)) {
289           logger.warn("The given value for duration can not be parsed.");
290           return "";
291         }
292 
293         final String duration = (String) value;
294         final String[] durationParts = duration.split(":");
295         if (durationParts.length < 3)
296           return null;
297         final long hours = Long.parseLong(durationParts[0]);
298         final long minutes = Long.parseLong(durationParts[1]);
299         final long seconds = Long.parseLong(durationParts[2]);
300 
301         final long returnValue = ((hours * 60 + minutes) * 60 + seconds) * 1000;
302 
303         return Long.toString(returnValue);
304       }
305       case ITERABLE_TEXT: {
306         final JSONArray array = (JSONArray) value;
307         if (array == null)
308           return null;
309         final String[] arrayOut = new String[array.size()];
310         for (int i = 0; i < array.size(); i++)
311           arrayOut[i] = (String) array.get(i);
312         return Arrays.asList(arrayOut);
313       }
314       case MIXED_TEXT: {
315         final JSONParser parser = new JSONParser();
316         final JSONArray array;
317         if (value instanceof String) {
318           try {
319             array = (JSONArray) parser.parse((String) value);
320           } catch (final ParseException e) {
321             throw new IllegalArgumentException("Unable to parse Mixed Iterable value into a JSONArray:", e);
322           }
323         } else {
324           array = (JSONArray) value;
325         }
326 
327         if (array == null)
328           return new ArrayList<>();
329         final String[] arrayOut = new String[array.size()];
330         for (int i = 0; i < array.size(); i++)
331           arrayOut[i] = (String) array.get(i);
332         return Arrays.asList(arrayOut);
333       }
334       case TEXT:
335       case TEXT_LONG:
336       case ORDERED_TEXT: {
337         if (value == null)
338           return "";
339         if (!(value instanceof String)) {
340           logger.warn("Value cannot be parsed as String. Expecting type 'String', but received type '{}'.", value.getClass().getName());
341           return null;
342         }
343         return value;
344       }
345       case LONG: {
346         if (!(value instanceof String)) {
347           logger.warn("The given value for Long can not be parsed.");
348           return 0L;
349         }
350         final String longString = (String) value;
351         return Long.parseLong(longString);
352       }
353       case START_DATE:
354       case START_TIME:
355       {
356         final String date = (String) value;
357 
358         if (StringUtils.isBlank(date))
359           return "";
360 
361         try {
362           final SimpleDateFormat dateFormat = getSimpleDateFormatter(field.getPattern());
363           dateFormat.parse(date);
364         } catch (final java.text.ParseException e) {
365           logger.error("Not able to parse date string {}: {}", value, getMessage(e));
366           return null;
367         }
368 
369         return date;
370       }
371       default:
372         throw new IllegalArgumentException("invalid field type '" + field.getType() + "'");
373     }
374   }
375 
376   public static JObject fieldToJson(final MetadataField f, final boolean withOrderedText) {
377     Objects.requireNonNull(f);
378     final Map<String, Field> values = new HashMap<>();
379     values.put(JSON_KEY_ID, f(JSON_KEY_ID, v(f.getOutputID(), Jsons.BLANK)));
380     values.put(JSON_KEY_LABEL, f(JSON_KEY_LABEL, v(f.getLabel(), Jsons.BLANK)));
381     values.put(JSON_KEY_VALUE, f(JSON_KEY_VALUE, valueToJson(f.getValue(), f.getType(), f.getPattern())));
382     values.put(JSON_KEY_TYPE, f(JSON_KEY_TYPE, v(jsonType(f, withOrderedText).toString().toLowerCase(), Jsons.BLANK)));
383     values.put(JSON_KEY_READONLY, f(JSON_KEY_READONLY, v(f.isReadOnly())));
384     values.put(JSON_KEY_REQUIRED, f(JSON_KEY_REQUIRED, v(f.isRequired())));
385 
386     if (f.getCollection() != null)
387       values.put(JSON_KEY_COLLECTION, f(JSON_KEY_COLLECTION, mapToJson(f.getCollection())));
388     else if (f.getCollectionID() != null)
389       values.put(JSON_KEY_COLLECTION, f(JSON_KEY_COLLECTION, v(f.getCollectionID())));
390     if (f.isTranslatable() != null)
391       values.put(JSON_KEY_TRANSLATABLE, f(JSON_KEY_TRANSLATABLE, v(f.isTranslatable())));
392     if (f.getDelimiter() != null)
393       values.put(JSON_KEY_DELIMITER, f(JSON_KEY_DELIMITER, v(f.getDelimiter())));
394     if (f.hasDifferentValues() != null)
395       values.put(JSON_KEY_DIFFERENT_VALUES, f(JSON_KEY_DIFFERENT_VALUES, v(f.hasDifferentValues())));
396     return obj(values);
397   }
398 
399   public static MetadataField copyWithDifferentJsonValue(final MetadataField t, final String v) {
400     final MetadataField copy = new MetadataField(t);
401     copy.setValue(valueFromJson(v, copy));
402     return copy;
403   }
404 
405   public static JValue collectionToJson(final DublinCoreMetadataCollection collection, final boolean withOrderedText) {
406     return arr(collection.getFields().stream().map(field -> fieldToJson(field, withOrderedText))
407             .collect(Collectors.toList()));
408   }
409 
410   public static JSONArray extractSingleCollectionfromListJson(JSONArray json) {
411     if (json == null || json.size() != 1) {
412       throw new IllegalArgumentException("Input has to be a JSONArray with one entry");
413     }
414 
415     return (JSONArray) ((JSONObject) json.get(0)).get(KEY_METADATA_FIELDS);
416   }
417 
418   public static void fillCollectionFromJson(final DublinCoreMetadataCollection collection, final Object json) {
419     if (!(json instanceof  JSONArray))
420       throw new IllegalArgumentException("couldn't fill metadata collection, didn't get an array");
421 
422     final JSONArray metadataJson = (JSONArray) json;
423     for (final JSONObject item : (Iterable<JSONObject>) metadataJson) {
424       final String fieldId = (String) item.get(KEY_METADATA_ID);
425 
426       if (fieldId == null)
427         continue;
428       final Object value = item.get(KEY_METADATA_VALUE);
429       if (value == null)
430         continue;
431 
432       final MetadataField target = collection.getOutputFields().get(fieldId);
433       if (target == null)
434         continue;
435 
436       final Object o = valueFromJson(value, target);
437       target.setValue(o);
438     }
439   }
440 
441   public static void fillListFromJson(final MetadataList metadataList, final JSONArray json) {
442     for (final JSONObject item : (Iterable<JSONObject>) json) {
443       final MediaPackageElementFlavor flavor = MediaPackageElementFlavor
444               .parseFlavor((String) item.get(KEY_METADATA_FLAVOR));
445       final String title = (String) item.get(KEY_METADATA_TITLE);
446       if (title == null)
447         continue;
448 
449       final JSONArray value = (JSONArray) item.get(KEY_METADATA_FIELDS);
450       if (value == null)
451         continue;
452 
453       final DublinCoreMetadataCollection collection = metadataList.getMetadataByFlavor(flavor.toString());
454       if (collection == null)
455         continue;
456       MetadataJson.fillCollectionFromJson(collection, value);
457     }
458   }
459 
460   public static JValue listToJson(final MetadataList metadataList, final boolean withOrderedText) {
461     final List<JValue> catalogs = new ArrayList<>();
462     for (final Map.Entry<String, MetadataList.TitledMetadataCollection> metadata : metadataList.getMetadataList().entrySet()) {
463       final List<Field> fields = new ArrayList<>();
464 
465       DublinCoreMetadataCollection metadataCollection = metadata.getValue().getCollection();
466 
467       if (!MetadataList.Locked.NONE.equals(metadataList.getLocked())) {
468         fields.add(f(KEY_METADATA_LOCKED, v(metadataList.getLocked().getValue())));
469         metadataCollection = metadataCollection.readOnlyCopy();
470       }
471 
472       fields.add(f(KEY_METADATA_FLAVOR, v(metadata.getKey())));
473       fields.add(f(KEY_METADATA_TITLE, v(metadata.getValue().getTitle())));
474       fields.add(f(KEY_METADATA_FIELDS, MetadataJson.collectionToJson(metadataCollection, withOrderedText)));
475 
476       catalogs.add(obj(fields));
477     }
478     return arr(catalogs);
479   }
480 }