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) 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
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
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 }