1/*
2 * Copyright (C) 2009 The Guava Authors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.common.collect;
18
19import com.google.common.base.Function;
20import com.google.gwt.user.client.Timer;
21
22import java.util.LinkedHashMap;
23import java.util.Map;
24import java.util.concurrent.ConcurrentHashMap;
25import java.util.concurrent.ConcurrentMap;
26import java.util.concurrent.TimeUnit;
27
28/**
29 * MapMaker emulation. Since Javascript is single-threaded and have no references, this reduces to
30 * the creation of expiring and computing maps.
31 *
32 * @author Charles Fry
33 */
34public class MapMaker extends GenericMapMaker<Object, Object> {
35
36  // TODO(fry,user): ConcurrentHashMap never throws a CME when mutating the map during iteration, but
37  // this implementation (based on a LHM) does. This will all be replaced soon anyways, so leaving
38  // it as is for now.
39  private static class ExpiringComputingMap<K, V> extends LinkedHashMap<K, V>
40      implements ConcurrentMap<K, V> {
41    private final long expirationMillis;
42    private final Function<? super K, ? extends V> computer;
43    private final int maximumSize;
44
45    ExpiringComputingMap(long expirationMillis, int maximumSize, int initialCapacity,
46        float loadFactor) {
47      this(expirationMillis, null, maximumSize, initialCapacity, loadFactor);
48    }
49
50    ExpiringComputingMap(long expirationMillis, Function<? super K, ? extends V> computer,
51        int maximumSize, int initialCapacity, float loadFactor) {
52      super(initialCapacity, loadFactor, (maximumSize != -1));
53      this.expirationMillis = expirationMillis;
54      this.computer = computer;
55      this.maximumSize = maximumSize;
56    }
57
58    @Override
59    public V put(K key, V value) {
60      V result = super.put(key, value);
61      if (expirationMillis > 0) {
62        scheduleRemoval(key, value);
63      }
64      return result;
65    }
66
67    @Override
68    protected boolean removeEldestEntry(Map.Entry<K, V> ignored) {
69      return (maximumSize == -1) ? false : size() > maximumSize;
70    }
71
72    @Override
73    public V putIfAbsent(K key, V value) {
74      if (!containsKey(key)) {
75        return put(key, value);
76      } else {
77        return get(key);
78      }
79    }
80
81    @Override
82    public boolean remove(Object key, Object value) {
83      if (containsKey(key) && get(key).equals(value)) {
84        remove(key);
85        return true;
86      }
87      return false;
88    }
89
90    @Override
91    public boolean replace(K key, V oldValue, V newValue) {
92      if (containsKey(key) && get(key).equals(oldValue)) {
93        put(key, newValue);
94        return true;
95      }
96      return false;
97    }
98
99    @Override
100    public V replace(K key, V value) {
101      return containsKey(key) ? put(key, value) : null;
102    }
103
104    private void scheduleRemoval(final K key, final V value) {
105      // from MapMaker
106      /*
107       * TODO: Keep weak reference to map, too. Build a priority queue out of the entries themselves
108       * instead of creating a task per entry. Then, we could have one recurring task per map (which
109       * would clean the entire map and then reschedule itself depending upon when the next
110       * expiration comes). We also want to avoid removing an entry prematurely if the entry was set
111       * to the same value again.
112       */
113      Timer timer = new Timer() {
114        @Override
115        public void run() {
116          remove(key, value);
117        }
118      };
119      timer.schedule((int) expirationMillis);
120    }
121
122    @Override
123    public V get(Object k) {
124      // from CustomConcurrentHashMap
125      V result = super.get(k);
126      if (result == null && computer != null) {
127        /*
128         * This cast isn't safe, but we can rely on the fact that K is almost always passed to
129         * Map.get(), and tools like IDEs and Findbugs can catch situations where this isn't the
130         * case.
131         *
132         * The alternative is to add an overloaded method, but the chances of a user calling get()
133         * instead of the new API and the risks inherent in adding a new API outweigh this little
134         * hole.
135         */
136        @SuppressWarnings("unchecked")
137        K key = (K) k;
138        result = compute(key);
139      }
140      return result;
141    }
142
143    private V compute(K key) {
144      // from MapMaker
145      V value;
146      try {
147        value = computer.apply(key);
148      } catch (Throwable t) {
149        throw new ComputationException(t);
150      }
151
152      if (value == null) {
153        String message = computer + " returned null for key " + key + ".";
154        throw new NullPointerException(message);
155      }
156      put(key, value);
157      return value;
158    }
159  }
160
161  private int initialCapacity = 16;
162  private float loadFactor = 0.75f;
163  private long expirationMillis = 0;
164  private int maximumSize = -1;
165  private boolean useCustomMap;
166
167  public MapMaker() {}
168
169  @Override
170  public MapMaker initialCapacity(int initialCapacity) {
171    if (initialCapacity < 0) {
172      throw new IllegalArgumentException();
173    }
174    this.initialCapacity = initialCapacity;
175    return this;
176  }
177
178  public MapMaker loadFactor(float loadFactor) {
179    if (loadFactor <= 0) {
180      throw new IllegalArgumentException();
181    }
182    this.loadFactor = loadFactor;
183    return this;
184  }
185
186  @Override
187  public
188  MapMaker expiration(long duration, TimeUnit unit) {
189    return expireAfterWrite(duration, unit);
190  }
191
192  @Override
193  MapMaker expireAfterWrite(long duration, TimeUnit unit) {
194    if (expirationMillis != 0) {
195      throw new IllegalStateException(
196          "expiration time of " + expirationMillis + " ns was already set");
197    }
198    if (duration <= 0) {
199      throw new IllegalArgumentException("invalid duration: " + duration);
200    }
201    this.expirationMillis = unit.toMillis(duration);
202    useCustomMap = true;
203    return this;
204  }
205
206  @Override
207  MapMaker maximumSize(int maximumSize) {
208    if (this.maximumSize != -1) {
209      throw new IllegalStateException("maximum size of " + maximumSize + " was already set");
210    }
211    if (maximumSize < 0) {
212      throw new IllegalArgumentException("invalid maximum size: " + maximumSize);
213    }
214    this.maximumSize = maximumSize;
215    useCustomMap = true;
216    return this;
217  }
218
219  @Override
220  public MapMaker concurrencyLevel(int concurrencyLevel) {
221    if (concurrencyLevel < 1) {
222      throw new IllegalArgumentException("GWT only supports a concurrency level of 1");
223    }
224    // GWT technically only supports concurrencyLevel == 1, but we silently
225    // ignore other positive values.
226    return this;
227  }
228
229  @Override
230  MapMaker strongKeys() {
231    return this;
232  }
233
234  @Override
235  MapMaker strongValues() {
236    return this;
237  }
238
239  @Override
240  public <K, V> ConcurrentMap<K, V> makeMap() {
241    return useCustomMap
242        ? new ExpiringComputingMap<K, V>(expirationMillis, null, maximumSize, initialCapacity,
243            loadFactor)
244        : new ConcurrentHashMap<K, V>(initialCapacity, loadFactor);
245  }
246
247  @Override
248  public <K, V> ConcurrentMap<K, V> makeComputingMap(Function<? super K, ? extends V> computer) {
249    return new ExpiringComputingMap<K, V>(expirationMillis, computer, maximumSize, initialCapacity,
250        loadFactor);
251  }
252}
253