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 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
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
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
77
78
79
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
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
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 }