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

nl.vpro.domain.api.media.ChangeIterator Maven / Gradle / Ivy

Go to download

Contains the objects used by the Frontend API, like forms and result objects

There is a newer version: 8.3.3
Show newest version
package nl.vpro.domain.api.media;

import lombok.extern.slf4j.Slf4j;

import java.time.Instant;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Objects;

import nl.vpro.domain.api.MediaChange;
import nl.vpro.domain.api.profile.ProfileDefinition;
import nl.vpro.domain.constraint.AbstractFilter;
import nl.vpro.domain.media.MediaObject;
import nl.vpro.util.FilteringIterator;

/**
 * @author Roelof Jan Koekoek
 * @since 3.0
 */
@Slf4j
public class ChangeIterator implements Iterator {

    private static final int LOG_BATCH = 50000;

    private final Iterator wrapped;

    private final ProfileDefinition current;

    private final ProfileDefinition previous;

    private final Instant sinceDate;
    private final Long since;

    private MediaChange next;
    private MediaChange nextnext;

    private boolean needsFindNext = true;
    private Boolean hasNext = null; // not known yet

    private Long sequence; // sequence number of most recent DocumentChange (whether filtered or not)
    private Instant publishDate;

    private long count = 0; // total number found until now
    private long updatesSkipped = 0; // how many are skipped in total now
    private long deletedsSkipped = 0; // how many are skipped in total now
    private long currentSkipCount = 0; // how many are skipped in e sequence now
    private final long keepAliveNulls; // after how many sequential skips a 'null' must be returned.

    /**
     * @param iterator      The original couchdb change feed
     * @param since         The since argument, which is sometimes needed during filtering
     * @param current       The current profile as used by filtering
     * @param previous      The profile at the moment 'since'.
     * @param keepAliveNull (optional) If filtering will skip very many changes, the next() call may take several minutes.
     *                      If you set this to some finite positive value (it defaults to MAX_VALUE), every so many skips it wil return 'null'.
     *                      This null can be used to send something to the client to keep the connection alive.
     */

    public ChangeIterator(Iterator iterator, Instant since, final ProfileDefinition current, final ProfileDefinition previous, long keepAliveNull) {
        this.wrapped = new FilteringIterator<>(iterator, Objects::nonNull);
        this.sinceDate = since;
        this.since = null;
        this.current = nullIsMatchAlways(current);
        this.previous = nullIsMatchAlways(previous);
        this.keepAliveNulls = keepAliveNull;
    }

    public ChangeIterator(Iterator iterator, Instant since, final ProfileDefinition current, final ProfileDefinition previous) {
        this(iterator, since, current, previous, Long.MAX_VALUE);
    }

    public ChangeIterator(Iterator iterator, Long since, final ProfileDefinition current, final ProfileDefinition previous, long keepAliveNull) {
        this.wrapped = new FilteringIterator<>(iterator, Objects::nonNull);
        this.since = since;
        this.sinceDate = null;
        this.current = nullIsMatchAlways(current);
        this.previous = nullIsMatchAlways(previous);
        this.keepAliveNulls = keepAliveNull;
    }

    public ChangeIterator(Iterator iterator, Long since, final ProfileDefinition current, final ProfileDefinition previous) {
        this(iterator, since, current, previous, Long.MAX_VALUE);
    }

    @Override
    public boolean hasNext() {
        findNext();
        return hasNext;

    }

    @Override
    public MediaChange next() {
        findNext();
        if (!hasNext) {
            throw new NoSuchElementException();
        }
        needsFindNext = true;
        if (currentSkipCount > keepAliveNulls) {
            currentSkipCount = 0;
            return null;
        }
        MediaChange result = next;
        next = null;
        return result;
    }

    public Long getSequence() {
        return sequence;
    }

    public Instant getPublishDate() {
        return publishDate;
    }

    protected void findNext() {
        // we administrate the next change, but also the 'next next' one.
        // See NPA-105
        if (needsFindNext) {
            while (wrapped.hasNext()) {
                MediaChange n = wrapped.next();
                count++;
                sequence = n.getSequence();
                publishDate = n.getPublishDate();
                boolean isDelete = n.isDeleted();
                if (needsOutputAndAdapt(n)) {
                    next = nextnext;
                    nextnext = n;
                    if (next != null) {
                        currentSkipCount = 0;
                        break;
                    }
                } else {
                    if (isDelete) {
                        deletedsSkipped++;
                    } else {
                        updatesSkipped++;
                    }
                    currentSkipCount++;
                    if (nextnext != null) {
                        nextnext.setSequence(sequence);
                        nextnext.setPublishDate(publishDate);
                    }
                    if (currentSkipCount > keepAliveNulls) {
                        hasNext = true;
                        needsFindNext = false;
                        return;
                    }
                }
                if (count % LOG_BATCH == 0) {
                    log.info("{}: sequence: {} count: {}  skipped: updates: {}, deletes: {}", current.getName(), sequence, count, updatesSkipped, deletedsSkipped);
                }
            }
            // handle last one
            if (next == null && nextnext != null) {
                next = nextnext;
                next.setSequence(sequence);
                next.setPublishDate(publishDate);
                nextnext = null;
                if (count > LOG_BATCH) {
                    log.info("{}: sequence: {} count: {}  skipped: updates: {}, deletes: {}. Ready.", current.getName(), sequence, count, updatesSkipped, deletedsSkipped);
                }
            }
            needsFindNext = false;
            hasNext = next != null;
        }
    }

    protected boolean needsOutputAndAdapt(MediaChange input) {
        if (input.isDeleted()) {
            return deleteNeedsOutput(input);
        } else {
            return updateNeedsOutput(input);
        }
    }

    protected boolean deleteNeedsOutput(MediaChange input) {
        if (! (input.getMedia() == null || previous.test(input.getMedia()))) {
            return false;
        }
        return appliesToSince(input);
    }


    protected boolean updateNeedsOutput(MediaChange input) {
        final boolean inCurrent = current.test(input.getMedia());
        final boolean inPrevious = previous.test(input.getMedia());

        if (inCurrent) {
            // Is newer then since or did not apply under previous profile
            return sendAfterSince(input) || !inPrevious;
        } else {
            if (sendBeforeSince(input) && !inPrevious) {
                // Older then since but outside previous as well
                return false;
            }
            // Return a delete since we can't determine whether the previous revision applied to the current
            // profile without extra document retrievals (NPA-134)
            input.setDeleted(true);
            return true;
        }
    }


    protected boolean sendAfterSince(MediaChange input) {
        return appliesToSince(input);
    }

    protected boolean appliesToSince(MediaChange input) {
        if (! hasSince()) {
            return true;
        }
        if (since == null) {
            return sinceDate == null || input.getPublishDate() == null || !input.getPublishDate().isBefore(sinceDate);
        } else {
            return input.getSequence() >= since;
        }
    }

    protected boolean sendBeforeSince(MediaChange input) {
        if (! hasSince()) {
            return false;
        }
        if (since == null) {
            return input.getPublishDate() == null || input.getPublishDate().isBefore(sinceDate);
        } else {
            return input.getSequence() < since;
        }
    }

    protected boolean hasSince() {
        return since != null || sinceDate != null;
    }

    @Override
    public void remove() {
        wrapped.remove();
    }

    private static ProfileDefinition nullIsMatchAlways(ProfileDefinition def) {
        if (def != null) {
            return def;
        }
        return new ProfileDefinition<>(new AbstractFilter(null) {
        }); // matches everything
    }


}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy