1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
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
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
84
85
86
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
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
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
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
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
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
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 }