Skip to content

Package: Directory

Directory

nameinstructionbranchcomplexitylinemethod
Directory()
M: 20 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 7 C: 0
0%
M: 1 C: 0
0%
Directory(Instant)
M: 23 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 8 C: 0
0%
M: 1 C: 0
0%
as(As.Type)
M: 5 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
as(Class)
M: 5 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
asMany(As.Type)
M: 5 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
asMany(Class)
M: 5 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
cast(Object, Class, int)
M: 130 C: 0
0%
M: 30 C: 0
0%
M: 16 C: 0
0%
M: 24 C: 0
0%
M: 1 C: 0
0%
contains(int)
M: 6 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
equals(Object)
M: 54 C: 0
0%
M: 10 C: 0
0%
M: 6 C: 0
0%
M: 13 C: 0
0%
M: 1 C: 0
0%
equals(Object, Object)
M: 38 C: 0
0%
M: 10 C: 0
0%
M: 6 C: 0
0%
M: 9 C: 0
0%
M: 1 C: 0
0%
forEachTag(BiConsumer)
M: 11 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 2 C: 0
0%
M: 1 C: 0
0%
forEachTag(Consumer)
M: 9 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 2 C: 0
0%
M: 1 C: 0
0%
forEachTag(IntConsumer)
M: 6 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 2 C: 0
0%
M: 1 C: 0
0%
formatDateTime(Instant)
M: 11 C: 0
0%
M: 2 C: 0
0%
M: 2 C: 0
0%
M: 3 C: 0
0%
M: 1 C: 0
0%
get(Directory.Tag)
M: 6 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
get(Directory.Tag, Class)
M: 6 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
get(int, Class)
M: 10 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
getLatestModificationTime()
M: 3 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
getRaw(int)
M: 6 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
getSubDirectory(String)
M: 7 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
getSubDirectoryNames()
M: 7 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
getTagCodes()
M: 9 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
getTagInfo(int)
M: 16 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 2 C: 0
0%
M: 1 C: 0
0%
getTags()
M: 12 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
hashCode()
M: 36 C: 0
0%
M: 4 C: 0
0%
M: 3 C: 0
0%
M: 5 C: 0
0%
M: 1 C: 0
0%
isEmpty()
M: 4 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
isSubClass(Class, String)
M: 15 C: 0
0%
M: 4 C: 0
0%
M: 3 C: 0
0%
M: 4 C: 0
0%
M: 1 C: 0
0%
lambda$forEachTag$2(BiConsumer, Directory.Tag)
M: 8 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
lambda$get$0(Class, int, Object)
M: 6 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
lambda$getTags$1(int)
M: 3 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
lambda$parseDateTime$3(String, ZoneOffset, DateTimeFormatter)
M: 0 C: 10
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 3
100%
M: 0 C: 1
100%
lambda$toTag$4(int)
M: 8 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
load(DirectoryLoader)
M: 63 C: 0
0%
M: 4 C: 0
0%
M: 3 C: 0
0%
M: 8 C: 0
0%
M: 1 C: 0
0%
maybeAs(As.Type)
M: 5 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
maybeAs(Class)
M: 5 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
parseDateTime(String)
M: 6 C: 20
77%
M: 2 C: 2
50%
M: 2 C: 1
33%
M: 2 C: 6
75%
M: 0 C: 1
100%
remove(int)
M: 9 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 3 C: 0
0%
M: 1 C: 0
0%
set(Directory.Tag, Object)
M: 39 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 8 C: 0
0%
M: 1 C: 0
0%
setRaw(int, Object)
M: 50 C: 0
0%
M: 8 C: 0
0%
M: 5 C: 0
0%
M: 14 C: 0
0%
M: 1 C: 0
0%
static {...}
M: 0 C: 27
100%
M: 0 C: 0
100%
M: 0 C: 1
100%
M: 0 C: 6
100%
M: 0 C: 1
100%
toString()
M: 54 C: 0
0%
M: 4 C: 0
0%
M: 3 C: 0
0%
M: 8 C: 0
0%
M: 1 C: 0
0%
toTag(int)
M: 8 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 1 C: 0
0%
M: 1 C: 0
0%
touch()
M: 4 C: 0
0%
M: 0 C: 0
100%
M: 1 C: 0
0%
M: 2 C: 0
0%
M: 1 C: 0
0%

Coverage

1: /*
2: * *********************************************************************************************************************
3: *
4: * Mistral: open source imaging engine
5: * http://tidalwave.it/projects/mistral
6: *
7: * Copyright (C) 2003 - 2023 by Tidalwave s.a.s. (http://tidalwave.it)
8: *
9: * *********************************************************************************************************************
10: *
11: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
12: * the License. You may obtain a copy of the License at
13: *
14: * http://www.apache.org/licenses/LICENSE-2.0
15: *
16: * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
17: * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
18: * specific language governing permissions and limitations under the License.
19: *
20: * *********************************************************************************************************************
21: *
22: * git clone https://bitbucket.org/tidalwave/mistral-src
23: * git clone https://github.com/tidalwave-it/mistral-src
24: *
25: * *********************************************************************************************************************
26: */
27: package it.tidalwave.image.metadata;
28:
29: import java.lang.reflect.Array;
30: import javax.annotation.CheckForNull;
31: import javax.annotation.Nonnegative;
32: import javax.annotation.Nonnull;
33: import javax.annotation.concurrent.Immutable;
34: import java.time.Instant;
35: import java.time.LocalDateTime;
36: import java.time.ZoneOffset;
37: import java.time.format.DateTimeFormatter;
38: import java.util.Arrays;
39: import java.util.HashMap;
40: import java.util.List;
41: import java.util.Map;
42: import java.util.Optional;
43: import java.util.Set;
44: import java.util.concurrent.CopyOnWriteArraySet;
45: import java.util.function.BiConsumer;
46: import java.util.function.Consumer;
47: import java.util.function.IntConsumer;
48: import java.util.stream.IntStream;
49: import java.util.stream.Stream;
50: import java.io.Serializable;
51: import com.drew.metadata.StringValue;
52: import it.tidalwave.util.As;
53: import it.tidalwave.image.Rational;
54: import it.tidalwave.image.metadata.loader.DirectoryLoader;
55: import lombok.EqualsAndHashCode;
56: import lombok.Getter;
57: import lombok.RequiredArgsConstructor;
58: import lombok.ToString;
59: import lombok.experimental.Delegate;
60: import lombok.extern.slf4j.Slf4j;
61: import static java.util.stream.Collectors.*;
62: import static java.nio.charset.StandardCharsets.UTF_8;
63:
64: /***********************************************************************************************************************
65: *
66: * This class provides basic support for all kinds of metadata such EXIF, IPTC or maker notes.
67: *
68: * @author Fabrizio Giudici
69: *
70: **********************************************************************************************************************/
71: @Slf4j
72: public class Directory extends JavaBeanSupport implements As, Serializable
73: {
74: /*******************************************************************************************************************
75: *
76: * A descriptor for a tag.
77: *
78: * @param <T> the type of the tag
79: *
80: **********************************************************************************************************************/
81: @Immutable @RequiredArgsConstructor(staticName = "of") @Getter @ToString @EqualsAndHashCode
82: public static class Tag<T>
83: {
84: private final int code;
85:
86: @Nonnull
87: private final String name;
88:
89: @Nonnull
90: private final String propertyName;
91:
92: @Nonnull
93: private final Class<T> type;
94: }
95:
96: private static final long serialVersionUID = 308812466726854722L;
97: private static final List<DateTimeFormatter> EXIF_DATE_TIME_FORMATTERS =
98: Stream.of("yyyy:MM:dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss")
99: .map(DateTimeFormatter::ofPattern).collect(toList());;
100:
101: protected final static Map<String, Tag> tagMapByCode = new HashMap<>();
102:
103: @Delegate
104: private final As asDelegate = As.forObject(this);
105:
106: private final Map<Integer, Object> valueMapByCode = new HashMap<>();
107:
108: private final Map<String, Directory> directoryMapByName = new HashMap<>();
109:
110: private Instant latestModificationTime = Instant.now();
111:
112: private static int nextId = 1;
113:
114: private transient int idForToString;
115:
116: /*******************************************************************************************************************
117: *
118: * Creates an empty directory.
119: *
120: ******************************************************************************************************************/
121: public Directory()
122: {
123: }
124:
125: /*******************************************************************************************************************
126: *
127: * Creates an empty directory with a given latest modification time.
128: *
129: * @param latestModificationTime the latest modification time
130: *
131: ******************************************************************************************************************/
132: public Directory (final @Nonnull Instant latestModificationTime)
133: {
134: this.latestModificationTime = latestModificationTime;
135: }
136:
137: /*******************************************************************************************************************
138: *
139: * Returns a value given its tag. The result is converted to the standard type of the tag (e.g. an enum).
140: *
141: * @param <T> the static type of the tag
142: * @param tag the tag to retrieve
143: * @return the value
144: *
145: ******************************************************************************************************************/
146: @Nonnull
147: public <T> Optional<T> get (@Nonnull final Tag<T> tag)
148: {
149: return get(tag, tag.getType());
150: }
151:
152: /*******************************************************************************************************************
153: *
154: * Returns a value given its tag. The result is converted to the specified type, is possible.
155: *
156: * @param <U> the static type of the tag
157: * @param <T> the static type of the type to convert
158: * @param tag the tag to retrieve
159: * @param asType the type to convert the value into
160: * @return the value
161: *
162: ******************************************************************************************************************/
163: @Nonnull
164: public <T, U> Optional<T> get (@Nonnull final Tag<U> tag, @Nonnull final Class<T> asType)
165: {
166: return get(tag.code, asType);
167: }
168:
169: /*******************************************************************************************************************
170: *
171: * Returns a value given its tag. The result is converted to the specified type, is possible.
172: *
173: * @param <T> the static type of the type to convert
174: * @param code the code of the tag to retrieve
175: * @param asType the type to convert the value into
176: * @return the value
177: *
178: ******************************************************************************************************************/
179: @Nonnull
180: public <T> Optional<T> get (@Nonnegative final int code, @Nonnull final Class<T> asType)
181: {
182: return Optional.ofNullable(getRaw(code)).map(v -> cast(v, asType, code));
183: }
184:
185: /*******************************************************************************************************************
186: *
187: * Returns a value given its tag code. No type conversion is applied.
188: *
189: * @param code the code of the tag to retrieve
190: * @return the value (can be null)
191: *
192: ******************************************************************************************************************/
193: @CheckForNull
194: public Object getRaw (@Nonnegative final int code)
195: {
196: return valueMapByCode.get(code);
197: }
198:
199: /*******************************************************************************************************************
200: *
201: * Sets a value for a tag. {@code null} and {@link Optional} are accepted and
202: * eventually unpacked: passing {@code null} or an empty {@code Optional} is equivalent to a call to
203: * {@link #remove(int)}.
204: *
205: * If the value is different from the previous one, events are fired:
206: *
207: * <ul>
208: * <li>{@code the property name}</li>
209: * <li>{@code empty}</li>
210: * <li>{@code latestModificationTime}</li>
211: * </ul>
212: *
213: * @param tag the tag to retrieve
214: * @param value the new value
215: *
216: ******************************************************************************************************************/
217: public void set (final @Nonnull Tag<?> tag, Object value)
218: {
219: final var oldValue = getRaw(tag.code);
220: final var oldEmpty = isEmpty();
221: final var oldLatestModificationTime = getLatestModificationTime();
222: setRaw(tag.code, value); // FIXME: reverse cast
223: pcs.firePropertyChange(tag.propertyName, oldValue, tag.propertyName);
224: pcs.firePropertyChange("empty", oldEmpty, isEmpty());
225: pcs.firePropertyChange("latestModificationTime", oldLatestModificationTime, getLatestModificationTime());
226: }
227:
228: /*******************************************************************************************************************
229: *
230: * Sets a raw value, that is without converting any type. {@code null} and {@link Optional} are accepted and
231: * eventually unpacked: passing {@code null} or an empty {@code Optional} is equivalent to a call to
232: * {@link #remove(int)}.
233: *
234: * This method does not fire events.
235: *
236: * @param code the code of the tag to set
237: * @param value the value
238: *
239: ******************************************************************************************************************/
240: public void setRaw (final @Nonnegative int code, Object value)
241: {
242:• if ((value != null) && (value instanceof Optional))
243: {
244: value = (((Optional<?>)value).orElse(null));
245: }
246:
247:• if (value == null)
248: {
249: remove(code);
250: return;
251: }
252:
253:• if (value.getClass().isEnum())
254: {
255: try
256: {
257: final var getValueMethod = value.getClass().getMethod("getValue");
258: value = getValueMethod.invoke(value);
259: }
260: catch (Exception e)
261: {
262: throw new RuntimeException(e);
263: }
264: }
265:
266: valueMapByCode.put(code, value);
267: touch();
268: }
269:
270: /*******************************************************************************************************************
271: *
272: * Verifies if a value is present.
273: *
274: * @param code the code of the tag
275: * @return if the value is present
276: *
277: ******************************************************************************************************************/
278: public boolean contains (final @Nonnegative int code)
279: {
280: return valueMapByCode.containsKey(code);
281: }
282:
283: /*******************************************************************************************************************
284: *
285: * Removes a value. This method does not fire events.
286: *
287: * @param code the code of the tag to remove
288: * @return if the value is present
289: *
290: ******************************************************************************************************************/
291: public void remove (final @Nonnegative int code)
292: {
293: valueMapByCode.remove(code);
294: touch();
295: }
296:
297: /*******************************************************************************************************************
298: *
299: * Returns information about a tag.
300: *
301: * @param tag the tag code
302: * @return the tag info
303: *
304: ******************************************************************************************************************/
305: @Nonnull
306: public Optional<Tag<?>> getTagInfo (@Nonnegative final int tag)
307: {
308: final var s = (getClass().getSimpleName() + "DirectoryGenerated").replaceAll("TIFF", "EXIF");
309: return Optional.ofNullable(tagMapByCode.get(s + "." + tag));
310: }
311:
312: /*******************************************************************************************************************
313: *
314: * Returns the tag codes contained in this directory, sorted by code.
315: *
316: * @return the tag codes
317: *
318: ******************************************************************************************************************/
319: @Nonnull
320: public int[] getTagCodes()
321: {
322: return valueMapByCode.keySet().stream().mapToInt(Integer::intValue).sorted().toArray();
323: }
324:
325: /*******************************************************************************************************************
326: *
327: * Returns the tags contained in this directory. Tags are sorted by code.
328: *
329: * @return the tags
330: *
331: ******************************************************************************************************************/
332: @Nonnull
333: public Tag[] getTags()
334: {
335: return valueMapByCode.keySet().stream().sorted().map(this::toTag).toArray(Tag[]::new);
336: }
337:
338: /*******************************************************************************************************************
339: *
340: * Iterates through the tags calling the provided action.
341: *
342: * @param action the action to call
343: *
344: ******************************************************************************************************************/
345: public void forEachTag (@Nonnull final Consumer<Tag<?>> action)
346: {
347: IntStream.of(getTagCodes()).mapToObj(this::toTag).forEach(action);
348: }
349:
350: /*******************************************************************************************************************
351: *
352: * Iterates through the tags and related raw values calling the provided action.
353: *
354: * @param action the action to call
355: *
356: ******************************************************************************************************************/
357: public void forEachTag (@Nonnull final BiConsumer<Tag<?>, Object> action)
358: {
359: IntStream.of(getTagCodes()).mapToObj(this::toTag).forEach(t -> action.accept(t, getRaw(t.getCode())));
360: }
361:
362: /*******************************************************************************************************************
363: *
364: * Iterates through the tag codes calling the provided action.
365: *
366: * @param action the action to call
367: *
368: ******************************************************************************************************************/
369: public void forEachTag (@Nonnull final IntConsumer action)
370: {
371: IntStream.of(getTagCodes()).forEach(action);
372: }
373:
374: /*******************************************************************************************************************
375: *
376: * Checks whether this directory is empty.
377: *
378: * @return {@code true} if this directory doesn't contain any tag
379: *
380: ******************************************************************************************************************/
381: public boolean isEmpty()
382: {
383: return this.valueMapByCode.isEmpty();
384: }
385:
386: /*******************************************************************************************************************
387: *
388: * Returns the names of the available subdirectories.
389: *
390: * @return the names of subdirectories
391: *
392: ******************************************************************************************************************/
393: @Nonnull
394: public Set<String> getSubDirectoryNames()
395: {
396: return new CopyOnWriteArraySet<>(directoryMapByName.keySet());
397: }
398:
399: /*******************************************************************************************************************
400: *
401: * Returns a subdirectory given its name.
402: *
403: * @param name the name of the subdirectory
404: * @return the subdirectory
405: *
406: ******************************************************************************************************************/
407: @Nonnull
408: public Optional<Directory> getSubDirectory (final @Nonnull String name)
409: {
410: return Optional.ofNullable(directoryMapByName.get(name));
411: }
412:
413: /*******************************************************************************************************************
414: *
415: * Returns the latest modification time of this object.
416: *
417: * @return the latest modification time
418: *
419: ******************************************************************************************************************/
420: @Nonnull
421: public Instant getLatestModificationTime()
422: {
423: return latestModificationTime;
424: }
425:
426: /*******************************************************************************************************************
427: *
428: * Loads tags and subdirectories from the given loader.
429: *
430: ******************************************************************************************************************/
431: public void load (final @Nonnull DirectoryLoader loader)
432: {
433: log.debug("load({})", loader);
434:
435:• for (final var tag : loader.getTags())
436: {
437: valueMapByCode.put(tag, loader.getObject(tag));
438: }
439:
440:• for (final var directoryName : loader.getSubDirectoryNames())
441: {
442: final var directory = new Directory();
443: directory.load(loader.getSubDirectory(directoryName));
444: directoryMapByName.put(directoryName, directory);
445: }
446: }
447:
448: /*******************************************************************************************************************
449: *
450: *
451: ******************************************************************************************************************/
452: @Override
453: public final boolean equals (final Object object)
454: {
455:• if (object == null)
456: {
457: return false;
458: }
459:
460:• if (getClass() != object.getClass())
461: {
462: return false;
463: }
464:
465: final var other = (Directory)object;
466: final var myTags = getTagCodes();
467: final var otherTags = other.getTagCodes();
468:
469:• if (!Arrays.equals(myTags, otherTags))
470: {
471: return false;
472: }
473:
474:• for (final var tag : myTags)
475: {
476:• if (!equals(getRaw(tag), other.getRaw(tag)))
477: {
478: return false;
479: }
480: }
481:
482: // if (this.tagMap != other.tagMap && (this.tagMap == null || !this.tagMap.equals(other.tagMap)))
483: // {
484: // return false;
485: // }
486:
487: // FIXME if (this.directoryMap != other.directoryMap && (this.directoryMap == null || !this.directoryMap
488: // .equals(other.directoryMap)))
489: // {
490: // return false;
491: // }
492:
493: // FIXME
494: // if (this.latestModificationTime != other.latestModificationTime && (this.latestModificationTime == null ||
495: // !this.latestModificationTime.equals(other.latestModificationTime)))
496: // {
497: // return false;
498: // }
499:
500: return true;
501: }
502:
503: /*******************************************************************************************************************
504: *
505: * {@inheritDoc}
506: *
507: ******************************************************************************************************************/
508: @Override @Nonnull
509: public final String toString()
510: {
511: synchronized (this)
512: {
513:• if (idForToString == 0) // first time or just deserialized
514: {
515: idForToString = nextId++;
516: }
517: }
518:
519: var name = getClass().getSimpleName();
520:
521:• if ("".equals(name))
522: {
523: name = getClass().getName().replaceAll("^.*\\.", "");
524: }
525:
526: return String.format("%s@%x[%d tags]", name, idForToString, valueMapByCode.size());
527: }
528:
529: /*******************************************************************************************************************
530: *
531: ******************************************************************************************************************/
532: protected synchronized void touch()
533: {
534: // latestModificationTime.setTime(System.currentTimeMillis()) breaks firePropertyChange()
535: latestModificationTime = Instant.now();
536: }
537:
538: /*******************************************************************************************************************
539: *
540: * Tries to convert a value to the target type.
541: *
542: * @param <T> the static type to convert to
543: * @param value the value to convert
544: * @param toType the dynamic type to convert to
545: * @param code the tag code
546: * @return the converted value
547: *
548: ******************************************************************************************************************/
549: @Nonnull
550: private <T> T cast (@Nonnull Object value, @Nonnull final Class<T> toType, @Nonnegative final int code)
551: {
552:• if (toType.equals(Object.class))
553: {
554: return toType.cast(value);
555: }
556:
557:• if (value instanceof Number)
558: {
559: try
560: {
561:• if (toType.isEnum())
562: {
563: final var fromIntegerMethod = toType.getMethod("fromInteger", int.class);
564: value = fromIntegerMethod.invoke(null, value);
565: }
566: }
567: catch (Exception e)
568: {
569: throw new RuntimeException(e);
570: }
571:
572: // Handle promotions
573:• if (((value instanceof Short) || (value instanceof Byte)) && toType.equals(Integer.class))
574: {
575: value = ((Number)value).intValue();
576: }
577:• else if (((value instanceof Short) || (value instanceof Integer) || (value instanceof Byte)) &&
578:• toType.equals(Long.class))
579: {
580: value = (long)((Number)value).intValue();
581: }
582: }
583:
584:• if ((value instanceof long[][]) && Rational.class.equals(toType))
585: {
586: final var array = (long[][])value;
587: value = Rational.of((int)array[0][0], (int)array[0][1]);
588: }
589:
590:• if (value instanceof StringValue)
591: {
592: value = ((StringValue)value).toString(UTF_8);
593: }
594:
595: // If an array is asked and a scalar is available, convert it to an array[1]
596:• if (toType.isArray() && !value.getClass().isArray())
597: {
598: final var array = Array.newInstance(toType.getComponentType(), 1);
599: Array.set(array, 0, value);
600: value = array;
601: }
602:
603: return toType.cast(value);
604: }
605:
606: /*******************************************************************************************************************
607: *
608: *
609: ******************************************************************************************************************/
610: private static boolean equals (final Object o1, final Object o2) // FIXME: check if Objects.deepEquals() would do
611: {
612:• if (o1 == null)
613: {
614:• return o2 == null;
615: }
616:
617:• if (o1.getClass().isArray())
618: {
619: final var length = Array.getLength(o1);
620:
621:• if (length != Array.getLength(o2))
622: {
623: return false;
624: }
625:
626:• for (var i = 0; i < length; i++)
627: {
628: return equals(Array.get(o1, i), Array.get(o2, i));
629: }
630: }
631:
632: return o1.equals(o2);
633: }
634:
635: /*******************************************************************************************************************
636: *
637: *
638: ******************************************************************************************************************/
639: @Override
640: public final int hashCode()
641: {
642: var hash = 5;
643:
644:• for (final var tag : getTagCodes())
645: {
646: final var object = getRaw(tag);
647:• hash = 67 * hash + (object != null ? object.hashCode() : 0);
648: }
649:
650: // hash = 67 * hash + (this.tagMap != null ? this.tagMap.hashCode() : 0);
651: // hash = 67 * hash + (this.directoryMap != null ? this.directoryMap.hashCode() : 0);
652: // FIXME
653: // hash = 67 * hash + (this.latestModificationTime != null ? this.latestModificationTime.hashCode() : 0);
654: return hash;
655: }
656:
657: /*******************************************************************************************************************
658: *
659: * @return
660: *
661: ******************************************************************************************************************/
662: protected boolean isSubClass (@Nonnull Class aClass, final @Nonnull String ancestorClassName)
663: {
664:• for (; aClass != null; aClass = aClass.getSuperclass())
665: {
666:• if (aClass.getName().equals(ancestorClassName))
667: {
668: return true;
669: }
670: }
671:
672: return false;
673: }
674:
675: /*******************************************************************************************************************
676: *
677: ******************************************************************************************************************/
678: protected static String formatDateTime (final Instant date)
679: {
680:• if (date == null)
681: {
682: return null;
683: }
684:
685: return EXIF_DATE_TIME_FORMATTERS.get(0).format(date);
686: }
687:
688: /*******************************************************************************************************************
689: *
690: ******************************************************************************************************************/
691: protected static Instant parseDateTime (final String string)
692: {
693:• if (string == null)
694: {
695: return null;
696: }
697:
698: final var defaultZoneOffset = ZoneOffset.UTC; // of(ZoneOffset.systemDefault().getId());
699:
700: final var instant = EXIF_DATE_TIME_FORMATTERS.stream().flatMap(f ->
701: {
702: try
703: {
704: return Stream.of(LocalDateTime.parse(string, f).toInstant(defaultZoneOffset));
705: }
706: catch (Exception e)
707: {
708: return Stream.empty();
709: }
710: }).findFirst();
711:
712:• if (instant.isEmpty())
713: {
714: log.warn("*** BAD DATE " + string);
715: }
716:
717: return instant.orElse(null);
718: }
719:
720: /*******************************************************************************************************************
721: *
722: * Converts a tag code to a {@code Tag}. If the tag code is unknown, a new instance of {@code Tag} is created on
723: * the fly.
724: *
725: * @param code the tag code
726: * @return the tag
727: *
728: ******************************************************************************************************************/
729: @Nonnull
730: private Tag<?> toTag (@Nonnegative final int code)
731: {
732: return getTagInfo(code).orElseGet(() -> Tag.of(code, "" + code, "" + code, Object.class));
733: }
734: }