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) return new JsonPrimitive("");
115         SimpleDateFormat dateFormat = getSimpleDateFormatter(pattern);
116         return new JsonPrimitive(dateFormat.format((Date) rawValue));
117       }
118 
119       case DURATION: {
120         if (rawValue == null) return new JsonPrimitive("");
121         long returnValue = 0L;
122         String value = (String) rawValue;
123         DCMIPeriod period = EncodingSchemeUtils.decodePeriod(value);
124 
125         if (period != null && period.hasStart() && period.hasEnd()) {
126           returnValue = period.getEnd().getTime() - period.getStart().getTime();
127         } else {
128           try {
129             returnValue = Long.parseLong(value);
130           } catch (NumberFormatException e) {
131             logger.debug("Unable to parse duration '{}' as either period or millisecond duration.", value);
132           }
133         }
134         return new JsonPrimitive(DurationFormatUtils.formatDuration(returnValue, PATTERN_DURATION));
135       }
136 
137       case ITERABLE_TEXT:
138       case MIXED_TEXT: {
139         JsonArray jsonArray = new JsonArray();
140 
141         if (rawValue == null) return jsonArray;
142 
143         if (rawValue instanceof String) {
144           for (String entry : ((String) rawValue).split(",")) {
145             if (StringUtils.isNotBlank(entry)) {
146               jsonArray.add(safeString(entry));
147             }
148           }
149         } else {
150           for (Object val : (Iterable<?>) rawValue) {
151             if (val != null) jsonArray.add(safeString(val));
152           }
153         }
154         return jsonArray;
155       }
156 
157       case ORDERED_TEXT:
158       case TEXT_LONG:
159       case TEXT:
160         return rawValue == null ? new JsonPrimitive("") : new JsonPrimitive(rawValue.toString());
161 
162       case LONG:
163         return rawValue == null ? new JsonPrimitive("") : new JsonPrimitive(rawValue.toString());
164 
165       case START_DATE:
166       case START_TIME: {
167         if (rawValue == null) return new JsonPrimitive("");
168         String value = (String) rawValue;
169 
170         if (StringUtils.isBlank(value)) return new JsonPrimitive("");
171 
172         // Try to parse the metadata as DCIM metadata.
173         final DCMIPeriod p = EncodingSchemeUtils.decodePeriod(value);
174         final SimpleDateFormat dateFormat = getSimpleDateFormatter(pattern);
175         if (p != null) return new JsonPrimitive(dateFormat.format(p.getStart()));
176 
177         // Not DCIM metadata so it might already be formatted (given from the front and is being returned there
178         try {
179           dateFormat.parse(value);
180           return new JsonPrimitive(value);
181         } catch (Exception e) {
182           logger.error(
183               "Unable to parse temporal metadata '{}' as either DCIM data or a formatted date using pattern {} because:",
184               value,
185               pattern,
186               e);
187           throw new IllegalArgumentException(e);
188         }
189       }
190 
191       default:
192         throw new IllegalArgumentException("invalid metadata field of type '" + type + "'");
193     }
194   }
195 
196   private static JsonType jsonType(final MetadataField f, final boolean withOrderedText) {
197     switch (f.getType()) {
198       case BOOLEAN:
199         return JsonType.BOOLEAN;
200       case DATE:
201       case START_DATE:
202         return JsonType.DATE;
203       case DURATION:
204       case ITERABLE_TEXT:
205       case TEXT:
206         return JsonType.TEXT;
207       case MIXED_TEXT:
208         return JsonType.MIXED_TEXT;
209       case ORDERED_TEXT:
210         return withOrderedText ? JsonType.ORDERED_TEXT : JsonType.TEXT;
211       case LONG:
212         return JsonType.NUMBER;
213       case START_TIME:
214         return JsonType.TIME;
215       case TEXT_LONG:
216         return JsonType.TEXT_LONG;
217       default:
218         throw new IllegalArgumentException("invalid field type '" + f.getType() + "'");
219     }
220   }
221 
222   private static Object valueFromJson(final Object value, final MetadataField field) {
223     switch (field.getType()) {
224       case BOOLEAN: {
225         if (value instanceof Boolean)
226           return value;
227         final String stringValue = value.toString();
228         if (StringUtils.isBlank(stringValue))
229           return null;
230         return Boolean.parseBoolean(stringValue);
231       }
232       case DATE: {
233         final SimpleDateFormat dateFormat = getSimpleDateFormatter(field.getPattern());
234         try {
235           final String date = (String) value;
236 
237           if (StringUtils.isBlank(date))
238             return null;
239 
240           return dateFormat.parse(date);
241         } catch (final java.text.ParseException e) {
242           logger.error("Not able to parse date {}: {}", value, e.getMessage());
243           return null;
244         }
245       }
246       case DURATION: {
247         if (!(value instanceof String)) {
248           logger.warn("The given value for duration can not be parsed.");
249           return "";
250         }
251 
252         final String duration = (String) value;
253         final String[] durationParts = duration.split(":");
254         if (durationParts.length < 3)
255           return null;
256         final long hours = Long.parseLong(durationParts[0]);
257         final long minutes = Long.parseLong(durationParts[1]);
258         final long seconds = Long.parseLong(durationParts[2]);
259 
260         final long returnValue = ((hours * 60 + minutes) * 60 + seconds) * 1000;
261 
262         return Long.toString(returnValue);
263       }
264       case ITERABLE_TEXT: {
265         final JSONArray array = (JSONArray) value;
266         if (array == null)
267           return null;
268         final String[] arrayOut = new String[array.size()];
269         for (int i = 0; i < array.size(); i++)
270           arrayOut[i] = (String) array.get(i);
271         return Arrays.asList(arrayOut);
272       }
273       case MIXED_TEXT: {
274         final JSONParser parser = new JSONParser();
275         final JSONArray array;
276         if (value instanceof String) {
277           try {
278             array = (JSONArray) parser.parse((String) value);
279           } catch (final ParseException e) {
280             throw new IllegalArgumentException("Unable to parse Mixed Iterable value into a JSONArray:", e);
281           }
282         } else {
283           array = (JSONArray) value;
284         }
285 
286         if (array == null)
287           return new ArrayList<>();
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         return Arrays.asList(arrayOut);
292       }
293       case TEXT:
294       case TEXT_LONG:
295       case ORDERED_TEXT: {
296         if (value == null)
297           return "";
298         if (!(value instanceof String)) {
299           logger.warn("Value cannot be parsed as String. Expecting type 'String', but received type '{}'.", value.getClass().getName());
300           return null;
301         }
302         return value;
303       }
304       case LONG: {
305         if (!(value instanceof String)) {
306           logger.warn("The given value for Long can not be parsed.");
307           return 0L;
308         }
309         final String longString = (String) value;
310         return Long.parseLong(longString);
311       }
312       case START_DATE:
313       case START_TIME:
314       {
315         final String date = (String) value;
316 
317         if (StringUtils.isBlank(date))
318           return "";
319 
320         try {
321           final SimpleDateFormat dateFormat = getSimpleDateFormatter(field.getPattern());
322           dateFormat.parse(date);
323         } catch (final java.text.ParseException e) {
324           logger.error("Not able to parse date string {}: {}", value, getMessage(e));
325           return null;
326         }
327 
328         return date;
329       }
330       default:
331         throw new IllegalArgumentException("invalid field type '" + field.getType() + "'");
332     }
333   }
334 
335   public static JsonObject fieldToJson(final MetadataField f, final boolean withOrderedText) {
336     Objects.requireNonNull(f);
337 
338     JsonObject json = new JsonObject();
339 
340     json.addProperty(JSON_KEY_ID, safeString(f.getOutputID()));
341     json.addProperty(JSON_KEY_LABEL, safeString(f.getLabel()));
342     json.add(JSON_KEY_VALUE, valueToJson(f.getValue(), f.getType(), f.getPattern()));
343     json.addProperty(JSON_KEY_TYPE, safeString(jsonType(f, withOrderedText).toString().toLowerCase()));
344     json.addProperty(JSON_KEY_READONLY, f.isReadOnly());
345     json.addProperty(JSON_KEY_REQUIRED, f.isRequired());
346 
347     if (f.getCollection() != null) {
348       json.add(JSON_KEY_COLLECTION, mapToJson(f.getCollection()));
349     } else if (f.getCollectionID() != null) {
350       json.addProperty(JSON_KEY_COLLECTION, f.getCollectionID());
351     }
352 
353     if (f.isTranslatable() != null) {
354       json.addProperty(JSON_KEY_TRANSLATABLE, f.isTranslatable());
355     }
356     if (f.getDelimiter() != null) {
357       json.addProperty(JSON_KEY_DELIMITER, f.getDelimiter());
358     }
359     if (f.hasDifferentValues() != null) {
360       json.addProperty(JSON_KEY_DIFFERENT_VALUES, f.hasDifferentValues());
361     }
362 
363     return json;
364   }
365 
366   public static String safeString(Object input) {
367     return input != null ? input.toString() : "";
368   }
369 
370   public static MetadataField copyWithDifferentJsonValue(final MetadataField t, final String v) {
371     final MetadataField copy = new MetadataField(t);
372     copy.setValue(valueFromJson(v, copy));
373     return copy;
374   }
375 
376   public static JsonArray collectionToJson(final DublinCoreMetadataCollection collection, final boolean withOrderedText) {
377     JsonArray jsonArray = new JsonArray();
378     for (MetadataField field : collection.getFields()) {
379       JsonObject fieldJson = fieldToJson(field, withOrderedText);
380       jsonArray.add(fieldJson);
381     }
382     return jsonArray;
383   }
384 
385   public static JSONArray extractSingleCollectionfromListJson(JSONArray json) {
386     if (json == null || json.size() != 1) {
387       throw new IllegalArgumentException("Input has to be a JSONArray with one entry");
388     }
389 
390     return (JSONArray) ((JSONObject) json.get(0)).get(KEY_METADATA_FIELDS);
391   }
392 
393   public static void fillCollectionFromJson(final DublinCoreMetadataCollection collection, final Object json) {
394     if (!(json instanceof  JSONArray))
395       throw new IllegalArgumentException("couldn't fill metadata collection, didn't get an array");
396 
397     final JSONArray metadataJson = (JSONArray) json;
398     for (final JSONObject item : (Iterable<JSONObject>) metadataJson) {
399       final String fieldId = (String) item.get(KEY_METADATA_ID);
400 
401       if (fieldId == null)
402         continue;
403       final Object value = item.get(KEY_METADATA_VALUE);
404       if (value == null)
405         continue;
406 
407       final MetadataField target = collection.getOutputFields().get(fieldId);
408       if (target == null)
409         continue;
410 
411       final Object o = valueFromJson(value, target);
412       target.setValue(o);
413     }
414   }
415 
416   public static void fillListFromJson(final MetadataList metadataList, final JSONArray json) {
417     for (final JSONObject item : (Iterable<JSONObject>) json) {
418       final MediaPackageElementFlavor flavor = MediaPackageElementFlavor
419               .parseFlavor((String) item.get(KEY_METADATA_FLAVOR));
420       final String title = (String) item.get(KEY_METADATA_TITLE);
421       if (title == null)
422         continue;
423 
424       final JSONArray value = (JSONArray) item.get(KEY_METADATA_FIELDS);
425       if (value == null)
426         continue;
427 
428       final DublinCoreMetadataCollection collection = metadataList.getMetadataByFlavor(flavor.toString());
429       if (collection == null)
430         continue;
431       MetadataJson.fillCollectionFromJson(collection, value);
432     }
433   }
434 
435   public static JsonArray listToJson(final MetadataList metadataList, final boolean withOrderedText) {
436     JsonArray catalogs = new JsonArray();
437 
438     for (Map.Entry<String, MetadataList.TitledMetadataCollection> metadata : metadataList.getMetadataList().entrySet()) {
439       JsonObject catalogJson = new JsonObject();
440 
441       DublinCoreMetadataCollection metadataCollection = metadata.getValue().getCollection();
442 
443       if (!MetadataList.Locked.NONE.equals(metadataList.getLocked())) {
444         catalogJson.addProperty(KEY_METADATA_LOCKED, metadataList.getLocked().getValue());
445         metadataCollection = metadataCollection.readOnlyCopy();
446       }
447 
448       catalogJson.addProperty(KEY_METADATA_FLAVOR, metadata.getKey());
449       catalogJson.addProperty(KEY_METADATA_TITLE, metadata.getValue().getTitle());
450       catalogJson.add(KEY_METADATA_FIELDS, collectionToJson(metadataCollection, withOrderedText));
451 
452       catalogs.add(catalogJson);
453     }
454 
455     return catalogs;
456   }
457 }