net.sf.jasperreports.engine.fill.JRAbstractLRUVirtualizer.java Source code

Java tutorial

Introduction

Here is the source code for net.sf.jasperreports.engine.fill.JRAbstractLRUVirtualizer.java

Source

/*
 * JasperReports - Free Java Reporting Library.
 * Copyright (C) 2005 Works, Inc. All rights reserved.
 * http://www.works.com
 * Copyright (C) 2005 - 2019 TIBCO Software Inc. All rights reserved.
 * http://www.jaspersoft.com
 *
 * Unless you have purchased a commercial license agreement from Jaspersoft,
 * the following license terms apply:
 *
 * This program is part of JasperReports.
 *
 * JasperReports is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * JasperReports is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with JasperReports. If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * Licensed to Jaspersoft Corporation under a Contributer Agreement
 */
package net.sf.jasperreports.engine.fill;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;

import org.apache.commons.collections4.map.ReferenceMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import net.sf.jasperreports.engine.JRRuntimeException;
import net.sf.jasperreports.engine.JRVirtualizable;
import net.sf.jasperreports.engine.JRVirtualizer;
import net.sf.jasperreports.engine.util.LocalVirtualizationSerializer;
import net.sf.jasperreports.engine.util.VirtualizationSerializer;

/**
 * Abstract base for LRU and serialization based virtualizer
 *
 * @author John Bindel
 */
public abstract class JRAbstractLRUVirtualizer implements JRVirtualizer {
    private static final Log log = LogFactory.getLog(JRAbstractLRUVirtualizer.class);

    protected static class CacheReference extends WeakReference<JRVirtualizable> {
        private final String id;

        public CacheReference(JRVirtualizable o, ReferenceQueue<JRVirtualizable> queue) {
            super(o, queue);
            id = o.getUID();
        }

        public String getId() {
            return id;
        }
    }

    /**
     * This class keeps track of how many objects are currently in memory, and
     * when there are too many, it pushes the last touched one to disk.
     */
    protected class Cache {
        private final int maxSize;
        private final ReferenceQueue<JRVirtualizable> refQueue;
        private final LinkedHashMap<String, CacheReference> map;

        Cache(int maxSize) {
            this.maxSize = maxSize;
            map = new LinkedHashMap<String, CacheReference>(16, 0.75f, true);
            refQueue = new ReferenceQueue<JRVirtualizable>();
        }

        protected JRVirtualizable getMapValue(CacheReference val) {
            JRVirtualizable o;
            if (val == null) {
                o = null;
            } else {
                if (val.isEnqueued()) {
                    o = null;
                } else {
                    o = val.get();
                }
            }
            return o;
        }

        protected CacheReference toMapValue(JRVirtualizable val) {
            return val == null ? null : new CacheReference(val, refQueue);
        }

        protected void purge() {
            CacheReference ref;
            while ((ref = (CacheReference) refQueue.poll()) != null) {
                map.remove(ref.getId());
            }
        }

        public boolean contains(String id) {
            purge();

            return map.containsKey(id);
        }

        public JRVirtualizable get(String id) {
            purge();

            return getMapValue(map.get(id));
        }

        public JRVirtualizable put(String id, JRVirtualizable o) {
            purge();

            return getMapValue(map.put(id, toMapValue(o)));
        }

        public List<JRVirtualizable> evictionCandidates() {
            if (map.size() <= maxSize) {
                return Collections.<JRVirtualizable>emptyList();
            }

            int candidateCount = map.size() - maxSize;
            List<JRVirtualizable> candidates = new ArrayList<JRVirtualizable>();
            Iterator<Entry<String, CacheReference>> mapIterator = map.entrySet().iterator();
            while (candidates.size() < candidateCount && mapIterator.hasNext()) {
                Entry<String, CacheReference> entry = mapIterator.next();
                JRVirtualizable value = getMapValue(entry.getValue());

                if (value == null) {
                    // this entry will get removed by purge()
                    --candidateCount;
                } else if (isEvictable(value)) {
                    if (log.isDebugEnabled()) {
                        log.debug("LRU eviction candidate: " + entry.getKey());
                    }

                    candidates.add(value);
                }
            }

            if (candidates.size() < candidateCount) {
                log.debug("The virtualizer is used by more contexts than its in-memory cache size " + maxSize);
            }

            return candidates;
        }

        public JRVirtualizable remove(String id) {
            purge();

            return getMapValue(map.remove(id));
        }

        public Iterator<String> idIterator() {
            purge();

            final Iterator<CacheReference> valsIt = map.values().iterator();
            return new Iterator<String>() {
                @Override
                public boolean hasNext() {
                    return valsIt.hasNext();
                }

                @Override
                public String next() {
                    CacheReference ref = valsIt.next();
                    return ref.getId();
                }

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

    protected final VirtualizationSerializer serializer;

    protected final Cache pagedIn;

    protected final ReferenceMap<String, Object> pagedOut;

    protected volatile WeakReference<JRVirtualizable> lastObjectRef;
    protected ReferenceMap<JRVirtualizationContext, Object> lastObjectMap;
    protected ReferenceMap<Object, Boolean> lastObjectSet;

    private boolean readOnly;

    /**
     * @param maxSize
     *            the maximum size (in JRVirtualizable objects) of the paged in
     *            cache.
     */
    protected JRAbstractLRUVirtualizer(int maxSize) {
        this(new LocalVirtualizationSerializer(), maxSize);
    }

    protected JRAbstractLRUVirtualizer(VirtualizationSerializer serializer, int maxSize) {
        this.serializer = serializer;

        this.pagedIn = new Cache(maxSize);
        this.pagedOut = new ReferenceMap<String, Object>(ReferenceMap.ReferenceStrength.HARD,
                ReferenceMap.ReferenceStrength.WEAK);
        this.lastObjectRef = null;

        this.lastObjectMap = new ReferenceMap<JRVirtualizationContext, Object>(ReferenceMap.ReferenceStrength.WEAK,
                ReferenceMap.ReferenceStrength.WEAK);
        this.lastObjectSet = new ReferenceMap<Object, Boolean>(ReferenceMap.ReferenceStrength.WEAK,
                ReferenceMap.ReferenceStrength.HARD);
    }

    protected synchronized final boolean isPagedOut(String id) {
        return pagedOut.containsKey(id);
    }

    protected synchronized boolean isPagedOutAndTouch(JRVirtualizable o, String uid) {
        boolean virtualized = isPagedOut(uid);
        if (!virtualized) {
            touch(o);
        }
        return virtualized;
    }

    protected JRVirtualizable lastObject() {
        WeakReference<JRVirtualizable> ref = lastObjectRef;
        JRVirtualizable object = ref == null ? null : ref.get();
        return object;
    }

    protected final void setLastObject(JRVirtualizable o) {
        JRVirtualizable currentLast = lastObject();
        if (o != null && currentLast != o) {
            // lastObject is mostly an optimization, we don't care if we don't have atomic operations here
            this.lastObjectRef = new WeakReference<JRVirtualizable>(o);

            synchronized (this) {
                JRVirtualizationContext context = o.getContext();
                Object ownerLast = lastObjectMap.get(context);
                if (ownerLast != o) {
                    if (ownerLast != null) {
                        lastObjectSet.remove(ownerLast);
                    }
                    lastObjectMap.put(context, o);
                    lastObjectSet.put(o, Boolean.TRUE);
                }
            }
        }
    }

    /**
     * Sets the read only mode for the virtualizer.
     * <p/>
     * When in read-only mode, the virtualizer assumes that virtualizable objects are final
     * and any change in a virtualizable object's data is discarded.
     * <p/>
     * When the virtualizer is used for multiple virtualization contexts (in shared mode),
     * calling this method would override the read-only flags from all the contexts and all the
     * objects will be manipulated in read-only mode.
     * Use {@link JRVirtualizationContext#setReadOnly(boolean) JRVirtualizationContext.setReadOnly(boolean)}
     * to set the read-only mode for one specific context.
     *
     * @param ro the read-only mode to set
     */
    public void setReadOnly(boolean ro) {
        this.readOnly = ro;
    }

    /**
     * Determines whether the virtualizer is in read-only mode.
     *
     * @return whether the virtualizer is in read-only mode
     * @see #setReadOnly(boolean)
     */
    public boolean isReadOnly() {
        return readOnly;
    }

    protected final boolean isReadOnly(JRVirtualizable o) {
        return readOnly || o.getContext().isReadOnly();
    }

    @Override
    public void registerObject(JRVirtualizable o) {
        if (log.isDebugEnabled()) {
            log.debug("registering " + o.getUID());
        }

        synchronized (this) {
            setLastObject(o);
            JRVirtualizable old = pagedIn.put(o.getUID(), o);
            if (old != null && old != o) {
                pagedIn.put(o.getUID(), old);
                throw new IllegalStateException("Wrong object stored with UID \"" + o.getUID() + "\"");
            }
        }

        if (log.isDebugEnabled()) {
            log.debug("registered object " + o + " with id " + o.getUID());
        }

        evict();
    }

    protected boolean isEvictable(JRVirtualizable value) {
        return value.getContext().isDisposed() || !lastObjectSet.containsKey(value);
    }

    protected void evict() {
        //FIXME lucianc also attempt to evict on non-put operations if the last evict was not successful 
        //FIXME lucianc prevent two threads from attempting to evict the same objects 

        List<JRVirtualizable> candidates;
        synchronized (this) {
            candidates = pagedIn.evictionCandidates();
        }

        for (JRVirtualizable o : candidates) {
            String uid = o.getUID();
            if (o.getContext().tryLock()) {
                try {
                    boolean evictable;
                    synchronized (this) {
                        // check again due to sequential locking
                        evictable = pagedIn.contains(uid) && isEvictable(o);
                        if (evictable) {
                            pagedIn.remove(uid);
                        }
                    }

                    if (evictable) {
                        if (log.isDebugEnabled()) {
                            log.debug("evicting " + uid);
                        }

                        if (!o.getContext().isDisposed()) {
                            virtualizeData(o);
                        }
                    } else {
                        if (log.isDebugEnabled()) {
                            log.debug("no longer evictable: " + uid);
                        }
                    }
                } finally {
                    o.getContext().unlock();
                }
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("couldn't lock for eviction " + uid);
                }
            }
        }
    }

    @Override
    public void deregisterObject(JRVirtualizable o) {
        String uid = o.getUID();

        if (log.isDebugEnabled()) {
            log.debug("deregistering " + uid);
        }

        //try to remove virtual data
        try {
            dispose(o);
        } catch (Exception e) {
            log.error("Error removing virtual data", e);
            //ignore
        }

        synchronized (this) {
            JRVirtualizable oldIn = pagedIn.remove(uid);
            if (oldIn != null) {
                if (oldIn != o) {
                    pagedIn.put(uid, oldIn);
                    throw new IllegalStateException("Wrong object stored with UID \"" + o.getUID() + "\"");
                }

                Object contextLast = lastObjectMap.get(o.getContext());
                if (contextLast == o) {
                    lastObjectMap.remove(o.getContext());
                    lastObjectSet.remove(o);
                }
            } else {
                Object oldOut = pagedOut.remove(uid);
                if (oldOut != null && oldOut != o) {
                    pagedOut.put(uid, oldOut);
                    throw new IllegalStateException("Wrong object stored with UID \"" + o.getUID() + "\"");
                }
            }

            // We don't really care if someone deregisters an object
            // that's not registered.
        }

        if (log.isDebugEnabled()) {
            log.debug("deregistered object " + o + " with id " + o.getUID());
        }
    }

    @Override
    public void touch(JRVirtualizable o) {
        // If we just touched this object, don't touch it again.
        if (lastObject() != o) {
            //FIXME lucianc this doesn't scale well with concurrency
            // get the object from the map to update LRU order
            JRVirtualizable internalObject;
            synchronized (this) {
                internalObject = pagedIn.get(o.getUID());
            }

            setLastObject(internalObject);
        }
    }

    @Override
    public void requestData(JRVirtualizable o) {
        String uid = o.getUID();
        boolean evictRequired = false;

        o.getContext().lock();
        try {
            if (isPagedOutAndTouch(o, uid)) {
                if (log.isDebugEnabled()) {
                    log.debug("internalizing " + uid);
                }

                // unvirtualize
                try {
                    pageIn(o);
                } catch (IOException e) {
                    log.error("Error devirtualizing object", e);
                    throw new JRRuntimeException(e);
                }

                synchronized (this) {
                    setLastObject(o);
                    pagedOut.remove(uid);
                    pagedIn.put(uid, o);
                }

                o.afterInternalization();

                evictRequired = true;
            }
        } finally {
            o.getContext().unlock();
        }

        if (evictRequired) {
            evict();
        }
    }

    @Override
    public void clearData(JRVirtualizable o) {
        String uid = o.getUID();
        if (isPagedOutAndTouch(o, uid)) {
            // remove virtual data
            dispose(uid);

            synchronized (this) {
                pagedOut.remove(uid);
            }
        }
    }

    @Override
    public void virtualizeData(JRVirtualizable o) {
        String uid = o.getUID();
        if (!isPagedOut(uid)) {
            if (log.isDebugEnabled()) {
                log.debug("externalizing " + uid);
            }

            o.beforeExternalization();

            // virtualize
            try {
                pageOut(o);
            } catch (IOException e) {
                log.error("Error virtualizing object", e);
                throw new JRRuntimeException(e);
            }

            o.afterExternalization();

            // Wait until we know it worked before tossing the data.
            o.removeVirtualData();

            synchronized (this) {
                pagedOut.put(uid, o);
            }
        }
    }

    @Override
    protected void finalize() throws Throwable //NOSONAR
    {
        cleanup();

        super.finalize();
    }

    /**
     * Writes serialized indentity and virtual data of a virtualizable object to a stream.
     *
     * @param o the serialized object
     * @param out the output stream
     * @throws JRRuntimeException
     */
    protected final void writeData(JRVirtualizable o, OutputStream out) throws JRRuntimeException {
        try {
            serializer.writeData(o, out);
        } catch (IOException e) {
            log.error("Error virtualizing object", e);
            throw new JRRuntimeException(e);
        }
    }

    /**
     * Reads serialized identity and virtual data for a virtualizable object
     * from a stream.
     *
     * @param o the virtualizable object
     * @param in the input stream
     * @throws JRRuntimeException
     */
    protected final void readData(JRVirtualizable o, InputStream in) throws JRRuntimeException {
        try {
            serializer.readData(o, in);
        } catch (IOException e) {
            log.error("Error devirtualizing object", e);
            throw new JRRuntimeException(e);
        }
    }

    protected synchronized void reset() {
        readOnly = false;
    }

    protected final void disposeAll() {
        // Remove all paged-out swap files.
        for (Iterator<String> it = pagedOut.keySet().iterator(); it.hasNext();) {
            String id = it.next();
            try {
                dispose(id);
                it.remove();
            } catch (Exception e) {
                log.error("Error cleaning up virtualizer.", e);
                // Do nothing because we want to try to remove all swap files.
            }
        }

        for (Iterator<String> it = pagedIn.idIterator(); it.hasNext();) {
            String id = it.next();
            try {
                dispose(id);
                it.remove();
            } catch (Exception e) {
                log.error("Error cleaning up virtualizer.", e);
                // Do nothing because we want to try to remove all swap files.
            }
        }
    }

    /**
     * Writes a virtualizable object's data to an external storage.
     *
     * @param o a virtualizable object
     * @throws IOException
     */
    protected abstract void pageOut(JRVirtualizable o) throws IOException;

    /**
     * Reads a virtualizable object's data from an external storage.
     *
     * @param o a virtualizable object
     * @throws IOException
     */
    protected abstract void pageIn(JRVirtualizable o) throws IOException;

    protected void dispose(JRVirtualizable o) {
        dispose(o.getUID());
    }

    /**
     * Removes the external data associated with a virtualizable object.
     *
     * @param virtualId the ID of the virtualizable object
     */
    protected abstract void dispose(String virtualId);
}