/* * 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