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