001    
002    /*
003     * Copyright (C) 2011 Archie L. Cobbs. All rights reserved.
004     *
005     * $Id: IdGenerator.java 312 2012-03-26 21:46:17Z archie.cobbs $
006     */
007    
008    package org.dellroad.stuff.java;
009    
010    import java.lang.ref.Reference;
011    import java.lang.ref.ReferenceQueue;
012    import java.lang.ref.WeakReference;
013    import java.util.HashMap;
014    import java.util.LinkedList;
015    import java.util.concurrent.Callable;
016    
017    /**
018     * Registry of unique IDs for objects.
019     *
020     * <p>
021     * Instances support creating unique {@code long} ID numbers for objects, as well as setting the unique ID
022     * to a specific value for any unregistered object.
023     *
024     * <p>
025     * This class uses object identity, not {@link Object#equals Object.equals()}, to distinguish objects.
026     *
027     * <p>
028     * Weak references are used to ensure that registered objects can be garbage collected normally.
029     *
030     * <p>
031     * New {@code long} ID numbers are issued serially; after 2<sup>64</sup>-1 invocations of {@link #getId getId()},
032     * an {@link IllegalStateException} will be thrown.
033     *
034     * @see org.dellroad.stuff.jibx.IdMapper
035     */
036    public class IdGenerator {
037    
038        private static final ThreadLocal<LinkedList<IdGenerator>> CURRENT = new ThreadLocal<LinkedList<IdGenerator>>() {
039            @Override
040            public LinkedList<IdGenerator> initialValue() {
041                return new LinkedList<IdGenerator>();
042            }
043        };
044    
045        private final HashMap<Ref, Long> idMap = new HashMap<Ref, Long>();
046        private final HashMap<Long, Ref> refMap = new HashMap<Long, Ref>();
047        private final ReferenceQueue<Object> queue = new ReferenceQueue<Object>();
048    
049        private long next = 1;
050    
051        /**
052         * Get a unique ID for the given object.
053         *
054         * <p>
055         * If this method has been previously invoked on this instance with the same {@code obj} parameter (where "same" means
056         * object identity, not {@link Object#equals Object.equals()} identity), then the same ID value will be returned.
057         * Otherwise a new ID value will be returned.
058         *
059         * <p>
060         * New IDs are assigned sequentially starting at {@code 1}. No conflict avoidance with IDs assigned
061         * via {@link #setId setId()} is performed; if there is a conflict, an exception is thrown.
062         *
063         * @throws IllegalArgumentException if {@code obj} is null
064         * @throws IllegalStateException if the next sequential ID has already been assigned to a different object
065         *  via {@link #setId setId()}
066         * @throws IllegalStateException if all 2<sup>64</sup>-1 values have been used up
067         * @return a non-zero, unique identifier for {@code obj}
068         */
069        public synchronized long getId(Object obj) {
070            if (obj == null)
071                throw new IllegalArgumentException("null obj");
072            this.flush();
073            Ref ref = new Ref(obj, this.queue);
074            Long id = this.idMap.get(ref);
075            if (id == null) {
076                if (this.next == 0)
077                    throw new IllegalStateException("no more identifiers left!");
078                id = this.next++;
079                this.idMap.put(ref, id);
080                this.refMap.put(id, ref);
081            }
082            return id;
083        }
084    
085        /**
086         * Assign a unique ID to the given object. Does nothing if the object and ID number are already associated.
087         *
088         * @param obj object to assign
089         * @param id unique ID number to assign
090         * @throws IllegalArgumentException if {@code obj} is null
091         * @throws IllegalArgumentException if {@code id} has already been assigned to some other object
092         */
093        public synchronized void setId(Object obj, long id) {
094            if (obj == null)
095                throw new IllegalArgumentException("null obj");
096            this.flush();
097            Ref ref = this.refMap.get(id);
098            if (ref != null) {
099                if (ref.get() != obj)
100                    throw new IllegalArgumentException("id " + id + " is already assigned to another object");
101                return;
102            }
103            ref = new Ref(obj, this.queue);
104            this.idMap.put(ref, id);
105            this.refMap.put(id, ref);
106        }
107    
108        /**
109         * Get the object assigned to the given ID.
110         *
111         * @param id unique ID
112         * @return object associated with that ID, or null if no object is assigned to {@code id}
113         */
114        public synchronized Object getObject(long id) {
115            this.flush();
116            Ref ref = this.refMap.get(id);
117            return ref != null ? ref.get() : null;
118        }
119    
120        /**
121         * Flush any cleared weak references.
122         *
123         * <p>
124         * This operation is invoked by {@link #getId getId()}, so it's not necessary to explicitly invoke it.
125         * However, if a lot of previously ID'd objects have been garbage collected since the last call to
126         * {@link #getId getId()}, then invoking this method may free up some additional memory.
127         */
128        public synchronized void flush() {
129            Reference<? extends Object> entry;
130            while ((entry = this.queue.poll()) != null) {
131                Ref ref = (Ref)entry;
132                Long id = this.idMap.get(ref);
133                this.idMap.remove(ref);
134                this.refMap.remove(id);
135            }
136        }
137    
138        /**
139         * Create a new {@link IdGenerator} and make it available via {@link #get()} for the duration of the given operation.
140         *
141         * <p>
142         * This method is re-entrant: nested invocations of this method in the same thread will cause new {@link IdGenerator}
143         * instances to be created and used for the duration of the nested action.
144         *
145         * @param action action to perform, and which may successfully invoke {@link #get}
146         * @throws NullPointerException if {@code action} is null
147         */
148        public static void run(final Runnable action) {
149            IdGenerator.CURRENT.get().push(new IdGenerator());
150            try {
151                action.run();
152            } finally {
153                IdGenerator.CURRENT.get().pop();
154            }
155        }
156    
157        /**
158         * Create a new {@link IdGenerator} and make it available via {@link #get()} for the duration of the given operation.
159         *
160         * <p>
161         * This method is re-entrant: nested invocations of this method in the same thread will cause new {@link IdGenerator}
162         * instances to be created and used for the duration of the nested action.
163         *
164         * @param action action to perform, and which may successfully invoke {@link #get}
165         * @return result of invoking {@code action}
166         * @throws NullPointerException if {@code action} is null
167         */
168        public static <R> R run(final Callable<R> action) throws Exception {
169            IdGenerator.CURRENT.get().push(new IdGenerator());
170            try {
171                return action.call();
172            } finally {
173                IdGenerator.CURRENT.get().pop();
174            }
175        }
176    
177        /**
178         * Get the {@link IdGenerator} associated with the current thread.
179         * This method only works when the current thread is running within an invocation of {@link #run run()};
180         * otherwise, an {@link IllegalStateException} is thrown.
181         *
182         * @return the {@link IdGenerator} created in the most recent, still running invocation of {@link #run} in this thread
183         * @throws IllegalStateException if there is not such instance
184         */
185        public static IdGenerator get() {
186            IdGenerator current = IdGenerator.CURRENT.get().peek();
187            if (current == null)
188                throw new IllegalStateException("not running within an invocation of run()");
189            return current;
190        }
191    
192        // Reference to a registered object that weakly references the actual object
193        private static final class Ref extends WeakReference<Object> {
194    
195            private final int hashCode;
196    
197            Ref(Object obj, ReferenceQueue<Object> queue) {
198                super(obj, queue);
199                if (obj == null)
200                    throw new IllegalArgumentException("null obj");
201                this.hashCode = System.identityHashCode(obj);
202            }
203    
204            @Override
205            public boolean equals(Object obj) {
206                if (obj == this)
207                    return true;
208                if (obj == null || obj.getClass() != this.getClass())
209                    return false;
210                Ref that = (Ref)obj;
211                obj = this.get();
212                return obj != null ? obj == that.get() : false;
213            }
214    
215            @Override
216            public int hashCode() {
217                return this.hashCode;
218            }
219        }
220    }
221