001/*002// This software is subject to the terms of the Eclipse Public License v1.0003// Agreement, available at the following URL:004// http://www.eclipse.org/legal/epl-v10.html.005// You must accept the terms of that agreement to use this software.006//007// Copyright (C) 2010-2012 Pentaho008// All Rights Reserved.009*/010package mondrian.util;011012import java.util.*;013014/**015 * Provides a way to pass objects via a string moniker.016 *017 * <p>This is useful if you are passing an object over an API that admits only018 * strings (such as019 * {@link java.sql.DriverManager#getConnection(String, java.util.Properties)})020 * and where tricks such as {@link ThreadLocal} do not work. The callee needs021 * to be on the same JVM, but other than that, the object does not need to have022 * any special properties. In particular, it does not need to be serializable.023 *024 * <p>First, register the object to obtain a lock box entry. Every lock box025 * entry has a string moniker that is very difficult to guess, is unique, and026 * is not recycled. Pass that moniker to the callee, and from that moniker the027 * callee can retrieve the entry and with it the object.028 *029 * <p>The entry cannot be forged and cannot be copied. If you lose the entry,030 * you can no longer retrieve the object, and the entry will eventually be031 * garbage-collected. If you call {@link #deregister(Entry)}, callees will no032 * longer be able to look up an entry from its moniker.033 *034 * <p>The same is not true of the moniker string. Having the moniker string035 * does not guarantee that the entry will not be removed. Therefore, the036 * creator who called {@link #register(Object)} and holds the entry controls037 * the lifecycle.038 *039 * <p>The moniker consists of the characters A..Z, a..z, 0..9, $, #, and is040 * thus a valid (case-sensitive) identifier.041 *042 * <p>All methods are thread-safe.043 *044 * @author jhyde045 * @since 2010/11/18046 */047public class LockBox {048 private static final Object DUMMY = new Object();049050 /**051 * Mapping from monikers to entries.052 *053 * <p>Per WeakHashMap: "An entry in a WeakHashMap will automatically be054 * removed when its key is no longer in ordinary use. More precisely,055 * the presence of a mapping for a given key will not prevent the key056 * from being discarded by the garbage collector, that is, made057 * finalizable, finalized, and then reclaimed. When a key has been058 * discarded its entry is effectively removed from the map..."059 *060 * <p>LockBoxEntryImpl meets those constraints precisely. An entry will061 * disappear when the caller forgets the key, or calls deregister. If062 * the caller (or someone) still has the moniker, it is not sufficient063 * to prevent the entry from being garbage collected.064 */065 private final Map<LockBoxEntryImpl, Object> map =066 new WeakHashMap<LockBoxEntryImpl, Object>();067 private final Random random = new Random();068 private final byte[] bytes = new byte[16]; // 128 bit... secure enough069 private long ordinal;070071 /**072 * Creates a LockBox.073 */074 public LockBox() {075 }076077 private static Object wrap(Object o) {078 return o == null ? DUMMY : o;079 }080081 private static Object unwrap(Object value) {082 return value == DUMMY ? null : value;083 }084085 /**086 * Adds an object to the lock box, and returns a key for it.087 *088 * <p>The object may be null. The same object may be registered multiple089 * times; each time it is registered, a new entry with a new string090 * moniker is generated.091 *092 * @param o Object to register. May be null.093 * @return Entry containing the object's string key and the object itself094 */095 public synchronized Entry register(Object o) {096 String moniker = generateMoniker();097 final LockBoxEntryImpl entry = new LockBoxEntryImpl(this, moniker);098 map.put(entry, wrap(o));099 return entry;100 }101102 /**103 * Generates a non-repeating, random string.104 *105 * <p>Must be called from synchronized context.106 *107 * @return Non-repeating random string108 */109 private String generateMoniker() {110 // The prefixed ordinal ensures that the string never repeats. Of111 // course, there will be a pattern to the first few chars of the112 // returned string, but that doesn't matter for these purposes.113 random.nextBytes(bytes);114115 // Remove trailing '='. It is padding required by base64 spec but does116 // not help us.117 String base64 = Base64.encodeBytes(bytes);118 while (base64.endsWith("=")) {119 base64 = base64.substring(0, base64.length() - 1);120 }121 // Convert '/' to '$' and '+' to '_'. The resulting moniker starts with122 // a '$', and contains only A-Z, a-z, 0-9, _ and $; it is a valid123 // identifier, and does not need to be quoted in XML or an HTTP URL.124 base64 = base64.replace('/', '$');125 base64 = base64.replace('+', '_');126 return "$"127 + Long.toHexString(++ordinal)128 + base64;129 }130131 /**132 * Removes an entry from the lock box.133 *134 * <p>It is safe to call this method multiple times.135 *136 * @param entry Entry to deregister137 * @return Whether the object was removed138 */139 public synchronized boolean deregister(Entry entry) {140 return map.remove(entry) != null;141 }142143 /**144 * Retrieves an entry using its string moniker. Returns null if there is145 * no entry with that moniker.146 *147 * <p>Successive calls for the same moniker do not necessarily return148 * the same {@code Entry} object, but those entries'149 * {@link LockBox.Entry#getValue()} will nevertheless return the same150 * value.</p>151 *152 * @param moniker Moniker of the lock box entry153 * @return Entry, or null if there is no entry with this moniker154 */155 public synchronized Entry get(String moniker) {156 // Linear scan through keys. Not perfect, but safer than maintaining157 // a map that might mistakenly allow/prevent GC.158 for (LockBoxEntryImpl entry : map.keySet()) {159 if (entry.moniker.equals(moniker)) {160 return entry;161 }162 }163 return null;164 }165166 /**167 * Entry in a {@link LockBox}.168 *169 * <p>Entries are created using {@link LockBox#register(Object)}.170 *171 * <p>The object can be retrieved using {@link #getValue()} if you have172 * the entry, or {@link LockBox#get(String)} if you only have the173 * string key.174 *175 * <p>Holding onto an Entry will prevent the entry, and the associated176 * value from being garbage collected. Holding onto the moniker will177 * not prevent garbage collection.</p>178 */179 public interface Entry180 {181 /**182 * Returns the value in this lock box entry.183 *184 * @return Value in this lock box entry.185 */186 Object getValue();187188 /**189 * String key by which to identify this object. Not null, not easily190 * forged, and unique within the lock box.191 *192 * <p>Given this moniker, you retrieve the Entry using193 * {@link LockBox#get(String)}. The retrieved Entry will will have the194 * same moniker, and will be able to access the same value.</p>195 *196 * @return String key197 */198 String getMoniker();199200 /**201 * Returns whether the entry is still valid. Returns false if202 * {@link LockBox#deregister(mondrian.util.LockBox.Entry)} has been203 * called on this Entry or any entry with the same moniker.204 *205 * @return whether entry is registered206 */207 boolean isRegistered();208 }209210 /**211 * Implementation of {@link Entry}.212 *213 * <p>It is important that entries cannot be forged. Therefore this class,214 * and its constructor, are private. And equals and hashCode use object215 * identity.216 */217 private static class LockBoxEntryImpl implements Entry {218 private final LockBox lockBox;219 private final String moniker;220221 private LockBoxEntryImpl(LockBox lockBox, String moniker) {222 this.lockBox = lockBox;223 this.moniker = moniker;224 }225226 public Object getValue() {227 final Object value = lockBox.map.get(this);228 if (value == null) {229 throw new RuntimeException(230 "LockBox has no entry with moniker [" + moniker + "]");231 }232 return unwrap(value);233 }234235 public String getMoniker() {236 return moniker;237 }238239 public boolean isRegistered() {240 return lockBox.map.containsKey(this);241 }242 }243}244245// End LockBox.java