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>
reverse cast
<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 { <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>