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

info.freelibrary.iiif.presentation.v3.AbstractCanvas 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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.databind.type.TypeFactory;

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

import info.freelibrary.iiif.presentation.v3.ids.Minter;
import info.freelibrary.iiif.presentation.v3.properties.Behavior;
import info.freelibrary.iiif.presentation.v3.properties.Label;
import info.freelibrary.iiif.presentation.v3.properties.behaviors.CanvasBehavior;
import info.freelibrary.iiif.presentation.v3.properties.selectors.MediaFragmentSelector;
import info.freelibrary.iiif.presentation.v3.utils.JSON;
import info.freelibrary.iiif.presentation.v3.utils.JsonKeys;
import info.freelibrary.iiif.presentation.v3.utils.MessageCodes;

/**
 * A virtual container that represents a page or view and has content resources associated with it or with parts of it.
 * The canvas provides a frame of reference for the layout of the content. The concept of a canvas is borrowed from
 * standards like PDF and HTML, or applications like Photoshop and Powerpoint, where the display starts from a blank
 * canvas and images, text and other resources are "painted" on to it.
 */
@SuppressWarnings({ PMD.GOD_CLASS, PMD.EXCESSIVE_IMPORTS })
@JsonPropertyOrder({ JsonKeys.ID, JsonKeys.TYPE, JsonKeys.LABEL, JsonKeys.HEIGHT, JsonKeys.WIDTH, JsonKeys.DURATION,
    JsonKeys.THUMBNAIL, JsonKeys.PLACEHOLDER_CANVAS, JsonKeys.ACCOMPANYING_CANVAS, JsonKeys.METADATA, JsonKeys.ITEMS,
    JsonKeys.ANNOTATIONS })
abstract class AbstractCanvas> extends NavigableResource> { // NOPMD

    /**
     * The abstract canvas' logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractCanvas.class, MessageCodes.BUNDLE);

    /**
     * A Jackson serialization filter name for width and height.
     */
    private static final String WIDTH_HEIGHT_FILTER = "JPv3WidthHeightFilter";

    /**
     * A spatial constant.
     */
    private static final String SPATIAL = "spatial";

    /**
     * A temporal constant.
     */
    private static final String TEMPORAL = "temporal";

    /**
     * A zero (non-existent) duration.
     */
    private static final float ZERO_DURATION = 0.0f;

    /**
     * The painting annotations on the canvas.
     */
    private List> myPaintingPageList;

    /**
     * The supplementing annotations on the canvas.
     */
    private List> mySupplementingPageList;

    /**
     * The canvas' other canvases (other than painting or supplementing).
     */
    private List>> myOtherAnnotations;

    /**
     * The canvas' duration.
     */
    private float myDuration;

    /**
     * The canvas' width.
     */
    private int myWidth;

    /**
     * The canvas' height.
     */
    private int myHeight;

    /**
     * Creates a new canvas.
     *
     * @param aID A canvas ID in string form
     * @param aLabel A canvas label in string form
     */
    protected AbstractCanvas(final String aID, final String aLabel) {
        super(ResourceTypes.CANVAS, aID, aLabel);
    }

    /**
     * Creates a new canvas.
     *
     * @param aID A canvas ID
     * @param aLabel A canvas label
     */
    protected AbstractCanvas(final URI aID, final Label aLabel) {
        super(ResourceTypes.CANVAS, aID, aLabel);
    }

    /**
     * Creates a new canvas, using the supplied minter to create the ID.
     *
     * @param aMinter A minter to use to create the canvas ID
     * @param aLabel A canvas label
     */
    protected AbstractCanvas(final Minter aMinter, final Label aLabel) {
        super(ResourceTypes.CANVAS, aMinter.getCanvasID(), aLabel);
    }

    /**
     * Creates a new canvas.
     *
     * @param aID A canvas ID
     */
    protected AbstractCanvas(final URI aID) {
        super(ResourceTypes.CANVAS, aID);
    }

    /**
     * Creates a new canvas.
     *
     * @param aID A canvas ID in string form
     */
    protected AbstractCanvas(final String aID) {
        super(ResourceTypes.CANVAS, URI.create(aID));
    }

    /**
     * Creates a new canvas, using a minter to create its ID.
     *
     * @param aMinter An ID minter
     */
    protected AbstractCanvas(final Minter aMinter) {
        super(ResourceTypes.CANVAS, aMinter.getCanvasID());
    }

    /**
     * Creates a new canvas.
     */
    protected AbstractCanvas() {
        super(ResourceTypes.CANVAS);
    }

    /**
     * Sets the canvas' behaviors.
     *
     * @param aBehaviorArray An array of behaviors
     * @return The canvas
     */
    @Override
    @JsonSetter(JsonKeys.BEHAVIOR)
    protected AbstractCanvas setBehaviors(final Behavior... aBehaviorArray) {
        return (AbstractCanvas) super.setBehaviors(checkBehaviors(CanvasBehavior.class, true, aBehaviorArray));
    }

    /**
     * Sets the canvas' behaviors.
     *
     * @param aBehaviorList A list of behaviors
     * @return The canvas
     */
    @Override
    @JsonSetter(JsonKeys.BEHAVIOR)
    protected AbstractCanvas setBehaviors(final List aBehaviorList) {
        return (AbstractCanvas) super.setBehaviors(checkBehaviors(CanvasBehavior.class, true, aBehaviorList));
    }

    @Override
    protected AbstractCanvas addBehaviors(final Behavior... aBehaviorArray) {
        return (AbstractCanvas) super.addBehaviors(checkBehaviors(CanvasBehavior.class, false, aBehaviorArray));
    }

    /**
     * Adds behaviors to the canvas.
     *
     * @param aBehaviorList A list of behaviors
     * @return The canvas
     */
    @Override
    protected AbstractCanvas addBehaviors(final List aBehaviorList) {
        return (AbstractCanvas) super.addBehaviors(checkBehaviors(CanvasBehavior.class, false, aBehaviorList));
    }

    /**
     * Sets the canvas' painting pages.
     *
     * @param aPageArray An array of painting pages
     * @return The canvas
     */
    @JsonSetter(JsonKeys.ITEMS)
    @SuppressWarnings(JDK.UNCHECKED) // Moved SafeVarargs to extending classes where method can be final
    protected AbstractCanvas setPaintingPages(final AnnotationPage... aPageArray) {
        if (myPaintingPageList != null) {
            myPaintingPageList.clear();
        }

        getPaintingPages().addAll(Arrays.asList(aPageArray));
        return this;
    }

    /**
     * Sets the canvas' painting pages.
     *
     * @param aPageList A list of painting pages
     * @return The canvas
     */
    @JsonIgnore
    protected AbstractCanvas setPaintingPages(final List> aPageList) {
        if (myPaintingPageList != null) {
            myPaintingPageList.clear();
        }

        getPaintingPages().addAll(aPageList);
        return this;
    }

    /**
     * Gets the canvas' annotation pages for painting annotations.
     *
     * @return The canvas' annotation pages for painting annotations
     */
    @JsonGetter(JsonKeys.ITEMS)
    public List> getPaintingPages() {
        if (myPaintingPageList == null) {
            myPaintingPageList = new ArrayList<>();
        }

        return myPaintingPageList;
    }

    /**
     * Gets the canvas' annotation pages that aren't related to painting.
     *
     * @return The canvas' non-painting annotation pages
     */
    @JsonIgnore
    public List>> getOtherAnnotations() {
        if (myOtherAnnotations == null) {
            myOtherAnnotations = new ArrayList<>();
        }

        return myOtherAnnotations;
    }

    /**
     * Sets the canvas annotation pages from a list.
     *
     * @param aAnnotationList A list of annotation pages
     * @return The canvas
     */
    @JsonIgnore
    public AbstractCanvas setOtherAnnotations(final List>> aAnnotationList) {
        final List>> annotations = getOtherAnnotations();

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

        return this;
    }

    /**
     * Sets the canvas' annotation pages from an array.
     *
     * @param aAnnotationArray An array of annotation pages
     * @return The canvas
     */
    @SuppressWarnings(JDK.UNCHECKED)
    @JsonIgnore
    public AbstractCanvas setOtherAnnotations(final AnnotationPage>... aAnnotationArray) {
        return setOtherAnnotations(Arrays.asList(aAnnotationArray));
    }

    /**
     * Sets the canvas' supplementing pages.
     *
     * @param aPageArray An array of supplementing pages
     * @return The canvas
     */
    @JsonIgnore
    @SuppressWarnings(JDK.UNCHECKED) // Moved SafeVarargs to extending classes where method can be final
    protected AbstractCanvas setSupplementingPages(final AnnotationPage... aPageArray) {
        if (mySupplementingPageList != null) {
            mySupplementingPageList.clear();
        }

        getSupplementingPages().addAll(Arrays.asList(aPageArray));
        return this;
    }

    /**
     * Sets the canvas' supplementing pages.
     *
     * @param aPageList A list of supplementing pages
     * @return The canvas
     */
    @JsonIgnore
    protected AbstractCanvas setSupplementingPages(final List> aPageList) {
        if (mySupplementingPageList != null) {
            mySupplementingPageList.clear();
        }

        getSupplementingPages().addAll(aPageList);
        return this;
    }

    /**
     * Gets the canvas' annotation pages for non-painting annotations.
     *
     * @return The canvas' non-painting annotation pages
     */
    @JsonIgnore
    public List> getSupplementingPages() {
        if (mySupplementingPageList == null) {
            mySupplementingPageList = new ArrayList<>();
        }

        return mySupplementingPageList;
    }

    /**
     * Gets the width of the canvas.
     *
     * @return The width of the canvas
     */
    @JsonGetter(JsonKeys.WIDTH)
    @JsonInclude(Include.NON_DEFAULT)
    public int getWidth() {
        return myWidth;
    }

    /**
     * Gets the height of the canvas.
     *
     * @return The height of the canvas
     */
    @JsonGetter(JsonKeys.HEIGHT)
    @JsonInclude(Include.NON_DEFAULT)
    public int getHeight() {
        return myHeight;
    }

    /**
     * Sets the width and height of the canvas.
     *
     * @param aWidth A canvas width
     * @param aHeight A canvas height
     * @return The canvas
     */
    @JsonIgnore
    protected AbstractCanvas setWidthHeight(final int aWidth, final int aHeight) {
        if (aWidth > 0 && aHeight > 0) {
            myWidth = aWidth;
            myHeight = aHeight;

            return this;
        } else {
            throw new IllegalArgumentException(LOGGER.getMessage(MessageCodes.JPA_011, aWidth, aHeight));
        }
    }

    /**
     * Gets the duration of the canvas.
     *
     * @return The duration of the canvas
     */
    @JsonGetter(JsonKeys.DURATION)
    @JsonInclude(Include.NON_DEFAULT)
    public float getDuration() {
        return myDuration;
    }

    /**
     * Sets the canvas duration.
     *
     * @param aDuration A canvas duration
     * @return The canvas
     */
    @JsonSetter(JsonKeys.DURATION)
    protected AbstractCanvas setDuration(final Number aDuration) {
        myDuration = convertToFinitePositiveFloat(aDuration);
        return this;
    }

    /**
     * Paints a canvas with content resources.
     *
     * @param  A type of canvas
     * @param aCanvas A canvas
     * @param aMinter An ID minter
     * @param aChoice Whether the content resource are painted on the canvas as a choice
     * @param aContentArray An array of content resources
     * @return The canvas
     * @throws ContentOutOfBoundsException If the painted content is out of bounds of the canvas
     */
    protected final > AbstractCanvas paint(final CanvasResource aCanvas,
            final Minter aMinter, final boolean aChoice, final ContentResource... aContentArray) {
        final PaintingAnnotation anno = new PaintingAnnotation(aMinter.getAnnotationID(), aCanvas);
        final AnnotationPage page;
        final int pageCount;

        anno.setChoice(aChoice);

        for (final ContentResource content : aContentArray) {
            if (canFrame(content)) {
                anno.getBodies().add((AnnotationBody) content);
            }
        }

        pageCount = getPaintingPages().size();

        if (pageCount == 0) {
            page = new AnnotationPage<>(aMinter.getAnnotationPageID(aCanvas));
            getPaintingPages().add(page);
        } else {
            page = getPaintingPages().get(pageCount - 1);
        }

        page.getAnnotations().add(anno);

        return this;
    }

    /**
     * Paints a canvas with content resources.
     *
     * @param  A type of canvas
     * @param aCanvas A canvas
     * @param aMinter An ID minter
     * @param aCanvasRegion A canvas region
     * @param aChoice Whether the content resource are painted on the canvas as a choice
     * @param aContentArray An array of content resources
     * @return The canvas
     * @throws ContentOutOfBoundsException If the painted content is out of bounds of the canvas
     * @throws SelectorOutOfBoundsException If the supplied selector is out of bounds of the canvas
     */
    protected final > AbstractCanvas paint(final CanvasResource aCanvas,
            final Minter aMinter, final MediaFragmentSelector aCanvasRegion, final boolean aChoice,
            final ContentResource... aContentArray) {
        final PaintingAnnotation anno = new PaintingAnnotation(aMinter, aCanvas, aCanvasRegion);
        final AnnotationPage page;
        final int pageCount;

        anno.setChoice(aChoice);

        for (final ContentResource content : aContentArray) {
            if (canFrame(content, aCanvasRegion)) {
                anno.getBodies().add((AnnotationBody) content);
            }
        }

        pageCount = getPaintingPages().size();

        if (pageCount == 0) {
            page = new AnnotationPage<>(aMinter.getAnnotationPageID(aCanvas));
            getPaintingPages().add(page);
        } else {
            page = getPaintingPages().get(pageCount - 1);
        }

        page.getAnnotations().add(anno);

        return this;
    }

    /**
     * Supplements a canvas with content resources.
     *
     * @param  A type of canvas resource
     * @param aCanvas A canvas
     * @param aMinter An ID minter
     * @param aChoice Whether content resources on the canvas use a choice
     * @param aContentArray An array of content resources
     * @return This canvas
     */
    protected final > AbstractCanvas supplement(final CanvasResource aCanvas,
            final Minter aMinter, final boolean aChoice, final ContentResource... aContentArray) {
        final SupplementingAnnotation anno = new SupplementingAnnotation(aMinter.getAnnotationID(), aCanvas);
        final List> bodies = new ArrayList<>();
        final int pageCount = getSupplementingPages().size();
        final AnnotationPage page;

        for (final ContentResource resource : aContentArray) {
            bodies.add((AnnotationBody) resource);
        }

        anno.setChoice(aChoice).getBodies().addAll(bodies);

        if (pageCount == 0) {
            page = new AnnotationPage<>(aMinter.getAnnotationPageID(aCanvas));
            getSupplementingPages().add(page);
        } else {
            page = getSupplementingPages().get(pageCount - 1);
        }

        page.getAnnotations().add(anno);

        return this;
    }

    /**
     * Supplements a canvas with content resources.
     *
     * @param  A type of canvas resource
     * @param aCanvas A canvas
     * @param aMinter An ID minter
     * @param aCanvasRegion A canvas region
     * @param aChoice Whether content resources on the canvas use a choice
     * @param aContentArray An array of content resources
     * @return This canvas
     * @throws SelectorOutOfBoundsException If the canvas region is out of bounds
     */
    protected final > AbstractCanvas supplement(final CanvasResource aCanvas,
            final Minter aMinter, final MediaFragmentSelector aCanvasRegion, final boolean aChoice,
            final ContentResource... aContentArray) {
        final SupplementingAnnotation anno =
                new SupplementingAnnotation(aMinter.getAnnotationID(), aCanvas, aCanvasRegion);
        final List> bodies = new ArrayList<>();
        final int pageCount = getSupplementingPages().size();
        final AnnotationPage page;

        for (final ContentResource resource : aContentArray) {
            bodies.add((AnnotationBody) resource);
        }

        getCanvasFragment(aCanvasRegion); // Check that the canvas region is valid by absence of exceptions
        anno.setChoice(aChoice).getBodies().addAll(bodies);

        if (pageCount == 0) {
            page = new AnnotationPage<>(aMinter.getAnnotationPageID(aCanvas));
            getSupplementingPages().add(page);
        } else {
            page = getSupplementingPages().get(pageCount - 1);
        }

        page.getAnnotations().add(anno);

        return this;
    }

    /**
     * Converts the canvas to its string/JSON representation.
     *
     * @return A string representation of the canvas
     */
    @Override
    public String toString() {
        final SimpleFilterProvider filterProvider = new SimpleFilterProvider();
        final Set filtered = new HashSet<>();

        // Don't write duration if it's zero
        if (myDuration == 0F) { // NOPMD
            filtered.add(JsonKeys.DURATION);
        }

        // Don't write width and height if they're both zero
        if (myHeight == 0 && myWidth == 0) {
            filtered.add(JsonKeys.HEIGHT);
            filtered.add(JsonKeys.WIDTH);
        }

        // These are the things we filter when we serialize to JSON
        filterProvider.addFilter(WIDTH_HEIGHT_FILTER, SimpleBeanPropertyFilter.serializeAllExcept(filtered));

        try {
            return JSON.getWriter(filterProvider).writeValueAsString(this);
        } catch (final JsonProcessingException details) {
            throw new JsonParsingException(details);
        }
    }

    /**
     * Sets the width of the canvas.
     *
     * @param aWidth The desired width of the canvas
     * @return The canvas
     */
    @JsonSetter(JsonKeys.WIDTH)
    private AbstractCanvas setWidth(final int aWidth) {
        myWidth = aWidth;
        return this;
    }

    /**
     * Sets the height of the canvas.
     *
     * @param aHeight The desired height of the canvas
     * @return The canvas
     */
    @JsonSetter(JsonKeys.HEIGHT)
    private AbstractCanvas setHeight(final int aHeight) {
        myHeight = aHeight;
        return this;
    }

    /**
     * Checks if a content resource can "fit" on this canvas.
     *
     * @param aContent A content resource
     * @return true if the content resource fits within the bounds of the canvas
     * @throws ContentOutOfBoundsException If the content resource won't fit
     */
    @SuppressWarnings({ "PMD.CyclomaticComplexity" })
    boolean canFrame(final ContentResource aContent) {
        if (aContent instanceof SpatialContentResource) {
            // The canvas must have a width and height, which must not be smaller than that of the content
            if (myWidth == 0 || myHeight == 0) {
                throw new ContentOutOfBoundsException(MessageCodes.JPA_059, aContent.getID(), SPATIAL, getID());
            } else {
                final SpatialContentResource spatialPainting = (SpatialContentResource) aContent;

                if (getWidth() < spatialPainting.getWidth() || getHeight() < spatialPainting.getHeight()) {
                    throw new ContentOutOfBoundsException(MessageCodes.JPA_060, aContent.getID(), SPATIAL, getID());
                }
            }
        }

        if (aContent instanceof TemporalContentResource) {
            // The canvas must have a duration, which must not be shorter than that of the content
            if (myDuration == ZERO_DURATION) {
                throw new ContentOutOfBoundsException(MessageCodes.JPA_059, aContent.getID(), TEMPORAL, getID());
            } else {
                final TemporalContentResource temporalPainting = (TemporalContentResource) aContent;

                if (getDuration() < temporalPainting.getDuration()) {
                    throw new ContentOutOfBoundsException(MessageCodes.JPA_060, aContent.getID(), TEMPORAL, getID());
                }
            }
        }

        return true;
    }

    /**
     * Checks if a content resource can "fit" on a fragment of this canvas.
     *
     * @param aContent A content resource
     * @param aCanvasRegion A {@link MediaFragmentSelector} identifying a fragment of this canvas
     * @return true if the content resource fits within the bounds of the canvas fragment identified by the
     *         given {@link MediaFragmentSelector}
     * @throws ContentOutOfBoundsException If the content resource won't fit
     * @throws SelectorOutOfBoundsException If the canvas fragment doesn't exist
     */
    private boolean canFrame(final ContentResource aContent, final MediaFragmentSelector aCanvasRegion) {
        return getCanvasFragment(aCanvasRegion).canFrame(aContent);
    }

    /**
     * Creates a temporary canvas for determining if a content resource can fit on a canvas fragment.
     *
     * @param aCanvasRegion A {@link MediaFragmentSelector} identifying a fragment of this canvas
     * @return A {@link Canvas} representing the fragment
     * @throws SelectorOutOfBoundsException If the canvas fragment identified by the given {@link MediaFragmentSelector}
     *         doesn't exist
     */
    @SuppressWarnings({ PMD.N_PATH_COMPLEXITY, PMD.CYCLOMATIC_COMPLEXITY })
    private Canvas getCanvasFragment(final MediaFragmentSelector aCanvasRegion) { // NOPMD
        final URI canvasID = URI.create(getID().toString() + Constants.HASH + aCanvasRegion.toString());
        final Canvas canvasFragment = new Canvas(canvasID);
        final int width;
        final int height;
        final double duration;

        if (aCanvasRegion.isSpatial()) {
            // Check for semantic errors according to 
            if (myWidth == 0 && myHeight == 0) {
                throw new SelectorOutOfBoundsException(MessageCodes.JPA_064, aCanvasRegion.toString(), SPATIAL,
                        getID());
            } else {
                // The bounding box of the selector must be entirely within the bounds of the canvas
                if (getWidth() < aCanvasRegion.getX() + aCanvasRegion.getWidth() ||
                        getHeight() < aCanvasRegion.getY() + aCanvasRegion.getHeight()) {
                    throw new SelectorOutOfBoundsException(MessageCodes.JPA_065, aCanvasRegion.toString(), SPATIAL,
                            getID());
                } else {
                    // Use the selector's spatial dimensions
                    width = aCanvasRegion.getWidth();
                    height = aCanvasRegion.getHeight();
                }
            }
        } else {
            // Use the canvas' spatial dimensions
            width = getWidth();
            height = getHeight();
        }

        if (aCanvasRegion.isTemporal()) {
            // Check for semantic errors
            if (myDuration == ZERO_DURATION) {
                throw new SelectorOutOfBoundsException(MessageCodes.JPA_064, aCanvasRegion.toString(), TEMPORAL,
                        getID());
            } else {
                // Cannot start at or beyond the duration of the canvas
                if (getDuration() <= aCanvasRegion.getStart()) {
                    throw new SelectorOutOfBoundsException(MessageCodes.JPA_065, aCanvasRegion.toString(), TEMPORAL,
                            getID());
                }

                // If an end is specified, cannot end past the duration of the canvas
                if (aCanvasRegion.hasEnd()) {
                    if (getDuration() < aCanvasRegion.getEnd()) {
                        throw new SelectorOutOfBoundsException(MessageCodes.JPA_065, aCanvasRegion.toString(), TEMPORAL,
                                getID());
                    } else {
                        // Use the selector's temporal dimension
                        duration = aCanvasRegion.getDuration();
                    }
                } else {
                    // Use the interval from the start of the selector to the end of the canvas as the duration
                    duration = getDuration() - aCanvasRegion.getStart();
                }
            }
        } else {
            // Use the canvas' temporal dimensions
            duration = getDuration();
        }

        if (width > 0 && height > 0) {
            canvasFragment.setWidthHeight(width, height);
        }

        if (duration > 0) {
            canvasFragment.setDuration(duration);
        }

        return canvasFragment;
    }

    /**
     * A method used by Jackson for serialization.
     *
     * @return A list of annotation pages
     */
    @JsonGetter(JsonKeys.ANNOTATIONS)
    private List>> getAnnotations() {
        final List>> annotations = new ArrayList<>();

        getSupplementingPages().forEach(page -> {
            annotations.add(page);
        });

        getOtherAnnotations().forEach(page -> {
            annotations.add(page);
        });

        return annotations;
    }

    /**
     * A method used by Jackson for deserialization.
     *
     * @param aObject A object used by Jackson to deserialize
     * @return This canvas
     */
    @SuppressWarnings(JDK.UNCHECKED)
    @JsonSetter(JsonKeys.ANNOTATIONS)
    private AbstractCanvas setAnnotations(final Object aObject) {
        final List> supplementingPages = getSupplementingPages();
        final List>> otherAnnotations = getOtherAnnotations();
        final List>> annotationList = getDeserializedPageList(aObject);

        supplementingPages.clear();
        otherAnnotations.clear();

        annotationList.forEach(page -> {
            // Check all the annotations on the page to make sure they are all supplementing annotations
            final List> annotations = page.getAnnotations();

            boolean supplementingAnnotations = true;

            for (final Annotation annotation : annotations) {
                if (!(annotation instanceof SupplementingAnnotation)) {
                    supplementingAnnotations = false;
                }
            }

            // If all the annotations on a page are supplementing, we can put the page into supplementing pages list
            if (supplementingAnnotations) {
                // The unchecked warning suppression on the method is for this
                supplementingPages.add((AnnotationPage) page);
            } else {
                otherAnnotations.add(page);
            }
        });

        return this;
    }

    /**
     * Gets a list of AnnotationPage(s) from a deserialized Jackson object (which should be a list).
     *
     * @param aObject An object Jackson has created while deserializing the incoming JSON
     * @return A list of AnnotationPage(s)
     */
    @JsonIgnore
    private List>> getDeserializedPageList(final Object aObject) {
        final List>> pagesList = new ArrayList<>();

        // Incoming object should be a list of AnnotationPage(s)
        if (aObject instanceof List) {
            final List pageList = (List) aObject;

            // Get each AnnotationPage object from the list and try to deserialize it
            for (final Object pageListObject : pageList) {
                if (pageListObject instanceof Map) {
                    pagesList.add(getDeserializedPage((Map) pageListObject));
                } // Just ignore stuff we don't know about(?)
            }
        } // Just ignore stuff we don't know about(?)

        return pagesList;
    }

    /**
     * Gets a AnnotationPage from a deserialized Jackson map.
     *
     * @param aAnnotationPageMap A map representing an AnnotationPage
     * @return An AnnotationPage
     */
    @JsonIgnore
    private AnnotationPage> getDeserializedPage(final Map aAnnotationPageMap) {
        final List items = (List) aAnnotationPageMap.get(JsonKeys.ITEMS);
        final TypeFactory typeFactory = JSON.getTypeFactory();
        final JavaType javaType;

        // If there are annotations, we can check whether they are supplementing or "other"
        if (items != null) {
            boolean supplementingAnnotations = true;

            for (final Object item : items) {
                final Map annotation = (Map) item;

                if (annotation != null &&
                        !SupplementingAnnotation.MOTIVATION.equals(annotation.get(JsonKeys.MOTIVATION))) {
                    supplementingAnnotations = false;
                }
            }

            // Either supplementing annotation or mixed annotations (which may include supplementing annotations)
            if (supplementingAnnotations) {
                javaType = typeFactory.constructParametricType(AnnotationPage.class, SupplementingAnnotation.class);
                return JSON.convertValue(aAnnotationPageMap, javaType);
            } else {
                javaType = typeFactory.constructParametricType(AnnotationPage.class, Annotation.class);
                return JSON.convertValue(aAnnotationPageMap, javaType);
            }
        } else {
            javaType = typeFactory.constructParametricType(AnnotationPage.class, Annotation.class);
            return JSON.convertValue(aAnnotationPageMap, javaType);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy