All Downloads are FREE. Search and download functionalities are using the official Maven repository.

info.freelibrary.iiif.presentation.v3.ids.DefaultMinter Maven / Gradle / Ivy

There is a newer version: 0.12.4
Show newest version

package info.freelibrary.iiif.presentation.v3.ids;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;

import org.paukov.combinatorics3.Generator;

import info.freelibrary.util.I18nRuntimeException;
import info.freelibrary.util.Logger;
import info.freelibrary.util.LoggerFactory;
import info.freelibrary.util.Stopwatch;
import info.freelibrary.util.StringUtils;
import info.freelibrary.util.warnings.PMD;

import info.freelibrary.iiif.presentation.v3.Annotation;
import info.freelibrary.iiif.presentation.v3.AnnotationPage;
import info.freelibrary.iiif.presentation.v3.Canvas;
import info.freelibrary.iiif.presentation.v3.CanvasResource;
import info.freelibrary.iiif.presentation.v3.Manifest;
import info.freelibrary.iiif.presentation.v3.PaintingAnnotation;
import info.freelibrary.iiif.presentation.v3.Range;
import info.freelibrary.iiif.presentation.v3.SupplementingAnnotation;
import info.freelibrary.iiif.presentation.v3.utils.MessageCodes;

/**
 * Mints intra-manifest IDs using predictable ID templates.
 */
class DefaultMinter implements Minter {

    /**
     * The default minter's logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultMinter.class, MessageCodes.BUNDLE);

    /**
     * A template for supplying canvas IDs.
     */
    private static final String CANVAS_ID_TEMPLATE = "{}/canvas-{}";

    /**
     * A template for supplying range IDs.
     */
    private static final String RANGE_ID_TEMPLATE = "{}/range-{}";

    /**
     * A template for supplying annotation IDs.
     */
    private static final String ANNO_ID_TEMPLATE = "{}/annotations/anno-{}";

    /**
     * A template for supplying page IDs.
     */
    private static final String PAGE_ID_TEMPLATE = "{}/anno-page-{}";

    /**
     * All the alpha-numeric characters we use in creating NOIDs; lower case L is not used because it looks like "1".
     */
    private static final Character[] CHARS = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'o',
        'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0' };

    /**
     * The maximum number of NOIDs, given the size of our character array.
     */
    private static final int MAX_NOID_COUNT = 1_500_625;

    /**
     * The list of NOIDs.
     */
    private static final List NOIDS; // Static array is initialized just once

    /**
     * The static initialization of the NOID list.
     */
    static {
        final Stopwatch stopwatch = new Stopwatch().start();
        final List noids = new ArrayList<>();

        // Create a list of NOIDs for the manuscript to use in constructing IDs
        Generator.permutation(CHARS).withRepetitions(4).stream().forEach(charList -> {
            noids.add(charList.stream().map(String::valueOf).collect(Collectors.joining()));
        });

        // Shuffle them so they appear to be random
        Collections.shuffle(noids);

        // Finalize the NOIDs List for all future minters
        NOIDS = Collections.unmodifiableList(noids);
        LOGGER.debug(MessageCodes.JPA_101, stopwatch.stop().getSeconds());

        // Do a sanity check on the number of NOIDs in our list
        if (MAX_NOID_COUNT != NOIDS.size()) {
            throw new I18nRuntimeException(MessageCodes.BUNDLE, MessageCodes.JPA_102, NOIDS.size(), MAX_NOID_COUNT);
        }
    }

    /**
     * The existing IDs.
     */
    private final Set myExistingIDs = new HashSet<>();

    /**
     * A NOID iterator.
     */
    private final Iterator myIterator;

    /**
     * A manifest ID.
     */
    private final URI myManifestID;

    /**
     * The number of used NOIDs.
     */
    private int myUsedNOIDs;

    /**
     * Creates a manifest component ID minter.
     *
     * @param aManifest The manifest for which to mint IDs
     */
    DefaultMinter(final Manifest aManifest) {
        myManifestID = aManifest.getID();
        myIterator = new NoidIterator();

        // Record which IDs are already in use by this manifest
        findPreexistingIDs(aManifest);
    }

    /**
     * A minter that just takes a manifest ID. It won't know about IDs that already exist in a manifest.
     *
     * @param aManifestID A manifest ID
     */
    DefaultMinter(final URI aManifestID) {
        myManifestID = aManifestID;
        myIterator = new NoidIterator();
    }

    /**
     * Gets the manifest ID associated with this minter.
     *
     * @return The manifest ID associated with this minter
     */
    @Override
    public URI getManifestID() {
        return myManifestID;
    }

    /**
     * Gets a new canvas ID.
     *
     * @return An ID to use on a canvas
     */
    @Override
    public URI getCanvasID() {
        try {
            final URI id = URI.create(StringUtils.format(CANVAS_ID_TEMPLATE, myManifestID, myIterator.next()));
            return myExistingIDs.contains(id) ? getCanvasID() : increment(id);
        } catch (final NoSuchElementException details) {
            throw new MintingException(details, MessageCodes.JPA_105, myManifestID, Canvas.class.getSimpleName());
        }
    }

    /**
     * Gets a new annotation ID.
     *
     * @return An ID to use on an annotation
     */
    @Override
    public URI getAnnotationID() {
        try {
            final URI id = URI.create(StringUtils.format(ANNO_ID_TEMPLATE, myManifestID, myIterator.next()));
            return myExistingIDs.contains(id) ? getAnnotationID() : increment(id);
        } catch (final NoSuchElementException details) {
            throw new MintingException(details, MessageCodes.JPA_105, myManifestID, Annotation.class.getSimpleName());
        }
    }

    // Could also do an annotation ID that lives under an AnnotationPage

    /**
     * Gets a new annotation page ID.
     *
     * @param  A type of canvas
     * @param aCanvasResource The canvas that the annotation page is going onto
     * @return An ID to use on the supplied annotation page
     */
    @Override
    public > URI getAnnotationPageID(final CanvasResource aCanvasResource) {
        try {
            final URI canvasID = aCanvasResource.getID();
            final URI id = URI.create(StringUtils.format(PAGE_ID_TEMPLATE, canvasID, myIterator.next()));

            return myExistingIDs.contains(id) ? getAnnotationPageID(aCanvasResource) : increment(id);
        } catch (final NoSuchElementException details) {
            throw new MintingException(details, MessageCodes.JPA_105, myManifestID,
                    AnnotationPage.class.getSimpleName());
        }
    }

    /**
     * Gets a new range ID.
     *
     * @return An ID to use on a range
     */
    @Override
    public URI getRangeID() {
        try {
            final URI id = URI.create(StringUtils.format(RANGE_ID_TEMPLATE, myManifestID, myIterator.next()));
            return myExistingIDs.contains(id) ? getRangeID() : increment(id);
        } catch (final NoSuchElementException details) {
            throw new MintingException(details, MessageCodes.JPA_105, myManifestID, Range.class.getSimpleName());
        }
    }

    /**
     * Gets total number of IDs that this minter can mint.
     *
     * @return The number of IDs that this minter can mint
     */
    @Override
    public int size() {
        return NOIDS.size();
    }

    /**
     * Gets the number of IDs that are available for use.
     *
     * @return The number of IDs that are available for use
     */
    @Override
    public int remaining() {
        return NOIDS.size() - (myUsedNOIDs + myExistingIDs.size());
    }

    /**
     * Returns whether there is another ID available to be minted.
     *
     * @return Whether there is another ID available to be minted
     */
    @Override
    public boolean hasNext() {
        return myIterator.hasNext();
    }

    /**
     * Increments the ID count, returning the ID to be used.
     *
     * @param aID An ID to be used
     * @return The ID to be used
     */
    private URI increment(final URI aID) {
        myUsedNOIDs += 1;
        return aID;
    }

    /**
     * Find IDs already associated with this manifest. This doesn't look for all IDs, but just the ones that a minter
     * might create.
     *
     * @param aManifest A supplied manifest
     */
    private void findPreexistingIDs(final Manifest aManifest) {
        for (final Canvas canvas : aManifest.getCanvases()) {
            if (!myExistingIDs.add(canvas.getID())) {
                throw new MintingException(MessageCodes.JPA_100, canvas.getID());
            }

            for (final AnnotationPage paintingPage : canvas.getPaintingPages()) {
                if (!myExistingIDs.add(paintingPage.getID())) {
                    LOGGER.warn(MessageCodes.JPA_100, paintingPage.getID());
                }

                findAnnotationIDs(paintingPage.getAnnotations());
            }

            for (final AnnotationPage supplementingPage : canvas.getSupplementingPages()) {
                if (!myExistingIDs.add(supplementingPage.getID())) {
                    LOGGER.warn(MessageCodes.JPA_100, supplementingPage.getID());
                }

                findAnnotationIDs(supplementingPage.getAnnotations());
            }
        }

        aManifest.getRanges().forEach(range -> {
            if (!myExistingIDs.add(range.getID())) {
                LOGGER.warn(MessageCodes.JPA_100, range.getID());
            }
        });
    }

    /**
     * Gets the IDs from annotations.
     *
     * @param  A type of annotation
     * @param aAnnotationList A list of annotations
     */
    private > void findAnnotationIDs(final List aAnnotationList) {
        aAnnotationList.stream().forEach(annotation -> {
            if (!myExistingIDs.add(annotation.getID())) {
                LOGGER.warn(MessageCodes.JPA_100, annotation.getID());
            }
        });
    }

    /**
     * An iterator that returns NOIDs in a pseudo-random order.
     */
    private class NoidIterator implements Iterator {

        /**
         * The high end of an integer range used for randomization.
         */
        private static final int MAX_RANDOM_INT = 20;

        /**
         * Where the iterator started cycling through the array.
         */
        private final int myStart;

        /**
         * The number of NOIDs to skip in each iteration.
         */
        private int mySkipCount;

        /**
         * The index position for the next available NOID.
         */
        private int myIndex;

        /**
         * The number of NOIDs this iterator has returned.
         */
        private int myCount;

        /**
         * The number of times we've cycled through the array.
         */
        private int myIteration;

        /**
         * Creates a new NOID iterator with a randomized start and skip count.
         */
        NoidIterator() {
            final ThreadLocalRandom randomizer = ThreadLocalRandom.current();

            // The MAX_RANDOM_INT + 1 makes the int range inclusive
            myStart = randomizer.nextInt(0, MAX_RANDOM_INT + 1);
            mySkipCount = randomizer.nextInt(1, MAX_RANDOM_INT + 1);
            myIndex = myStart;

            LOGGER.trace(MessageCodes.JPA_106, myManifestID, myStart, mySkipCount);
        }

        @Override
        public boolean hasNext() {
            return myCount < MAX_NOID_COUNT;
        }

        @Override
        @SuppressWarnings(PMD.AVOID_DEEPLY_NESTED_IF_STMTS)
        public String next() {
            if (hasNext()) {
                final String noid = NOIDS.get(myIndex);

                // Check to see if we've retrieved all the possible NOIDs and increment if not
                if (++myCount < MAX_NOID_COUNT) {
                    myIndex += mySkipCount;

                    // When at the end of an iteration cycle, reset the index to a new start
                    if (myIndex >= MAX_NOID_COUNT) {
                        myIndex = ++myIteration + myStart;

                        // Loop around to get the remaining ones from the start of the array
                        if (myIndex >= mySkipCount + myStart) { // NOPMD
                            mySkipCount = 1;
                            myIndex = 0;
                        }
                    }
                }

                return noid;
            } else {
                throw new IndexOutOfBoundsException(myIndex);
            }
        }

    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy