/*
* This software is subject to the terms of the Eclipse Public License v1.0
* Agreement, available at the following URL:
* http://www.eclipse.org/legal/epl-v10.html.
* You must accept the terms of that agreement to use this software.
*
* Copyright (c) 2002-2013 Pentaho Corporation..  All rights reserved.
*/

package mondrian.util;

import java.util.*;

/**
 * Provides a way to pass objects via a string moniker.
 *
 * <p>This is useful if you are passing an object over an API that admits only
 * strings (such as
 * {@link java.sql.DriverManager#getConnection(String, java.util.Properties)})
 * and where tricks such as {@link ThreadLocal} do not work. The callee needs
 * to be on the same JVM, but other than that, the object does not need to have
 * any special properties. In particular, it does not need to be serializable.
 *
 * <p>First, register the object to obtain a lock box entry. Every lock box
 * entry has a string moniker that is very difficult to guess, is unique, and
 * is not recycled. Pass that moniker to the callee, and from that moniker the
 * callee can retrieve the entry and with it the object.
 *
 * <p>The entry cannot be forged and cannot be copied. If you lose the entry,
 * you can no longer retrieve the object, and the entry will eventually be
 * garbage-collected. If you call {@link #deregister(Entry)}, callees will no
 * longer be able to look up an entry from its moniker.
 *
 * <p>The same is not true of the moniker string. Having the moniker string
 * does not guarantee that the entry will not be removed. Therefore, the
 * creator who called {@link #register(Object)} and holds the entry controls
 * the lifecycle.
 *
 * <p>The moniker consists of the characters A..Z, a..z, 0..9, $, #, and is
 * thus a valid (case-sensitive) identifier.
 *
 * <p>All methods are thread-safe.
 *
 * @author jhyde
  * @since 2010/11/18
 */
public class LockBox {
    private static final Object DUMMY = new Object();

    /**
     * Mapping from monikers to entries.
     *
     * <p>Per WeakHashMap: "An entry in a WeakHashMap will automatically be
     * removed when its key is no longer in ordinary use. More precisely,
     * the presence of a mapping for a given key will not prevent the key
     * from being discarded by the garbage collector, that is, made
     * finalizable, finalized, and then reclaimed. When a key has been
     * discarded its entry is effectively removed from the map..."
     *
     * <p>LockBoxEntryImpl meets those constraints precisely. An entry will
     * disappear when the caller forgets the key, or calls deregister. If
     * the caller (or someone) still has the moniker, it is not sufficient
     * to prevent the entry from being garbage collected.
     */
    private final Map<LockBoxEntryImpl, Object> map =
        new WeakHashMap<LockBoxEntryImpl, Object>();
    private final Random random = new Random();
    private final byte[] bytes = new byte[16]; // 128 bit... secure enough
    private long ordinal;

    /**
     * Creates a LockBox.
     */
    public LockBox() {
    }

    private static Object wrap(Object o) {
        return o == null ? DUMMY : o;
    }

    private static Object unwrap(Object value) {
        return value == DUMMY ? null : value;
    }

    /**
     * Adds an object to the lock box, and returns a key for it.
     *
     * <p>The object may be null. The same object may be registered multiple
     * times; each time it is registered, a new entry with a new string
     * moniker is generated.
     *
     * @param o Object to register. May be null.
     * @return Entry containing the object's string key and the object itself
     */
    public synchronized Entry register(Object o) {
        String moniker = generateMoniker();
        final LockBoxEntryImpl entry = new LockBoxEntryImpl(this, moniker);
        map.put(entry, wrap(o));
        return entry;
    }

    /**
     * Generates a non-repeating, random string.
     *
     * <p>Must be called from synchronized context.
     *
     * @return Non-repeating random string
     */
    private String generateMoniker() {
        // The prefixed ordinal ensures that the string never repeats. Of
        // course, there will be a pattern to the first few chars of the
        // returned string, but that doesn't matter for these purposes.
        random.nextBytes(bytes);

        // Remove trailing '='. It is padding required by base64 spec but does
        // not help us.
        String base64 = Base64.encodeBytes(bytes);
        while (base64.endsWith("=")) {
            base64 = base64.substring(0, base64.length() - 1);
        }
        // Convert '/' to '$' and '+' to '_'. The resulting moniker starts with
        // a '$', and contains only A-Z, a-z, 0-9, _ and $; it is a valid
        // identifier, and does not need to be quoted in XML or an HTTP URL.
        base64 = base64.replace('/', '$');
        base64 = base64.replace('+', '_');
        return "$"
               + Long.toHexString(++ordinal)
               + base64;
    }

    /**
     * Removes an entry from the lock box.
     *
     * <p>It is safe to call this method multiple times.
     *
     * @param entry Entry to deregister
     * @return Whether the object was removed
     */
    public synchronized boolean deregister(Entry entry) {
        return map.remove(entry) != null;
    }

    /**
     * Retrieves an entry using its string moniker. Returns null if there is
     * no entry with that moniker.
     *
     * <p>Successive calls for the same moniker do not necessarily return
     * the same {@code Entry} object, but those entries'
     * {@link LockBox.Entry#getValue()} will nevertheless return the same
     * value.</p>
     *
     * @param moniker Moniker of the lock box entry
     * @return Entry, or null if there is no entry with this moniker
     */
    public synchronized Entry get(String moniker) {
        // Linear scan through keys. Not perfect, but safer than maintaining
        // a map that might mistakenly allow/prevent GC.
        for (LockBoxEntryImpl entry : map.keySet()) {
            if (entry.moniker.equals(moniker)) {
                return entry;
            }
        }
        return null;
    }

    /**
     * Entry in a {@link LockBox}.
     *
     * <p>Entries are created using {@link LockBox#register(Object)}.
     *
     * <p>The object can be retrieved using {@link #getValue()} if you have
     * the entry, or {@link LockBox#get(String)} if you only have the
     * string key.
     *
     * <p>Holding onto an Entry will prevent the entry, and the associated
     * value from being garbage collected. Holding onto the moniker will
     * not prevent garbage collection.</p>
     */
    public interface Entry
    {
        /**
         * Returns the value in this lock box entry.
         *
         * @return Value in this lock box entry.
         */
        Object getValue();

        /**
         * String key by which to identify this object. Not null, not easily
         * forged, and unique within the lock box.
         *
         * <p>Given this moniker, you retrieve the Entry using
         * {@link LockBox#get(String)}. The retrieved Entry will will have the
         * same moniker, and will be able to access the same value.</p>
         *
         * @return String key
         */
        String getMoniker();

        /**
         * Returns whether the entry is still valid. Returns false if
         * {@link LockBox#deregister(mondrian.util.LockBox.Entry)} has been
         * called on this Entry or any entry with the same moniker.
         *
         * @return whether entry is registered
         */
        boolean isRegistered();
    }

    /**
     * Implementation of {@link Entry}.
     *
     * <p>It is important that entries cannot be forged. Therefore this class,
     * and its constructor, are private. And equals and hashCode use object
     * identity.
     */
    private static class LockBoxEntryImpl implements Entry {
        private final LockBox lockBox;
        private final String moniker;

        private LockBoxEntryImpl(LockBox lockBox, String moniker) {
            this.lockBox = lockBox;
            this.moniker = moniker;
        }

        public Object getValue() {
            final Object value = lockBox.map.get(this);
            if (value == null) {
                throw new RuntimeException(
                    "LockBox has no entry with moniker [" + moniker + "]");
            }
            return unwrap(value);
        }

        public String getMoniker() {
            return moniker;
        }

        public boolean isRegistered() {
            return lockBox.map.containsKey(this);
        }
    }
}

// End LockBox.java
