<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../../jacoco-resources/report.gif" type="image/gif"/><title>Directory.java</title><link rel="stylesheet" href="../../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../../index.html" class="el_report">Mistral Renderer</a> > <a href="../index.html" class="el_bundle">image-core</a> > <a href="index.source.html" class="el_package">it.tidalwave.image.metadata</a> > <span class="el_source">Directory.java</span></div><h1>Directory.java</h1><pre class="source lang-java linenums">/*
* *********************************************************************************************************************
*
* Mistral: open source imaging engine
* http://tidalwave.it/projects/mistral
*
* Copyright (C) 2003 - 2023 by Tidalwave s.a.s. (http://tidalwave.it)
*
* *********************************************************************************************************************
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* *********************************************************************************************************************
*
* git clone https://bitbucket.org/tidalwave/mistral-src
* git clone https://github.com/tidalwave-it/mistral-src
*
* *********************************************************************************************************************
*/
package it.tidalwave.image.metadata;
import java.lang.reflect.Array;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.io.Serializable;
import com.drew.metadata.StringValue;
import it.tidalwave.image.Rational;
import it.tidalwave.image.metadata.loader.DirectoryLoader;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import static java.util.stream.Collectors.*;
import static java.nio.charset.StandardCharsets.UTF_8;
/***********************************************************************************************************************
*
* This class provides basic support for all kinds of metadata such EXIF, IPTC or maker notes.
*
* @author Fabrizio Giudici
*
**********************************************************************************************************************/
<span class="fc" id="L69">@Slf4j</span>
public class Directory extends JavaBeanSupport implements Serializable
{
/*******************************************************************************************************************
*
* A descriptor for a tag.
*
* @param <T> the type of the tag
*
**********************************************************************************************************************/
<span class="pc bpc" id="L79" title="35 of 38 branches missed."> @Immutable @RequiredArgsConstructor(staticName = "of") @Getter @ToString @EqualsAndHashCode</span>
public static class Tag<T>
{
<span class="fc" id="L82"> private final int code;</span>
@Nonnull
<span class="fc" id="L85"> private final String name;</span>
@Nonnull
<span class="nc" id="L88"> private final String propertyName;</span>
@Nonnull
<span class="fc" id="L91"> private final Class<T> type;</span>
}
private static final long serialVersionUID = 308812466726854722L;
<span class="fc" id="L95"> private static final List<DateTimeFormatter> EXIF_DATE_TIME_FORMATTERS =</span>
<span class="fc" id="L96"> Stream.of("yyyy:MM:dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss")</span>
<span class="fc" id="L97"> .map(DateTimeFormatter::ofPattern).collect(toList());;</span>
<span class="fc" id="L99"> protected final static Map<String, Tag> tagMapByCode = new HashMap<>();</span>
<span class="pc" id="L101"> private final Map<Integer, Object> valueMapByCode = new HashMap<>();</span>
<span class="pc" id="L103"> private final Map<String, Directory> directoryMapByName = new HashMap<>();</span>
<span class="pc" id="L105"> private Instant latestModificationTime = Instant.now();</span>
<span class="fc" id="L107"> private static int nextId = 1;</span>
private transient int idForToString;
/*******************************************************************************************************************
*
* Creates an empty directory.
*
******************************************************************************************************************/
public Directory()
<span class="fc" id="L117"> {</span>
<span class="fc" id="L118"> }</span>
/*******************************************************************************************************************
*
* Creates an empty directory with a given latest modification time.
*
* @param latestModificationTime the latest modification time
*
******************************************************************************************************************/
public Directory (final @Nonnull Instant latestModificationTime)
<span class="nc" id="L128"> {</span>
<span class="nc" id="L129"> this.latestModificationTime = latestModificationTime;</span>
<span class="nc" id="L130"> }</span>
/*******************************************************************************************************************
*
* Returns a value given its tag. The result is converted to the standard type of the tag (e.g. an enum).
*
* @param <T> the static type of the tag
* @param tag the tag to retrieve
* @return the value
*
******************************************************************************************************************/
@Nonnull
public <T> Optional<T> get (@Nonnull final Tag<T> tag)
{
<span class="nc" id="L144"> return get(tag, tag.getType());</span>
}
/*******************************************************************************************************************
*
* Returns a value given its tag. The result is converted to the specified type, is possible.
*
* @param <U> the static type of the tag
* @param <T> the static type of the type to convert
* @param tag the tag to retrieve
* @param asType the type to convert the value into
* @return the value
*
******************************************************************************************************************/
@Nonnull
public <T, U> Optional<T> get (@Nonnull final Tag<U> tag, @Nonnull final Class<T> asType)
{
<span class="fc" id="L161"> return get(tag.code, asType);</span>
}
/*******************************************************************************************************************
*
* Returns a value given its tag. The result is converted to the specified type, is possible.
*
* @param <T> the static type of the type to convert
* @param code the code of the tag to retrieve
* @param asType the type to convert the value into
* @return the value
*
******************************************************************************************************************/
@Nonnull
public <T> Optional<T> get (@Nonnegative final int code, @Nonnull final Class<T> asType)
{
<span class="fc" id="L177"> return Optional.ofNullable(getRaw(code)).map(v -> cast(v, asType, code));</span>
}
/*******************************************************************************************************************
*
* Returns a value given its tag code. No type conversion is applied.
*
* @param code the code of the tag to retrieve
* @return the value (can be null)
*
******************************************************************************************************************/
@CheckForNull
public Object getRaw (@Nonnegative final int code)
{
<span class="fc" id="L191"> return valueMapByCode.get(code);</span>
}
/*******************************************************************************************************************
*
* Sets a value for a tag. {@code null} and {@link Optional} are accepted and
* eventually unpacked: passing {@code null} or an empty {@code Optional} is equivalent to a call to
* {@link #remove(int)}.
*
* If the value is different from the previous one, events are fired:
*
* <ul>
* <li>{@code the property name}</li>
* <li>{@code empty}</li>
* <li>{@code latestModificationTime}</li>
* </ul>
*
* @param tag the tag to retrieve
* @param value the new value
*
******************************************************************************************************************/
public void set (final @Nonnull Tag<?> tag, Object value)
{
<span class="nc" id="L214"> final var oldValue = getRaw(tag.code);</span>
<span class="nc" id="L215"> final var oldEmpty = isEmpty();</span>
<span class="nc" id="L216"> final var oldLatestModificationTime = getLatestModificationTime();</span>
<span class="nc" id="L217"> setRaw(tag.code, value); // FIXME: reverse cast</span>
<span class="nc" id="L218"> pcs.firePropertyChange(tag.propertyName, oldValue, tag.propertyName);</span>
<span class="nc" id="L219"> pcs.firePropertyChange("empty", oldEmpty, isEmpty());</span>
<span class="nc" id="L220"> pcs.firePropertyChange("latestModificationTime", oldLatestModificationTime, getLatestModificationTime());</span>
<span class="nc" id="L221"> }</span>
/*******************************************************************************************************************
*
* Sets a raw value, that is without converting any type. {@code null} and {@link Optional} are accepted and
* eventually unpacked: passing {@code null} or an empty {@code Optional} is equivalent to a call to
* {@link #remove(int)}.
*
* This method does not fire events.
*
* @param code the code of the tag to set
* @param value the value
*
******************************************************************************************************************/
public void setRaw (final @Nonnegative int code, Object value)
{
<span class="nc bnc" id="L237" title="All 4 branches missed."> if ((value != null) && (value instanceof Optional))</span>
{
<span class="nc" id="L239"> value = (((Optional<?>)value).orElse(null));</span>
}
<span class="nc bnc" id="L242" title="All 2 branches missed."> if (value == null)</span>
{
<span class="nc" id="L244"> remove(code);</span>
<span class="nc" id="L245"> return;</span>
}
<span class="nc bnc" id="L248" title="All 2 branches missed."> if (value.getClass().isEnum())</span>
{
try
{
<span class="nc" id="L252"> final var getValueMethod = value.getClass().getMethod("getValue");</span>
<span class="nc" id="L253"> value = getValueMethod.invoke(value);</span>
}
<span class="nc" id="L255"> catch (Exception e)</span>
{
<span class="nc" id="L257"> throw new RuntimeException(e);</span>
<span class="nc" id="L258"> }</span>
}
<span class="nc" id="L261"> valueMapByCode.put(code, value);</span>
<span class="nc" id="L262"> touch();</span>
<span class="nc" id="L263"> }</span>
/*******************************************************************************************************************
*
* Verifies if a value is present.
*
* @param code the code of the tag
* @return if the value is present
*
******************************************************************************************************************/
public boolean contains (final @Nonnegative int code)
{
<span class="nc" id="L275"> return valueMapByCode.containsKey(code);</span>
}
/*******************************************************************************************************************
*
* Removes a value. This method does not fire events.
*
* @param code the code of the tag to remove
* @return if the value is present
*
******************************************************************************************************************/
public void remove (final @Nonnegative int code)
{
<span class="nc" id="L288"> valueMapByCode.remove(code);</span>
<span class="nc" id="L289"> touch();</span>
<span class="nc" id="L290"> }</span>
/*******************************************************************************************************************
*
* Returns information about a tag.
*
* @param tag the tag code
* @return the tag info
*
******************************************************************************************************************/
@Nonnull
public Optional<Tag<?>> getTagInfo (@Nonnegative final int tag)
{
<span class="fc" id="L303"> final var s = (getClass().getSimpleName() + "DirectoryGenerated").replaceAll("TIFF", "EXIF");</span>
<span class="fc" id="L304"> return Optional.ofNullable(tagMapByCode.get(s + "." + tag));</span>
}
/*******************************************************************************************************************
*
* Returns the tag codes contained in this directory, sorted by code.
*
* @return the tag codes
*
******************************************************************************************************************/
@Nonnull
public int[] getTagCodes()
{
<span class="fc" id="L317"> return valueMapByCode.keySet().stream().mapToInt(Integer::intValue).sorted().toArray();</span>
}
/*******************************************************************************************************************
*
* Returns the tags contained in this directory. Tags are sorted by code.
*
* @return the tags
*
******************************************************************************************************************/
@Nonnull
public Tag[] getTags()
{
<span class="nc" id="L330"> return valueMapByCode.keySet().stream().sorted().map(this::toTag).toArray(Tag[]::new);</span>
}
/*******************************************************************************************************************
*
* Iterates through all the tags calling the provided action.
*
* @param action the action to call
*
******************************************************************************************************************/
public void forEachTag (@Nonnull final Consumer<Tag<?>> action)
{
<span class="nc" id="L342"> IntStream.of(getTagCodes()).mapToObj(this::toTag).forEach(action);</span>
<span class="nc" id="L343"> }</span>
/*******************************************************************************************************************
*
* Iterates through all the tags and related raw values calling the provided action.
*
* @param action the action to call
*
******************************************************************************************************************/
public void forEachTag (@Nonnull final BiConsumer<Tag<?>, Object> action)
{
<span class="fc" id="L354"> IntStream.of(getTagCodes()).mapToObj(this::toTag).forEach(t -> action.accept(t, getRaw(t.getCode())));</span>
<span class="fc" id="L355"> }</span>
/*******************************************************************************************************************
*
* Iterates through all the tag codes calling the provided action.
*
* @param action the action to call
*
******************************************************************************************************************/
public void forEachTagCode (@Nonnull final IntConsumer action)
{
<span class="nc" id="L366"> IntStream.of(getTagCodes()).forEach(action);</span>
<span class="nc" id="L367"> }</span>
/*******************************************************************************************************************
*
* Checks whether this directory is empty.
*
* @return {@code true} if this directory doesn't contain any tag
*
******************************************************************************************************************/
public boolean isEmpty()
{
<span class="fc" id="L378"> return this.valueMapByCode.isEmpty();</span>
}
/*******************************************************************************************************************
*
* Returns the names of the available subdirectories.
*
* @return the names of subdirectories
*
******************************************************************************************************************/
@Nonnull
public Set<String> getSubDirectoryNames()
{
<span class="nc" id="L391"> return new CopyOnWriteArraySet<>(directoryMapByName.keySet());</span>
}
/*******************************************************************************************************************
*
* Returns a subdirectory given its name.
*
* @param name the name of the subdirectory
* @return the subdirectory
*
******************************************************************************************************************/
@Nonnull
public Optional<Directory> getSubDirectory (final @Nonnull String name)
{
<span class="nc" id="L405"> return Optional.ofNullable(directoryMapByName.get(name));</span>
}
/*******************************************************************************************************************
*
* Returns the latest modification time of this object.
*
* @return the latest modification time
*
******************************************************************************************************************/
@Nonnull
public Instant getLatestModificationTime()
{
<span class="nc" id="L418"> return latestModificationTime;</span>
}
/*******************************************************************************************************************
*
* Loads tags and subdirectories from the given loader.
*
******************************************************************************************************************/
public void load (final @Nonnull DirectoryLoader loader)
{
<span class="fc" id="L428"> log.debug("load({})", loader);</span>
<span class="fc bfc" id="L430" title="All 2 branches covered."> for (final var tag : loader.getTags())</span>
{
<span class="fc" id="L432"> valueMapByCode.put(tag, loader.getObject(tag));</span>
}
<span class="pc bpc" id="L435" title="1 of 2 branches missed."> for (final var directoryName : loader.getSubDirectoryNames())</span>
{
<span class="nc" id="L437"> final var directory = new Directory();</span>
<span class="nc" id="L438"> directory.load(loader.getSubDirectory(directoryName));</span>
<span class="nc" id="L439"> directoryMapByName.put(directoryName, directory);</span>
}
<span class="fc" id="L441"> }</span>
/*******************************************************************************************************************
*
*
******************************************************************************************************************/
@Override
public final boolean equals (final Object object)
{
<span class="nc bnc" id="L450" title="All 2 branches missed."> if (object == null)</span>
{
<span class="nc" id="L452"> return false;</span>
}
<span class="nc bnc" id="L455" title="All 2 branches missed."> if (getClass() != object.getClass())</span>
{
<span class="nc" id="L457"> return false;</span>
}
<span class="nc" id="L460"> final var other = (Directory)object;</span>
<span class="nc" id="L461"> final var myTags = getTagCodes();</span>
<span class="nc" id="L462"> final var otherTags = other.getTagCodes();</span>
<span class="nc bnc" id="L464" title="All 2 branches missed."> if (!Arrays.equals(myTags, otherTags))</span>
{
<span class="nc" id="L466"> return false;</span>
}
<span class="nc bnc" id="L469" title="All 2 branches missed."> for (final var tag : myTags)</span>
{
<span class="nc bnc" id="L471" title="All 2 branches missed."> if (!equals(getRaw(tag), other.getRaw(tag)))</span>
{
<span class="nc" id="L473"> return false;</span>
}
}
// if (this.tagMap != other.tagMap && (this.tagMap == null || !this.tagMap.equals(other.tagMap)))
// {
// return false;
// }
// FIXME if (this.directoryMap != other.directoryMap && (this.directoryMap == null || !this.directoryMap
// .equals(other.directoryMap)))
// {
// return false;
// }
// FIXME
// if (this.latestModificationTime != other.latestModificationTime && (this.latestModificationTime == null ||
// !this.latestModificationTime.equals(other.latestModificationTime)))
// {
// return false;
// }
<span class="nc" id="L495"> return true;</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public final String toString()
{
<span class="fc" id="L506"> synchronized (this)</span>
{
<span class="pc bpc" id="L508" title="1 of 2 branches missed."> if (idForToString == 0) // first time or just deserialized</span>
{
<span class="fc" id="L510"> idForToString = nextId++;</span>
}
<span class="fc" id="L512"> }</span>
<span class="fc" id="L514"> var name = getClass().getSimpleName();</span>
<span class="pc bpc" id="L516" title="1 of 2 branches missed."> if ("".equals(name))</span>
{
<span class="nc" id="L518"> name = getClass().getName().replaceAll("^.*\\.", "");</span>
}
<span class="fc" id="L521"> return String.format("%s@%x[%d tags]", name, idForToString, valueMapByCode.size());</span>
}
/*******************************************************************************************************************
*
******************************************************************************************************************/
protected synchronized void touch()
{
// latestModificationTime.setTime(System.currentTimeMillis()) breaks firePropertyChange()
<span class="nc" id="L530"> latestModificationTime = Instant.now();</span>
<span class="nc" id="L531"> }</span>
/*******************************************************************************************************************
*
* Tries to convert a value to the target type.
*
* @param <T> the static type to convert to
* @param value the value to convert
* @param toType the dynamic type to convert to
* @param code the tag code
* @return the converted value
*
******************************************************************************************************************/
@Nonnull
private <T> T cast (@Nonnull Object value, @Nonnull final Class<T> toType, @Nonnegative final int code)
{
<span class="pc bpc" id="L547" title="1 of 2 branches missed."> if (toType.equals(Object.class))</span>
{
<span class="nc" id="L549"> return toType.cast(value);</span>
}
<span class="fc bfc" id="L552" title="All 2 branches covered."> if (value instanceof Number)</span>
{
try
{
<span class="fc bfc" id="L556" title="All 2 branches covered."> if (toType.isEnum())</span>
{
<span class="fc" id="L558"> final var fromIntegerMethod = toType.getMethod("fromInteger", int.class);</span>
<span class="fc" id="L559"> value = fromIntegerMethod.invoke(null, value);</span>
}
}
<span class="nc" id="L562"> catch (Exception e)</span>
{
<span class="nc" id="L564"> throw new RuntimeException(e);</span>
<span class="fc" id="L565"> }</span>
// Handle promotions
<span class="pc bpc" id="L568" title="4 of 6 branches missed."> if (((value instanceof Short) || (value instanceof Byte)) && toType.equals(Integer.class))</span>
{
<span class="nc" id="L570"> value = ((Number)value).intValue();</span>
}
<span class="pc bpc" id="L572" title="2 of 6 branches missed."> else if (((value instanceof Short) || (value instanceof Integer) || (value instanceof Byte)) &&</span>
<span class="pc bpc" id="L573" title="1 of 2 branches missed."> toType.equals(Long.class))</span>
{
<span class="nc" id="L575"> value = (long)((Number)value).intValue();</span>
}
}
<span class="pc bpc" id="L579" title="3 of 4 branches missed."> if ((value instanceof long[][]) && Rational.class.equals(toType))</span>
{
<span class="nc" id="L581"> final var array = (long[][])value;</span>
<span class="nc" id="L582"> value = Rational.of((int)array[0][0], (int)array[0][1]);</span>
}
<span class="fc bfc" id="L585" title="All 2 branches covered."> if (value instanceof StringValue)</span>
{
<span class="fc" id="L587"> value = ((StringValue)value).toString(UTF_8);</span>
}
// If an array is asked and a scalar is available, convert it to an array[1]
<span class="pc bpc" id="L591" title="1 of 4 branches missed."> if (toType.isArray() && !value.getClass().isArray())</span>
{
<span class="nc" id="L593"> final var array = Array.newInstance(toType.getComponentType(), 1);</span>
<span class="nc" id="L594"> Array.set(array, 0, value);</span>
<span class="nc" id="L595"> value = array;</span>
}
<span class="fc" id="L598"> return toType.cast(value);</span>
}
/*******************************************************************************************************************
*
*
******************************************************************************************************************/
private static boolean equals (final Object o1, final Object o2) // FIXME: check if Objects.deepEquals() would do