# COSIncrement.java

    /*
     * Licensed to the Apache Software Foundation (ASF) under one or more
     * contributor license agreements.  See the NOTICE file distributed with
     * this work for additional information regarding copyright ownership.
     * The ASF licenses this file to You under the Apache License, Version 2.0
     * (the "License"); 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 "AS IS" 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.
     */
    package org.apache.pdfbox.cos;

    import java.util.Arrays;
    import java.util.HashSet;
    import java.util.Iterator;
    import java.util.LinkedHashSet;
    import java.util.Set;

    /**
     * A {@link COSIncrement} starts at a given {@link COSUpdateInfo} to collect updates, that have been made to a
     * {@link COSDocument} and therefore should be added to it´s next increment.
     *
     * @author Christian Appl
     * @see COSUpdateState
     * @see COSUpdateInfo
     */
    public class COSIncrement implements Iterable<COSBase>
    {

        /**
         * Contains the {@link COSBase}s, that shall be added to the increment at top level.
         */
        private final Set<COSBase> objects = new LinkedHashSet<>();
        /**
         * Contains the direct {@link COSBase}s, that are either contained written directly by structures contained in
         * {@link #objects} or that must be excluded from being written as indirect {@link COSObject}s for other reasons.
         */
        private final Set<COSBase> excluded = new HashSet<>();
        /**
         * Contains all {@link COSObject}s, that have already been processed by this {@link COSIncrement} and shall not be
         * processed again.
         */
        private final Set<COSObject> processedObjects = new HashSet<>();
        /**
         * Contains the {@link COSUpdateInfo} that this {@link COSIncrement} creates an increment for.
         */
        private final COSUpdateInfo incrementOrigin;
        /**
         * Whether this {@link COSIncrement} has already been determined, or must still be evaluated.
         */
        private boolean initialized = false;

        /**
         * Creates a new {@link COSIncrement} for the given {@link COSUpdateInfo}, the increment will use it´s
         * {@link COSDocumentState} as it´s own origin and shall collect all updates contained in the given
         * {@link COSUpdateInfo}.<br>
         * Should the given object be {@code null}, the resulting increment shall be empty.
         *
         * @param incrementOrigin The {@link COSUpdateInfo} serving as an update source for this {@link COSIncrement}.
         */
        public COSIncrement(COSUpdateInfo incrementOrigin)
        {
            this.incrementOrigin = incrementOrigin;
        }

        /**
         * Collect all updates made to the given {@link COSBase} and it's contained structures.<br>
         * This shall forward all {@link COSUpdateInfo} objects to the proper specialized collection methods.
         *
         * @param base The {@link COSBase} updates shall be collected for.
         * @return Returns {@code true}, if the {@link COSBase} represents a direct child structure, that would require it´s
         * parent to be updated instead.
         * @see #collect(COSDictionary)
         * @see #collect(COSArray)
         * @see #collect(COSObject)
         */
        private boolean collect(COSBase base)
        {
            if(contains(base))
            {
                return false;
            }
            // handle updatable objects:
            if(base instanceof COSDictionary)
            {
                return collect((COSDictionary) base);
            }
            else if(base instanceof COSObject)
            {
                return collect((COSObject) base);
            }
            else if(base instanceof COSArray)
            {
                return collect((COSArray) base);
            }
            return false;
        }

        /**
         * Collect all updates made to the given {@link COSDictionary} and it's contained structures.
         *
         * @param dictionary The {@link COSDictionary} updates shall be collected for.
         * @return Returns {@code true}, if the {@link COSDictionary} represents a direct child structure, that would
         * require it´s parent to be updated instead.
         */
        private boolean collect(COSDictionary dictionary)
        {
            COSUpdateState updateState = dictionary.getUpdateState();
            // Is definitely part of the increment?
            if(!isExcluded(dictionary) && !contains(dictionary) && updateState.isUpdated())
            {
                add(dictionary);
            }
            boolean childDemandsParentUpdate = false;
            // Collect children:
            for(COSBase entry : dictionary.getValues())
            {
                // Primitives can not be part of an increment. (on top level)
                if(!(entry instanceof COSUpdateInfo) || contains(entry))
                {
                    continue;
                }
                COSUpdateInfo updatableEntry = (COSUpdateInfo) entry;
                COSUpdateState entryUpdateState = updatableEntry.getUpdateState();
                // Entries with different document origin must be part of the increment!
                updateDifferentOrigin(entryUpdateState);
                // Always attempt to write COSArrays as direct objects.
                if(updatableEntry.isNeedToBeUpdated() &&
                    ((!(entry instanceof COSObject) && entry.isDirect()) || entry instanceof COSArray))
                {
                    // Exclude direct entries from the increment!
                    exclude(entry);
                    childDemandsParentUpdate = true;
                }
                // Collect descendants:
                childDemandsParentUpdate = collect(entry) || childDemandsParentUpdate;
            }

            if(isExcluded(dictionary))
            {
                return childDemandsParentUpdate;
            }
            else
            {
                if(childDemandsParentUpdate && !contains(dictionary))
                {
                    add(dictionary);
                }
                return false;
            }
        }

        /**
         * Collect all updates made to the given {@link COSArray} and it's contained structures.
         *
         * @param array The {@link COSDictionary} updates shall be collected for.
         * @return Returns {@code true}, if the {@link COSArray}´s elements changed. A {@link COSArray} shall always be
         * treated as a direct structure, that would require it´s parent to be updated instead.
         */
        private boolean collect(COSArray array)
        {
            COSUpdateState updateState = array.getUpdateState();
            boolean childDemandsParentUpdate = updateState.isUpdated();
            for(COSBase entry : array)
            {
                // Primitives can not be part of an increment. (on top level)
                if(!(entry instanceof COSUpdateInfo) || contains(entry))
                {
                    continue;
                }
                COSUpdateState entryUpdateState = ((COSUpdateInfo) entry).getUpdateState();
                // Entries with different document origin must be part of the increment!
                updateDifferentOrigin(entryUpdateState);
                // Collect descendants:
                childDemandsParentUpdate = collect(entry) || childDemandsParentUpdate;
            }
            return childDemandsParentUpdate;
        }

        /**
         * Collect all updates made to the given {@link COSObject} and it's contained structures.
         *
         * @param object The {@link COSObject} updates shall be collected for.
         * @return Always returns {@code false}. {@link COSObject}s by definition are indirect and shall never cause a
         * parent structure to be updated.
         */
        private boolean collect(COSObject object)
        {
            if(contains(object))
            {
                return false;
            }
            addProcessedObject(object);
            COSUpdateState updateState = object.getUpdateState();
            // Objects with different document origin must be part of the increment!
            updateDifferentOrigin(updateState);
            // determine actual, if necessary or possible without dereferencing:
            COSUpdateInfo actual = null;
            if(updateState.isUpdated() || object.isDereferenced())
            {
                COSBase base = object.getObject();
                if(base instanceof COSUpdateInfo)
                {
                    actual = (COSUpdateInfo) base;
                }
            }
            // Skip?
            if(actual == null || contains(actual.getCOSObject()))
            {
                return false;
            }
            boolean childDemandsParentUpdate = false;
            COSUpdateState actualUpdateState = actual.getUpdateState();
            if(actualUpdateState.isUpdated())
            {
                childDemandsParentUpdate = true;
            }
            exclude(actual.getCOSObject());
            childDemandsParentUpdate = collect(actual.getCOSObject()) || childDemandsParentUpdate;
            if(updateState.isUpdated() || childDemandsParentUpdate)
            {
                add(actual.getCOSObject());
            }
            return false;
        }

        /**
         * Returns {@code true}, if the given {@link COSBase} is already known to and has been processed by this
         * {@link COSIncrement}.
         *
         * @param base The {@link COSBase} to check.
         * @return {@code true}, if the given {@link COSBase} is already known to and has been processed by this
         * {@link COSIncrement}.
         * @see #objects
         * @see #processedObjects
         */
        public boolean contains(COSBase base)
        {
            return objects.contains(base) || (base instanceof COSObject && processedObjects.contains((COSObject) base));
        }

        /**
         * Check whether the given {@link COSUpdateState}´s {@link COSDocumentState} differs from the {@link COSIncrement}´s
         * known {@link #incrementOrigin}.<br>
         * Should that be the case, the {@link COSUpdateState} originates from another {@link COSDocument} and must be added
         * to the {@link COSIncrement}, hence call {@link COSUpdateState#update()}.
         *
         * @param updateState The {@link COSUpdateState} that shall be updated, if it's originating from another
         *                    {@link COSDocument}.
         * @see #incrementOrigin
         */
        private void updateDifferentOrigin(COSUpdateState updateState)
        {
            if(incrementOrigin != null && updateState != null &&
                incrementOrigin.getUpdateState().getOriginDocumentState() != updateState.getOriginDocumentState())
            {
                updateState.update();
            }
        }

        /**
         * The given object and actual {COSBase}s shall be part of the increment and must be added to {@link #objects},
         * if possible.<br>
         * {@code null} values shall be skipped.
         *
         * @param object The {@link COSBase} to add to {@link #objects}.
         * @see #objects
         */
        private void add(COSBase object)
        {
            if(object != null)
            {
                objects.add(object);
            }
        }

        /**
         * The given {@link COSObject} has been processed, or is being processed. It shall be added to
         * {@link #processedObjects} to skip it, should it be encountered again.<br>
         * {@code null} values shall be ignored.
         *
         * @param base The {@link COSObject} to add to {@link #processedObjects}.
         * @see #processedObjects
         */
        private void addProcessedObject(COSObject base)
        {
            if(base != null)
            {
                processedObjects.add(base);
            }
        }

        /**
         * The given {@link COSBase}s are not fit for inclusion in an increment and shall be added to {@link #excluded}.<br>
         * {@code null} values shall be ignored.
         *
         * @param base The {@link COSBase}s to add to {@link #excluded}.
         * @return The {@link COSIncrement} itself, to allow method chaining.
         * @see #excluded
         */
        public COSIncrement exclude(COSBase... base)
        {
            if(base != null)
            {
                excluded.addAll(Arrays.asList(base));
            }
            return this;
        }

        /**
         * Returns {@code true}, if the given {@link COSBase} has been excluded from the increment, and hence is contained
         * in {@link #excluded}.
         *
         * @param base The {@link COSBase} to check for exclusion.
         * @return {@code true}, if the given {@link COSBase} has been excluded from the increment, and hence is contained
         * in {@link #excluded}.
         * @see #excluded
         */
        private boolean isExcluded(COSBase base)
        {
            return excluded.contains(base);
        }

        /**
         * Returns all indirect {@link COSBase}s, that shall be written to an increment as top level {@link COSObject}s.<br>
         * Calling this method will cause the increment to be initialized.
         *
         * @return All indirect {@link COSBase}s, that shall be written to an increment as top level {@link COSObject}s.
         * @see #objects
         */
        public Set<COSBase> getObjects()
        {
            if(!initialized && incrementOrigin != null)
            {
                collect(incrementOrigin.getCOSObject());
                initialized = true;
            }
            return objects;
        }

        /**
         * Return an iterator for the determined {@link #objects} contained in this {@link COSIncrement}.
         *
         * @return An iterator for the determined {@link #objects} contained in this {@link COSIncrement}.
         */
        @Override
        public Iterator<COSBase> iterator()
        {
            return getObjects().iterator();
        }

    }