Turbo.java :  » IDE-Netbeans » versioning » org » netbeans » modules » turbo » Java Open Source

Java Open Source » IDE Netbeans » versioning 
versioning » org » netbeans » modules » turbo » Turbo.java
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Contributor(s):
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 */
package org.netbeans.modules.turbo;

import org.openide.util.Lookup;
import org.openide.util.RequestProcessor;
import org.openide.ErrorManager;

import java.util.*;
import java.lang.ref.WeakReference;

/**
 * Turbo is general purpose entries/attributes dictionary with pluggable
 * layer enabling high scalability disk swaping implementations.
 * It allows to store several name identified values
 * for an entity identified by a key.
 *
 * <p>All methods take a <b><code>key</code></b> parameter. It
 * identifies entity to which attribute values are associated.
 * It must have properly implemented <code>equals()</code>,
 * <code>hashcode()</code> and <code>toString()</code> (returning
 * unique string) methods. Key lifetime must be well
 * understood to set cache strategy effeciently:
 * <ul>
 * <li>MAXIMUM: The implementation monitors key instance lifetime and does
 * not release data from memory until max. size limit reached or
 * key instance garbage collected whichever comes sooner. For
 * unbound caches the key must not be hard referenced from stored
 * value.
 *
 * <li>MINIMUM: For value lookups key's value based equalence is used.
 * Key's instance lifetime is monitored by instance based equalence.
 * Reasonable min limit must be set because there can be several
 * value equalent key instances but only one used key instance
 * actually stored in cache and lifetime monitored.
 * </ul>
 *
 * <p>Entry <b>name</b> fully indentifies contract between
 * consumers and providers. Contracts are described elsewhere.
 *
 * <p>The dictionary does not support storing <code>null</code>
 * <b>values</b>. Writing <code>null</code> value means that given
 * entry should be invalidated and removed (it actualy depends
 * on externally negotiated contract identified by name). Getting
 * <code>null</code> as requets result means that given value
 * is not (yet) known or does not exist at all.
 *
 * @author Petr Kuzel
 */
public final class Turbo {

    /** Default providers registry. */
    private static Lookup.Result providers;

    /** Custom providers 'registry'. */
    private final CustomProviders customProviders;

    private static WeakReference defaultInstance;

    private List listeners = new ArrayList(100);

    /** memory layer */
    private final Memory memory;

    private final Statistics statistics;

    private static Environment env;

    /**
     * Returns default instance. It's size is driven by
     * keys lifetime. It shrinks on keys become unreferenced
     * and grows without bounds on inserting keys.
     */
    public static synchronized Turbo getDefault() {
        Turbo turbo = null;
        if (defaultInstance != null) {
            turbo = (Turbo) defaultInstance.get();
        }

        if (turbo == null) {
            turbo = new Turbo(null, 47, -1);
            defaultInstance = new WeakReference(turbo);
        }

        return turbo;
    }

    /**
     * Creates new instance with customized providers layer.
     * @param providers never <code>null</null>
     * @param min minimum number of entries held by the cache
     * @param max maximum size or <code>-1</code> for unbound size
     *        (defined just by key instance lifetime)
     */
    public static synchronized Turbo createCustom(CustomProviders providers, int min, int max) {
        return new Turbo(providers, min, max);
    }

    private Turbo(CustomProviders customProviders, int min, int max) {
        statistics = Statistics.createInstance();
        memory = new Memory(statistics, min, max);
        this.customProviders = customProviders;
        if (customProviders == null && providers == null) {
            Lookup.Template t = new Lookup.Template(TurboProvider.class);
            synchronized(Turbo.class) {
                if (env == null) env = new Environment();
            }
            providers = env.getLookup().lookup(t);
        }
    }

    /** Tests can set different environment. Must be called before {@link #getDefault}. */
    static synchronized void initEnvironment(Environment environment) {
        assert env == null;
        env = environment;
        providers = null;
    }

    /** Logs cache statistics data. */
    protected void finalize() throws Throwable {
        super.finalize();
        statistics.shutdown();
    }

    /**
     * Reads given attribute for given entity key.
     * @param key a entity key, never <code>null</code>
     * @param name identifies requested entry, never <code>null</code>
     * @return entry value or <code>null</code> if it does not exist or unknown.
     */
    public Object readEntry(Object key, String name) {

        statistics.attributeRequest();

        // check memory cache

        if (memory.existsEntry(key, name)) {
            Object value = memory.get(key, name);
            statistics.memoryHit();
            return value;
        }

        // iterate over providers
        List speculative = new ArrayList(57);
        Object value = loadEntry(key, name, speculative);
        memory.put(key, name, value != null ? value : Memory.NULL);
        // XXX should fire here?  yes if name avalability changes should be
        // dispatched to clients that have not called prepare otherwise NO.

        // refire speculative results, can be optinized later on to fire
        // them lazilly on prepareAttribute or isPrepared calls
        Iterator it = speculative.iterator();
        while (it.hasNext()) {
            Object[] next = (Object[]) it.next();
            Object sKey =  next[0];
            String sName = (String) next[1];
            Object sValue = next[2];
            assert sKey != null;
            assert sName != null;
            fireEntryChange(sKey, sName, sValue);
        }

        return value;
    }

    private Iterator providers() {
        if (customProviders == null) {
            Collection plugins = providers.allInstances();
            List all = new ArrayList(plugins.size() +1);
            all.addAll(plugins);
            all.add(DefaultTurboProvider.getDefault());
            return all.iterator();
        } else {
            return customProviders.providers();
        }
    }

    /**
     * Iterate over providers asking for attribute values
     */
    private Object loadEntry(Object key, String name, List speculative) {

        TurboProvider provider;
        Iterator it = providers();
        while (it.hasNext()) {
            provider = (TurboProvider) it.next();
            try {
                if (provider.recognizesAttribute(name) && provider.recognizesEntity(key)) {
                    TurboProvider.MemoryCache cache = TurboProvider.MemoryCache.createDefault(memory, speculative);
                    Object value = provider.readEntry(key, name, cache);
                    statistics.providerHit();
                    return value;
                }
            } catch (ThreadDeath td) {
                throw td;
            } catch (Throwable t) {
                // error in provider
                ErrorManager.getDefault().annotate(t, "Error in provider " + provider + ", skipping... "); // NOI18N
                ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, t);  // XXX in Junit mode writes to stdout ommiting annotation!!!
            }
        }

        return null;
    }

    /**
     * Writes given attribute, value pair and notifies all listeners.
     * Written value is stored both into memory and providers layer.
     * The call speed depends on actually used provider. However do not
     * rely on synchronous provider  call. In future it may return and
     * fire immediately on writing into memory layer, populating providers
     * asychronously on background.
     *
     * <p>A client calling this method is reponsible for passing
     * valid value that is accepted by attribute serving providers.
     *
     * @param key identifies target, never <code>null</code>
     * @param name identifies attribute, never <code>null</code>
     * @param value actual attribute value to be stored, <code>null</code> behaviour
     * is defined specificaly for each name. It always invalidates memory entry
     * and it either invalidates or removes value from providers layer
     * (mening: value unknown versus value is known to not exist).
     *
     * <p>Client should consider a size of stored value. It must be corelated
     * with Turbo memory layer limits to avoid running ouf of memory.
     *
     * @return <ul>
     * <li><code>false</code> on write failure caused by a provider denying the value.
     * It means attribute contract violation and must be handled e.g.:
     * <p><code>
     *    boolean success = faq.writeAttribute(fo, name, value);<br>
     *    assert success : "Unexpected name[" + name + "] value[" + value + "] denial for " + key + "!";
     * </code>

     * <li><code>true</code> in all other cases includins I/O error.
     * After all it's just best efford cache. All values can be recomputed.
     * </ul>
     */
    public boolean writeEntry(Object key, String name, Object value) {

        if (value != null) {
            Object oldValue = memory.get(key, name);
            if (oldValue != null && oldValue.equals(value)) return true;  // XXX assuming provider has the same value, assert it!
        }

        int result = storeEntry(key, name, value);
        if (result >= 0) {
            // no one denied keep at least in memory cache
            memory.put(key, name, value);
            fireEntryChange(key, name, value);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Stores directly to providers.
     * @return 0 success, -1 contract failure, 1 other failure
     */
    int storeEntry(Object key, String name, Object value) {
        TurboProvider provider;
        Iterator it = providers();
        while (it.hasNext()) {
            provider = (TurboProvider) it.next();
            try {
                if (provider.recognizesAttribute(name) && provider.recognizesEntity(key)) {
                    if (provider.writeEntry(key, name, value)) {
                        return 0;
                    } else {
                        // for debugging purposes log which provider rejected defined name contract
                        IllegalArgumentException ex = new IllegalArgumentException("Attribute[" + name + "] value rejected by " + provider);
                        ErrorManager.getDefault().notify(ErrorManager.WARNING, ex);
                        return -1;
                    }
                }
            } catch (ThreadDeath td) {
                throw td;
            } catch (Throwable t) {
                // error in provider
                ErrorManager.getDefault().annotate(t, "Error in provider " + provider + ", skipping... "); // NOI18N
                ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, t);
            }
        }
        return 1;
    }

    /**
     * Checks value instant availability and possibly schedules its background
     * loading. It's designed to be called from UI tread.
     *
     * @return <ul>
     * <li><code>false</code> if not ready and providers must be consulted. It
     * asynchronously fires event possibly with <code>null</code> value
     * if given attribute does not exist.
     *
     * <li>
     * If <code>true</code> it's
     * ready and stays ready at least until next {@link #prepareEntry},
     * {@link #isPrepared}, {@link #writeEntry} <code>null</code> call
     * or {@link #readEntry} from the same thread.
     * </ul>
     */
    public boolean prepareEntry(Object key, String name) {

        statistics.attributeRequest();

        // check memory cache

        if (memory.existsEntry(key, name)) {
            statistics.memoryHit();
            return true;
        }

        // start asynchronous providers queriing
        scheduleLoad(key, name);
        return false;
    }

    /**
     * Checks name instant availability. Note that actual
     * value may be still <code>null</code>, in case
     * that it's known that value does not exist.
     *
     * @return <ul>
     * <li><code>false</code> if not present in memory for instant access.
     *
     * <li><code>true</code> when it's
     * ready and stays ready at least until next {@link #prepareEntry},
     * {@link #isPrepared}, {@link #writeEntry} <code>null</code> call
     * or {@link #readEntry} from the same thread.
     * </ul>
     */
    public boolean isPrepared(Object key, String name) {
        return memory.existsEntry(key, name);
    }

    /**
     * Gets key instance that it actually used in memory layer.
     * Client should keep reference to it if it wants to use
     * key lifetime monitoring cache size strategy.
     *
     * @param key key never <code>null</code>
     * @return key instance that is value-equalent or <code>null</code>
     * if monitored instance does not exist.
     */
    public Object getMonitoredKey(Object key) {
        return memory.getMonitoredKey(key);
    }

    public void addTurboListener(TurboListener l) {
        synchronized(listeners) {
            List copy = new ArrayList(listeners);
            copy.add(l);
            listeners = copy;
        }
    }

    public void removeTurboListener(TurboListener l) {
        synchronized(listeners) {
            List copy = new ArrayList(listeners);
            copy.remove(l);
            listeners = copy;
        }

    }

    protected void fireEntryChange(Object key, String name, Object value) {
        Iterator it = listeners.iterator();
        while (it.hasNext()) {
            TurboListener next = (TurboListener) it.next();
            next.entryChanged(key, name, value);
        }
    }

    /** For debugging purposes only. */
    public String toString() {
        StringBuffer sb = new StringBuffer("Turbo delegating to:");  // NOI18N
        Iterator it = providers();
        while (it.hasNext()) {
            TurboProvider provider = (TurboProvider) it.next();
            sb.append(" [" + provider + "]");   // NOI18N
        }
        return sb.toString();
    }

    /** Defines binding to external world. Used by tests. */
    static class Environment {
        /** Lookup that serves providers. */
        public Lookup getLookup() {
            return Lookup.getDefault();
        }
    }

    // Background loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


    /** Holds keys that were requested for background status retrieval. */
    private final Set prepareRequests = Collections.synchronizedSet(new LinkedHashSet(27));

    private static PreparationTask preparationTask;

    /** Tries to locate meta on disk on failure it forward to repository */
    private void scheduleLoad(Object key, String name) {
        synchronized(prepareRequests) {
            if (preparationTask == null) {
                preparationTask = new PreparationTask(prepareRequests);
                RequestProcessor.getDefault().post(preparationTask);
                statistics.backgroundThread();
            }
            preparationTask.notifyNewRequest(new Request(key, name));
        }
    }

    /** Requests queue entry featuring value based identity. */
    private final static class Request {
        private final Object key;
        private final String name;

        public Request(Object key, String name) {
            this.name = name;
            this.key = key;
        }

        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Request)) return false;

            final Request request = (Request) o;

            if (name != null ? !name.equals(request.name) : request.name != null) return false;
            if (key != null ? !key.equals(request.key) : request.key != null) return false;

            return true;
        }

        public int hashCode() {
            int result;
            result = (key != null ? key.hashCode() : 0);
            result = 29 * result + (name != null ? name.hashCode() : 0);
            return result;
        }

        public String toString() {
            return "Request[key=" + key + ", attr=" + name  + "]";
        }
    }

    /**
     * On background fetches data from providers layer.
     */
    private final class PreparationTask implements Runnable {

        private final Set requests;

        private static final int INACTIVITY_TIMEOUT = 123 * 1000; // 123 sec

        public PreparationTask(Set requests) {
            this.requests = requests;
        }

        public void run() {
            try {
                Thread.currentThread().setName("Turbo Async Fetcher");  // NOI18N
                while (waitForRequests()) {
                    Request request;
                    synchronized (requests) {
                        request = (Request) requests.iterator().next();
                        requests.remove(request);
                    }
                    Object key = request.key;
                    String name = request.name;
                    Object value;
                    boolean fire;
                    if (memory.existsEntry(key, name)) {

                        synchronized(Memory.class) {
                            fire = memory.existsEntry(key, name)  == false;
                            value = memory.get(key, name);
                        }
                        if (fire) {
                            statistics.providerHit(); // from our perpective we achieved hit
                        }
                    } else {
                        value = loadEntry(key, name, null);
                        // possible thread switch, so atomic fire test must be used
                        synchronized(Memory.class) {
                            fire = memory.existsEntry(key, name)  == false;
                            Object oldValue = memory.get(key, name);
                            memory.put(key, name, value != null ? value : Memory.NULL);
                            fire |= (oldValue != null && !oldValue.equals(value))
                                 || (oldValue == null && value != null);
                        }
                    }

                    // some one was faster, probably previous disk read that silently fetched whole directory
                    // our contract was to fire event once loading, stick to it. Note that get()
                    // silently populates stable memory area
//                    if (fire) {   ALWAYS because of above loadAttribute(key, name, null);
                        fireEntryChange(key, name, value);  // notify as soon as available in memory
//                    }

                }
            } catch (InterruptedException ex) {
                synchronized(requests) {
                    // forget about recent requests
                    requests.clear();
                }
            } finally {
                synchronized(requests) {
                    preparationTask = null;
                }
            }
        }

        /**
         * Wait for requests, it no request comes until timeout
         * it ommits suicide. It's respawned on next request however.
         */
        private boolean waitForRequests() throws InterruptedException {
            synchronized(requests) {
                if (requests.size() == 0) {
                    requests.wait(INACTIVITY_TIMEOUT);
                }
                return requests.size() > 0;
            }
        }

        public void notifyNewRequest(Request request) {
            synchronized(requests) {
                if (requests.add(request)) {
                    statistics.queueSize(requests.size());
                    requests.notify();
                } else {
                    statistics.duplicate();
                    statistics.providerHit();
                }
            }
        }

        public String toString() {
            return "Turbo.PreparationTask queue=[" + requests +"]";  // NOI18N
        }
    }


}
java2s.com  | Contact Us | Privacy Policy
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.