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

info.freelibrary.iiif.presentation.v3.Manifest Maven / Gradle / Ivy

There is a newer version: 0.12.4
Show newest version

package info.freelibrary.iiif.presentation.v3; // NOPMD

import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.core.JsonProcessingException;

import info.freelibrary.util.Logger;
import info.freelibrary.util.LoggerFactory;
import info.freelibrary.util.warnings.PMD;

import info.freelibrary.iiif.presentation.v3.properties.Behavior;
import info.freelibrary.iiif.presentation.v3.properties.Homepage;
import info.freelibrary.iiif.presentation.v3.properties.Label;
import info.freelibrary.iiif.presentation.v3.properties.Metadata;
import info.freelibrary.iiif.presentation.v3.properties.PartOf;
import info.freelibrary.iiif.presentation.v3.properties.Provider;
import info.freelibrary.iiif.presentation.v3.properties.Rendering;
import info.freelibrary.iiif.presentation.v3.properties.RequiredStatement;
import info.freelibrary.iiif.presentation.v3.properties.SeeAlso;
import info.freelibrary.iiif.presentation.v3.properties.Start;
import info.freelibrary.iiif.presentation.v3.properties.Summary;
import info.freelibrary.iiif.presentation.v3.properties.ViewingDirection;
import info.freelibrary.iiif.presentation.v3.properties.behaviors.ManifestBehavior;
import info.freelibrary.iiif.presentation.v3.utils.JSON;
import info.freelibrary.iiif.presentation.v3.utils.JsonKeys;
import info.freelibrary.iiif.presentation.v3.utils.MessageCodes;
import info.freelibrary.iiif.presentation.v3.utils.URIs;

/**
 * The overall description of the structure and properties of the digital representation of an object. It carries
 * information needed for the viewer to present the digitized content to the user, such as a title and other descriptive
 * information about the object or the intellectual work that it conveys. Each manifest describes how to present a
 * single object such as a book, a photograph, or a statue.
 */
@SuppressWarnings({ PMD.EXCESSIVE_PUBLIC_COUNT, PMD.EXCESSIVE_IMPORTS })
public class Manifest extends NavigableResource implements Resource { // NOPMD

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

    /**
     * The default number of contexts.
     */
    private static final int DEFAULT_CONTEXT_COUNT = 1;

    /**
     * The manifest's contexts.
     */
    private final List myContexts = Stream.of(URIs.CONTEXT_URI).collect(Collectors.toList());

    /**
     * The manifest's annotations.
     */
    private List>> myAnnotations;

    /**
     * The manifest's accompanying canvas.
     */
    private AccompanyingCanvas myAccompanyingCanvas;

    /**
     * The manifest's placeholder canvas.
     */
    private PlaceholderCanvas myPlaceholderCanvas;

    /**
     * The manifest's viewing direction.
     */
    private ViewingDirection myViewingDirection;

    /**
     * The manifest's service definitions.
     */
    private List> myServiceDefinitions;

    /**
     * The manifest's canvases.
     */
    private List myCanvases;

    /**
     * The manifest's ranges.
     */
    private List myRanges;

    /**
     * The manifest's start.
     */
    private Start myStart;

    /**
     * Creates a new manifest from the supplied ID and label.
     *
     * @param aID A manifest ID in string form
     * @param aLabel A manifest label in string form
     * @throws IllegalArgumentException If the supplied ID is not a valid URI
     */
    public Manifest(final String aID, final String aLabel) {
        super(ResourceTypes.MANIFEST, aID, aLabel);
    }

    /**
     * Creates a new manifest from the supplied ID and label.
     *
     * @param aID A manifest ID in string form
     * @param aLabel A manifest label
     * @throws IllegalArgumentException If the supplied ID is not a valid URI
     */
    public Manifest(final String aID, final Label aLabel) {
        super(ResourceTypes.MANIFEST, URI.create(aID), aLabel);
    }

    /**
     * Creates a new manifest from the supplied ID and label.
     *
     * @param aID A manifest ID
     * @param aLabel A manifest label
     */
    public Manifest(final URI aID, final Label aLabel) {
        super(ResourceTypes.MANIFEST, aID, aLabel);
    }

    /**
     * Creates a new manifest from the supplied ID, label, metadata, summary, thumbnail, and provider.
     *
     * @param aID A manifest ID in string form
     * @param aLabel A descriptive label in string form
     * @param aMetadataList A list of metadata properties
     * @param aSummary A summary in string form
     * @param aThumbnail A thumbnail
     * @param aProvider A resource provider
     */
    public Manifest(final String aID, final String aLabel, final List aMetadataList, final String aSummary,
            final ContentResource aThumbnail, final Provider aProvider) {
        super(ResourceTypes.MANIFEST, aID, aLabel, aMetadataList, aSummary, aThumbnail, aProvider);
    }

    /**
     * Creates a new manifest from the supplied ID, label, metadata, summary, thumbnail, and provider.
     *
     * @param aID A manifest ID in string form
     * @param aLabel A descriptive label
     * @param aMetadataList A list of metadata properties
     * @param aSummary A summary in string form
     * @param aThumbnail A thumbnail
     * @param aProvider A resource provider
     */
    public Manifest(final String aID, final Label aLabel, final List aMetadataList, final String aSummary,
            final ContentResource aThumbnail, final Provider aProvider) {
        this(URI.create(aID), aLabel, aMetadataList, new Summary(aSummary), aThumbnail, aProvider);
    }

    /**
     * Creates a new manifest from the supplied ID, label, metadata, summary, thumbnail, and provider.
     *
     * @param aID A manifest ID
     * @param aLabel A descriptive label
     * @param aMetadataList A list of metadata properties
     * @param aSummary A summary
     * @param aThumbnail A thumbnail
     * @param aProvider A resource provider
     */
    public Manifest(final URI aID, final Label aLabel, final List aMetadataList, final Summary aSummary,
            final ContentResource aThumbnail, final Provider aProvider) {
        super(ResourceTypes.MANIFEST, aID, aLabel, aMetadataList, aSummary, aThumbnail, aProvider);
    }

    /**
     * Creates a new manifest from the supplied ID, label, metadata, summary, thumbnail, and provider.
     *
     * @param aID A manifest ID in string form
     * @param aLabel A descriptive label
     * @param aMetadataList A list of metadata properties
     * @param aSummary A summary
     * @param aThumbnail A thumbnail
     * @param aProvider A resource provider
     */
    public Manifest(final String aID, final Label aLabel, final List aMetadataList, final Summary aSummary,
            final ContentResource aThumbnail, final Provider aProvider) {
        super(ResourceTypes.MANIFEST, URI.create(aID), aLabel, aMetadataList, aSummary, aThumbnail, aProvider);
    }

    /**
     * A private constructor used for Jackson's deserialization processes.
     */
    private Manifest() {
        super(ResourceTypes.MANIFEST);
    }

    @Override
    @JsonSetter(JsonKeys.PROVIDER)
    public Manifest setProviders(final Provider... aProviderArray) {
        return setProviders(Arrays.asList(aProviderArray));
    }

    @Override
    @JsonIgnore
    public Manifest setProviders(final List aProviderList) {
        return (Manifest) super.setProviders(aProviderList);
    }

    /**
     * Gets the manifest's placeholder canvas.
     *
     * @return A placeholder canvas
     */
    @JsonGetter(JsonKeys.PLACEHOLDER_CANVAS)
    public Optional getPlaceholderCanvas() {
        return Optional.ofNullable(myPlaceholderCanvas);
    }

    /**
     * Sets the manifest's placeholder canvas.
     *
     * @param aCanvas A placeholder canvas
     * @return This manifest
     */
    @JsonSetter(JsonKeys.PLACEHOLDER_CANVAS)
    public Manifest setPlaceholderCanvas(final PlaceholderCanvas aCanvas) {
        myPlaceholderCanvas = aCanvas;
        return this;
    }

    /**
     * Gets the manifest's accompanying canvas.
     *
     * @return The accompanying canvas
     */
    @JsonGetter(JsonKeys.ACCOMPANYING_CANVAS)
    public Optional getAccompanyingCanvas() {
        return Optional.ofNullable(myAccompanyingCanvas);
    }

    /**
     * Sets the manifest's accompanying canvas.
     *
     * @param aCanvas An accompanying canvas
     * @return This manifest
     */
    @JsonSetter(JsonKeys.ACCOMPANYING_CANVAS)
    public Manifest setAccompanyingCanvas(final AccompanyingCanvas aCanvas) {
        myAccompanyingCanvas = aCanvas;
        return this;
    }

    @Override
    public Manifest clearBehaviors() {
        return (Manifest) super.clearBehaviors();
    }

    @Override
    @JsonSetter(JsonKeys.BEHAVIOR)
    public Manifest setBehaviors(final Behavior... aBehaviorArray) {
        return (Manifest) super.setBehaviors(checkBehaviors(ManifestBehavior.class, true, aBehaviorArray));
    }

    @Override
    @JsonSetter(JsonKeys.BEHAVIOR)
    public Manifest setBehaviors(final List aBehaviorList) {
        return (Manifest) super.setBehaviors(checkBehaviors(ManifestBehavior.class, true, aBehaviorList));
    }

    @Override
    public Manifest addBehaviors(final Behavior... aBehaviorArray) {
        return (Manifest) super.addBehaviors(checkBehaviors(ManifestBehavior.class, false, aBehaviorArray));
    }

    @Override
    public Manifest addBehaviors(final List aBehaviorList) {
        return (Manifest) super.addBehaviors(checkBehaviors(ManifestBehavior.class, false, aBehaviorList));
    }

    /**
     * Gets an unmodifiable list of manifest contexts. To remove contexts, use {@link Manifest#removeContext(URI)
     * removeContext} or {@link Manifest#clearContexts() clearContexts}.
     *
     * @return The manifest context
     */
    @JsonIgnore
    public List getContexts() {
        if (myContexts.isEmpty()) {
            return null;
        } else {
            return Collections.unmodifiableList(myContexts);
        }
    }

    /**
     * Clears all contexts, but the required one.
     *
     * @return The manifest
     */
    public Manifest clearContexts() {
        myContexts.clear();
        myContexts.add(URIs.CONTEXT_URI);

        return this;
    }

    /**
     * Remove the supplied context. This will not remove the default required context though. If that's supplied, an
     * {@link UnsupportedOperationException} will be thrown.
     *
     * @param aContextURI A context to be removed from the contexts list
     * @return True if the context was removed; else, false
     * @throws UnsupportedOperationException If the required context is supplied to be removed
     */
    public boolean removeContext(final URI aContextURI) {
        if (URIs.CONTEXT_URI.equals(aContextURI)) {
            throw new UnsupportedOperationException(LOGGER.getMessage(MessageCodes.JPA_039, URIs.CONTEXT_URI));
        }

        return myContexts.remove(aContextURI);
    }

    /**
     * Gets the primary manifest context.
     *
     * @return The manifest context
     */
    @JsonIgnore
    public URI getContext() {
        return URIs.CONTEXT_URI;
    }

    /**
     * Adds an array of new context URIs to the manifest.
     *
     * @param aContextArray Manifest context URIs(s)
     * @return The manifest
     */
    public Manifest addContexts(final URI... aContextArray) {
        Objects.requireNonNull(aContextArray, MessageCodes.JPA_007);

        for (final URI uri : aContextArray) {
            Objects.requireNonNull(uri, MessageCodes.JPA_007);

            if (!URIs.CONTEXT_URI.equals(uri)) {
                myContexts.add(uri);
            }
        }

        Collections.sort(myContexts, new ContextListComparator<>());
        return this;
    }

    /**
     * Adds an array of new context URIs, in string form, to the manifest.
     *
     * @param aContextArray Manifest context URI(s) in string form
     * @return The manifest
     */
    public Manifest addContexts(final String... aContextArray) {
        Objects.requireNonNull(aContextArray, MessageCodes.JPA_007);

        for (final String uri : aContextArray) {
            Objects.requireNonNull(uri, MessageCodes.JPA_007);

            if (!URIs.CONTEXT_URI.toString().equals(uri)) {
                myContexts.add(URI.create(uri));
            }
        }

        Collections.sort(myContexts, new ContextListComparator<>());
        return this;
    }

    /**
     * Sets the viewing direction. The remove the existing viewing direction, set it to null.
     *
     * @param aViewingDirection A viewing direction
     * @return The manifest
     */
    @JsonSetter(JsonKeys.VIEWING_DIRECTION)
    public Manifest setViewingDirection(final ViewingDirection aViewingDirection) {
        myViewingDirection = aViewingDirection;
        return this;
    }

    /**
     * Gets the viewing direction.
     *
     * @return The viewing direction
     */
    @JsonGetter(JsonKeys.VIEWING_DIRECTION)
    public ViewingDirection getViewingDirection() {
        return myViewingDirection;
    }

    /**
     * Adds one or more canvases to the manifest.
     *
     * @param aCanvasArray An array of canvases to add to the manifest
     * @return The manifest
     */
    public Manifest addCanvases(final Canvas... aCanvasArray) {
        Collections.addAll(getCanvases(), aCanvasArray);
        return this;
    }

    /**
     * Adds one or more ranges to the manifest.
     *
     * @param aRangeArray An array of ranges to add to the manifest
     * @return The manifest
     */
    public Manifest addRanges(final Range... aRangeArray) {
        Collections.addAll(getRanges(), aRangeArray);
        return this;
    }

    /**
     * Gets the manifest's canvases.
     *
     * @return The manifest's canvases
     */
    @JsonGetter(JsonKeys.ITEMS)
    public List getCanvases() {
        if (myCanvases == null) {
            myCanvases = new ArrayList<>();
        }

        return myCanvases;
    }

    /**
     * Sets the manifest canvases to the supplied one(s).
     *
     * @param aCanvasArray An array of canvases to set
     * @return The manifest
     */
    @JsonGetter(JsonKeys.ITEMS)
    public Manifest setCanvases(final Canvas... aCanvasArray) {
        getCanvases().clear();
        return addCanvases(aCanvasArray);
    }

    /**
     * Sets the manifest's canvases from the contents of a list.
     *
     * @param aCanvasList A list of canvases to be set in the manifest
     * @return The manifest
     */
    @JsonIgnore
    public Manifest setCanvases(final List aCanvasList) {
        final List canvases = getCanvases();

        canvases.clear();
        canvases.addAll(aCanvasList);

        return this;
    }

    /**
     * Sets the optional start.
     *
     * @param aStart A start
     * @return The manifest
     */
    @JsonSetter(JsonKeys.START)
    public Manifest setStart(final Start aStart) {
        myStart = aStart;
        return this;
    }

    /**
     * Gets the optional start canvas.
     *
     * @return The optional start canvas
     */
    @JsonGetter(JsonKeys.START)
    public Optional getStart() {
        return Optional.ofNullable(myStart);
    }

    /**
     * Gets the manifest's range(s).
     *
     * @return The manifest's ranges
     */
    @JsonGetter(JsonKeys.STRUCTURES)
    public List getRanges() {
        if (myRanges == null) {
            myRanges = new ArrayList<>();
        }

        return myRanges;
    }

    /**
     * Sets the manifest's range(s).
     *
     * @param aRangeArray An array of ranges to set in the manifest
     * @return The manifest
     */
    @JsonSetter(JsonKeys.STRUCTURES)
    public Manifest setRanges(final Range... aRangeArray) {
        getRanges().clear();
        return addRanges(aRangeArray);
    }

    /**
     * Sets the manifest's ranges from the contents of a list.
     *
     * @param aRangeList A list of ranges to be set in the manifest
     * @return The manifest
     */
    @JsonIgnore
    public Manifest setRanges(final List aRangeList) {
        final List ranges = getRanges();

        ranges.clear();
        ranges.addAll(aRangeList);

        return this;
    }

    @Override
    public Manifest setSeeAlsoRefs(final SeeAlso... aSeeAlsoArray) {
        return (Manifest) super.setSeeAlsoRefs(aSeeAlsoArray);
    }

    @Override
    public Manifest setSeeAlsoRefs(final List aSeeAlsoList) {
        return (Manifest) super.setSeeAlsoRefs(aSeeAlsoList);
    }

    @Override
    public Manifest setServices(final Service... aServiceArray) {
        return (Manifest) super.setServices(aServiceArray);
    }

    @Override
    public Manifest setServices(final List> aServiceList) {
        return (Manifest) super.setServices(aServiceList);
    }

    /**
     * Sets the services referenced by different parts of the manifest.
     *
     * @param aServicesArray An array of services
     * @return The manifest
     */
    @JsonIgnore
    public Manifest setServiceDefinitions(final Service... aServicesArray) {
        return setServiceDefinitions(Arrays.asList(aServicesArray));
    }

    /**
     * Sets the services referenced by different parts of the manifest.
     *
     * @param aServicesList A list of services
     * @return The manifest
     */
    @JsonSetter(JsonKeys.SERVICES)
    public Manifest setServiceDefinitions(final List> aServicesList) {
        final List> servicesList = getServiceDefinitions();

        Objects.requireNonNull(aServicesList);
        servicesList.clear();
        servicesList.addAll(aServicesList);

        return this;
    }

    /**
     * Gets the services referenced by different parts of the manifest.
     *
     * @return A list of services referenced by different parts of the manifest
     */
    @JsonGetter(JsonKeys.SERVICES)
    public List> getServiceDefinitions() {
        if (myServiceDefinitions == null) {
            myServiceDefinitions = new ArrayList<>();
        }

        return myServiceDefinitions;
    }

    @Override
    public Manifest setPartOfs(final PartOf... aPartOfArray) {
        return (Manifest) super.setPartOfs(aPartOfArray);
    }

    @Override
    public Manifest setPartOfs(final List aPartOfList) {
        return (Manifest) super.setPartOfs(aPartOfList);
    }

    @Override
    public Manifest setRenderings(final Rendering... aRenderingArray) {
        return (Manifest) super.setRenderings(aRenderingArray);
    }

    @Override
    public Manifest setRenderings(final List aRenderingList) {
        return (Manifest) super.setRenderings(aRenderingList);
    }

    @Override
    public Manifest setHomepages(final Homepage... aHomepageArray) {
        return (Manifest) super.setHomepages(aHomepageArray);
    }

    @Override
    public Manifest setHomepages(final List aHomepageList) {
        return (Manifest) super.setHomepages(aHomepageList);
    }

    @Override
    public Manifest setThumbnails(final ContentResource... aThumbnailArray) {
        return (Manifest) super.setThumbnails(aThumbnailArray);
    }

    @Override
    public Manifest setThumbnails(final List> aThumbnailList) {
        return (Manifest) super.setThumbnails(aThumbnailList);
    }

    @Override
    public Manifest setID(final String aID) {
        return (Manifest) super.setID(aID);
    }

    @Override
    public Manifest setID(final URI aID) {
        return (Manifest) super.setID(aID);
    }

    @Override
    public Manifest setRights(final String aRights) {
        return (Manifest) super.setRights(aRights);
    }

    @Override
    public Manifest setRights(final URI aRights) {
        return (Manifest) super.setRights(aRights);
    }

    @Override
    public Manifest setRequiredStatement(final RequiredStatement aRequiredStatement) {
        return (Manifest) super.setRequiredStatement(aRequiredStatement);
    }

    @Override
    public Manifest setSummary(final String aSummary) {
        return (Manifest) super.setSummary(aSummary);
    }

    @Override
    public Manifest setSummary(final Summary aSummary) {
        return (Manifest) super.setSummary(aSummary);
    }

    @Override
    public Manifest setMetadata(final Metadata... aMetadataArray) {
        return (Manifest) super.setMetadata(aMetadataArray);
    }

    @Override
    public Manifest setMetadata(final List aMetadataList) {
        return (Manifest) super.setMetadata(aMetadataList);
    }

    @Override
    public Manifest setLabel(final String aLabel) {
        return (Manifest) super.setLabel(aLabel);
    }

    @Override
    public Manifest setLabel(final Label aLabel) {
        return (Manifest) super.setLabel(aLabel);
    }

    /**
     * Sets the manifest's annotation pages.
     *
     * @param aPageList A list of annotation pages
     * @return This manifest
     */
    @JsonSetter(JsonKeys.ANNOTATIONS)
    public Manifest setAnnotations(final List>> aPageList) {
        final List>> annotations = getAnnotations();

        Objects.requireNonNull(aPageList);
        annotations.clear();
        annotations.addAll(aPageList);

        return this;
    }

    /**
     * Sets the manifest's annotation pages.
     *
     * @param aPageList A list of annotation pages
     * @return This manifest
     */
    @SafeVarargs
    @JsonIgnore
    public final Manifest setAnnotations(final AnnotationPage>... aPageList) {
        setAnnotations(List.of(aPageList));
        return this;
    }

    /**
     * Gets the manifest's annotation pages.
     *
     * @return This manifest's annotation pages
     */
    @JsonGetter(JsonKeys.ANNOTATIONS)
    public List>> getAnnotations() {
        if (myAnnotations == null) {
            myAnnotations = new ArrayList<>();
        }

        return myAnnotations;
    }

    /**
     * Returns a string/JSON representation of the manifest.
     *
     * @return A string representation of the manifest
     */
    @Override
    public String toString() {
        try {
            return JSON.getWriter(Manifest.class).writeValueAsString(this);
        } catch (final JsonProcessingException details) {
            throw new JsonParsingException(details);
        }
    }

    /**
     * Returns a manifest from its JSON representation.
     *
     * @param aJsonString A manifest in JSON form
     * @return The manifest
     * @throws JsonParsingException If there is trouble parsing the JSON manifest
     */
    @JsonIgnore
    public static Manifest from(final String aJsonString) {
        try {
            final Manifest manifest = JSON.getReader(Manifest.class).readValue(aJsonString);
            final String type = manifest.getType();

            // No error is thrown if a Collection is passed in instead of a Manifest, so we check for that
            if (!ResourceTypes.MANIFEST.equals(type)) {
                throw new JsonParsingException(LOGGER.getMessage(MessageCodes.JPA_119, ResourceTypes.MANIFEST, type));
            }

            return manifest;
        } catch (final JsonProcessingException details) {
            // JsonProcessingException wraps other runtime exceptions, too (e.g., IllegalArgumentException(s))
            throw new JsonParsingException(details);
        }
    }

    /**
     * Method used internally to set context from JSON.
     *
     * @param aObject A Jackson deserialization object
     */
    @JsonSetter(JsonKeys.CONTEXT)
    private void deserializeContexts(final Object aObject) {
        if (aObject instanceof String) {
            deserializeContexts(List.of((String) aObject));
        } else if (aObject instanceof List) {
            final List genericList = (List) aObject;

            if (!genericList.isEmpty() && genericList.get(0).getClass().equals(String.class)) {
                setContexts(genericList);
            } else {
                throw new IllegalArgumentException(LOGGER.getMessage(MessageCodes.JPA_113));
            }
        } else {
            throw new IllegalArgumentException(LOGGER.getMessage(MessageCodes.JPA_113));
        }
    }

    /**
     * Sets the manifest's contexts from a list that Jackson builds.
     *
     * @param aContextList A list of contexts
     */
    @JsonIgnore
    private void setContexts(final List aContextList) {
        final List indices = new ArrayList<>();
        final List contextList = new ArrayList<>();

        for (int index = 0; index < aContextList.size(); index++) {
            final URI context = URI.create((String) aContextList.get(index));

            if (URIs.CONTEXT_URI.equals(context)) {
                indices.add(index); // We may have more than one required context in supplied list

                if (indices.size() == DEFAULT_CONTEXT_COUNT) { // Only keep one if this is the case
                    contextList.add(context);
                }
            } else {
                contextList.add(context);
            }
        }

        // Remove required context; we'll add it back at the end
        if (!indices.isEmpty()) {
            contextList.remove((int) indices.get(0));
        }

        myContexts.clear();
        myContexts.addAll(contextList);
        myContexts.add(URIs.CONTEXT_URI); // Add required context at end
    }

    /**
     * Gets the manifest context. The manifest can either have a single context or an array of contexts (Cf.
     * https://iiif.io/api/presentation/3.0/#46-linked-data-context-and-extensions)
     *
     * @return The manifest context
     */
    @JsonGetter(JsonKeys.CONTEXT)
    private Object getJsonContext() {
        if (myContexts.size() == DEFAULT_CONTEXT_COUNT) {
            return myContexts.get(0);
        } else if (!myContexts.isEmpty()) {
            return myContexts;
        } else {
            return null;
        }
    }

    /**
     * A context list comparator that makes sure the required context is always last in the list.
     * 

* Cf. https://iiif.io/api/presentation/3.0/#46-linked-data-context-and-extensions *

*/ static class ContextListComparator implements Comparator { @Override public int compare(final U aFirstURI, final U aSecondURI) { if (URIs.CONTEXT_URI.equals(aFirstURI) && URIs.CONTEXT_URI.equals(aSecondURI)) { return 0; } else if (URIs.CONTEXT_URI.equals(aFirstURI)) { return 1; } else if (URIs.CONTEXT_URI.equals(aSecondURI)) { return -1; } else { return 0; // We leave all non-required contexts where they are } } } }