Content of file Directory.java.html

<?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 Operations</a> &gt; <a href="../index.html" class="el_bundle">image-core</a> &gt; <a href="index.source.html" class="el_package">it.tidalwave.image.metadata</a> &gt; <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 &quot;License&quot;); 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 &quot;AS IS&quot; 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 &lt;T&gt;   the type of the tag
     *
     **********************************************************************************************************************/
<span class="pc bpc" id="L79" title="35 of 38 branches missed.">    @Immutable @RequiredArgsConstructor(staticName = &quot;of&quot;) @Getter @ToString @EqualsAndHashCode</span>
    public static class Tag&lt;T&gt;
      {
<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&lt;T&gt; type;</span>
      }

    private static final long serialVersionUID = 308812466726854722L;
<span class="fc" id="L95">    private static final List&lt;DateTimeFormatter&gt; EXIF_DATE_TIME_FORMATTERS =</span>
<span class="fc" id="L96">            Stream.of(&quot;yyyy:MM:dd HH:mm:ss&quot;, &quot;yyyy-MM-dd'T'HH:mm:ss&quot;)</span>
<span class="fc" id="L97">                  .map(DateTimeFormatter::ofPattern).collect(toList());;</span>

<span class="fc" id="L99">    protected final static Map&lt;String, Tag&gt; tagMapByCode = new HashMap&lt;&gt;();</span>

<span class="pc" id="L101">    private final Map&lt;Integer, Object&gt; valueMapByCode = new HashMap&lt;&gt;();</span>

<span class="pc" id="L103">    private final Map&lt;String, Directory&gt; directoryMapByName = new HashMap&lt;&gt;();</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     &lt;T&gt;     the static type of the tag
     * @param     tag     the tag to retrieve
     * @return            the value
     *
     ******************************************************************************************************************/
    @Nonnull
    public &lt;T&gt; Optional&lt;T&gt; get (@Nonnull final Tag&lt;T&gt; 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     &lt;U&gt;     the static type of the tag
     * @param     &lt;T&gt;     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 &lt;T, U&gt; Optional&lt;T&gt; get (@Nonnull final Tag&lt;U&gt; tag, @Nonnull final Class&lt;T&gt; 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     &lt;T&gt;     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 &lt;T&gt; Optional&lt;T&gt; get (@Nonnegative final int code, @Nonnull final Class&lt;T&gt; asType)
      {
<span class="fc" id="L177">        return Optional.ofNullable(getRaw(code)).map(v -&gt; 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:
     *
     * &lt;ul&gt;
     *   &lt;li&gt;{@code the property name}&lt;/li&gt;
     *   &lt;li&gt;{@code empty}&lt;/li&gt;
     *   &lt;li&gt;{@code latestModificationTime}&lt;/li&gt;
     * &lt;/ul&gt;
     *
     * @param     tag       the tag to retrieve
     * @param     value     the new value
     *
     ******************************************************************************************************************/
    public void set (final @Nonnull Tag&lt;?&gt; 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(&quot;empty&quot;, oldEmpty, isEmpty());</span>
<span class="nc" id="L220">        pcs.firePropertyChange(&quot;latestModificationTime&quot;, 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) &amp;&amp; (value instanceof Optional))</span>
          {
<span class="nc" id="L239">            value = (((Optional&lt;?&gt;)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(&quot;getValue&quot;);</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&lt;Tag&lt;?&gt;&gt; getTagInfo (@Nonnegative final int tag)
      {
<span class="fc" id="L303">        final var s = (getClass().getSimpleName() + &quot;DirectoryGenerated&quot;).replaceAll(&quot;TIFF&quot;, &quot;EXIF&quot;);</span>
<span class="fc" id="L304">        return Optional.ofNullable(tagMapByCode.get(s + &quot;.&quot; + 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&lt;Tag&lt;?&gt;&gt; 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&lt;Tag&lt;?&gt;, Object&gt; action)
      {
<span class="fc" id="L354">        IntStream.of(getTagCodes()).mapToObj(this::toTag).forEach(t -&gt; 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&lt;String&gt; getSubDirectoryNames()
      {
<span class="nc" id="L391">        return new CopyOnWriteArraySet&lt;&gt;(directoryMapByName.keySet());</span>
      }

    /*******************************************************************************************************************
     *
     * Returns a subdirectory given its name.
     *
     * @param     name      the name of the subdirectory
     * @return              the subdirectory
     *
     ******************************************************************************************************************/
    @Nonnull
    public Optional&lt;Directory&gt; 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(&quot;load({})&quot;, 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 &amp;&amp; (this.tagMap == null || !this.tagMap.equals(other.tagMap)))
//          {
//            return false;
//          }

// FIXME        if (this.directoryMap != other.directoryMap &amp;&amp; (this.directoryMap == null || !this.directoryMap
//  .equals(other.directoryMap)))
//          {
//            return false;
//          }

// FIXME
//        if (this.latestModificationTime != other.latestModificationTime &amp;&amp; (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 (&quot;&quot;.equals(name))</span>
          {
<span class="nc" id="L518">            name = getClass().getName().replaceAll(&quot;^.*\\.&quot;, &quot;&quot;);</span>
          }

<span class="fc" id="L521">        return String.format(&quot;%s@%x[%d tags]&quot;, 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     &lt;T&gt;     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 &lt;T&gt; T cast (@Nonnull Object value, @Nonnull final Class&lt;T&gt; 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(&quot;fromInteger&quot;, 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)) &amp;&amp; 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)) &amp;&amp;</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[][]) &amp;&amp; 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() &amp;&amp; !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
check if Objects.deepEquals() would do
{ <span class="nc bnc" id="L607" title="All 2 branches missed."> if (o1 == null)</span> { <span class="nc bnc" id="L609" title="All 2 branches missed."> return o2 == null;</span> } <span class="nc bnc" id="L612" title="All 2 branches missed."> if (o1.getClass().isArray())</span> { <span class="nc" id="L614"> final var length = Array.getLength(o1);</span> <span class="nc bnc" id="L616" title="All 2 branches missed."> if (length != Array.getLength(o2))</span> { <span class="nc" id="L618"> return false;</span> } <span class="nc bnc" id="L621" title="All 2 branches missed."> for (var i = 0; i &lt; length; i++)</span> { <span class="nc" id="L623"> return equals(Array.get(o1, i), Array.get(o2, i));</span> } } <span class="nc" id="L627"> return o1.equals(o2);</span> } /******************************************************************************************************************* * * ******************************************************************************************************************/ @Override public final int hashCode() { <span class="nc" id="L637"> var hash = 5;</span> <span class="nc bnc" id="L639" title="All 2 branches missed."> for (final var tag : getTagCodes())</span> { <span class="nc" id="L641"> final var object = getRaw(tag);</span> <span class="nc bnc" id="L642" title="All 2 branches missed."> hash = 67 * hash + (object != null ? object.hashCode() : 0);</span> } // hash = 67 * hash + (this.tagMap != null ? this.tagMap.hashCode() : 0); // hash = 67 * hash + (this.directoryMap != null ? this.directoryMap.hashCode() : 0); // FIXME // hash = 67 * hash + (this.latestModificationTime != null ? this.latestModificationTime.hashCode() : 0); <span class="nc" id="L649"> return hash;</span> } /******************************************************************************************************************* * * @param array * @return * ******************************************************************************************************************/ @Nonnull public String toHexString (final @Nonnull Rational[] array) { <span class="nc" id="L661"> final var buffer = new StringBuilder();</span> <span class="nc bnc" id="L663" title="All 2 branches missed."> for (var i = 0; i &lt; array.length; i++)</span> { <span class="nc bnc" id="L665" title="All 2 branches missed."> if (i &gt; 0)</span> { <span class="nc" id="L667"> buffer.append(&quot;,&quot;);</span> } <span class="nc" id="L670"> buffer.append(array[i].toString());</span> } <span class="nc" id="L673"> return buffer.toString();</span> } /******************************************************************************************************************* * * @return * ******************************************************************************************************************/ protected boolean isSubClass (@Nonnull Class aClass, final @Nonnull String ancestorClassName) { <span class="nc bnc" id="L683" title="All 2 branches missed."> for (; aClass != null; aClass = aClass.getSuperclass())</span> { <span class="nc bnc" id="L685" title="All 2 branches missed."> if (aClass.getName().equals(ancestorClassName))</span> { <span class="nc" id="L687"> return true;</span> } } <span class="nc" id="L691"> return false;</span> } /******************************************************************************************************************* * ******************************************************************************************************************/ protected static String formatDateTime (final Instant date) { <span class="nc bnc" id="L699" title="All 2 branches missed."> if (date == null)</span> { <span class="nc" id="L701"> return null;</span> } <span class="nc" id="L704"> return EXIF_DATE_TIME_FORMATTERS.get(0).format(date);</span> } /******************************************************************************************************************* * ******************************************************************************************************************/ protected static Instant parseDateTime (final String string) { <span class="pc bpc" id="L712" title="1 of 2 branches missed."> if (string == null)</span> { <span class="nc" id="L714"> return null;</span> } <span class="fc" id="L717"> final var defaultZoneOffset = ZoneOffset.UTC; // of(ZoneOffset.systemDefault().getId());</span> <span class="fc" id="L719"> final var instant = EXIF_DATE_TIME_FORMATTERS.stream().flatMap(f -&gt;</span> { try { <span class="fc" id="L723"> return Stream.of(LocalDateTime.parse(string, f).toInstant(defaultZoneOffset));</span> } <span class="fc" id="L725"> catch (Exception e)</span> { <span class="fc" id="L727"> return Stream.empty();</span> } <span class="fc" id="L729"> }).findFirst();</span> <span class="pc bpc" id="L731" title="1 of 2 branches missed."> if (instant.isEmpty())</span> { <span class="nc" id="L733"> log.warn(&quot;*** BAD DATE &quot; + string);</span> } <span class="fc" id="L736"> return instant.orElse(null);</span> } /******************************************************************************************************************* * ******************************************************************************************************************/ @Nonnull private Tag&lt;?&gt; toTag (@Nonnegative final int code) { <span class="fc" id="L745"> return getTagInfo(code).orElseGet(() -&gt; Tag.of(code, &quot;&quot; + code, &quot;&quot; + code, Object.class));</span> } } </pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.7.202105040129</span></div></body></html>