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