aboutsummaryrefslogtreecommitdiffstats
path: root/guava-gwt/src-super/com/google/common/collect/super/com/google/common/collect/MapMaker.java
blob: 6f88f8922123c65f7e2437854d29f0bdfe699c35 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/*
 * Copyright (C) 2009 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.collect;

import com.google.common.base.Function;
import com.google.gwt.user.client.Timer;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

/**
 * MapMaker emulation. Since Javascript is single-threaded and have no references, this reduces to
 * the creation of expiring and computing maps.
 *
 * @author Charles Fry
 */
public class MapMaker extends GenericMapMaker<Object, Object> {

  // TODO(fry,user): ConcurrentHashMap never throws a CME when mutating the map during iteration, but
  // this implementation (based on a LHM) does. This will all be replaced soon anyways, so leaving
  // it as is for now.
  private static class ExpiringComputingMap<K, V> extends LinkedHashMap<K, V>
      implements ConcurrentMap<K, V> {
    private final long expirationMillis;
    private final Function<? super K, ? extends V> computer;
    private final int maximumSize;

    ExpiringComputingMap(
        long expirationMillis, int maximumSize, int initialCapacity, float loadFactor) {
      this(expirationMillis, null, maximumSize, initialCapacity, loadFactor);
    }

    ExpiringComputingMap(long expirationMillis, Function<? super K, ? extends V> computer,
        int maximumSize, int initialCapacity, float loadFactor) {
      super(initialCapacity, loadFactor, (maximumSize != -1));
      this.expirationMillis = expirationMillis;
      this.computer = computer;
      this.maximumSize = maximumSize;
    }

    @Override
    public V put(K key, V value) {
      V result = super.put(key, value);
      if (expirationMillis > 0) {
        scheduleRemoval(key, value);
      }
      return result;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> ignored) {
      return (maximumSize == -1) ? false : size() > maximumSize;
    }

    @Override
    public V putIfAbsent(K key, V value) {
      if (!containsKey(key)) {
        return put(key, value);
      } else {
        return get(key);
      }
    }

    @Override
    public boolean remove(Object key, Object value) {
      if (containsKey(key) && get(key).equals(value)) {
        remove(key);
        return true;
      }
      return false;
    }

    @Override
    public boolean replace(K key, V oldValue, V newValue) {
      if (containsKey(key) && get(key).equals(oldValue)) {
        put(key, newValue);
        return true;
      }
      return false;
    }

    @Override
    public V replace(K key, V value) {
      return containsKey(key) ? put(key, value) : null;
    }

    private void scheduleRemoval(final K key, final V value) {
      // from MapMaker
      /*
       * TODO: Keep weak reference to map, too. Build a priority queue out of the entries themselves
       * instead of creating a task per entry. Then, we could have one recurring task per map (which
       * would clean the entire map and then reschedule itself depending upon when the next
       * expiration comes). We also want to avoid removing an entry prematurely if the entry was set
       * to the same value again.
       */
      Timer timer = new Timer() {
        @Override
        public void run() {
          remove(key, value);
        }
      };
      timer.schedule((int) expirationMillis);
    }

    @Override
    public V get(Object k) {
      // from CustomConcurrentHashMap
      V result = super.get(k);
      if (result == null && computer != null) {
        /*
         * This cast isn't safe, but we can rely on the fact that K is almost always passed to
         * Map.get(), and tools like IDEs and Findbugs can catch situations where this isn't the
         * case.
         *
         * The alternative is to add an overloaded method, but the chances of a user calling get()
         * instead of the new API and the risks inherent in adding a new API outweigh this little
         * hole.
         */
        @SuppressWarnings("unchecked")
        K key = (K) k;
        result = compute(key);
      }
      return result;
    }

    private V compute(K key) {
      // from MapMaker
      V value;
      try {
        value = computer.apply(key);
      } catch (Throwable t) {
        throw new ComputationException(t);
      }

      if (value == null) {
        String message = computer + " returned null for key " + key + ".";
        throw new NullPointerException(message);
      }
      put(key, value);
      return value;
    }
  }

  private int initialCapacity = 16;
  private float loadFactor = 0.75f;
  private long expirationMillis = 0;
  private int maximumSize = -1;
  private boolean useCustomMap;

  public MapMaker() {}

  @Override
  public MapMaker initialCapacity(int initialCapacity) {
    if (initialCapacity < 0) {
      throw new IllegalArgumentException();
    }
    this.initialCapacity = initialCapacity;
    return this;
  }

  public MapMaker loadFactor(float loadFactor) {
    if (loadFactor <= 0) {
      throw new IllegalArgumentException();
    }
    this.loadFactor = loadFactor;
    return this;
  }

  @Override
  MapMaker expireAfterWrite(long duration, TimeUnit unit) {
    if (expirationMillis != 0) {
      throw new IllegalStateException(
          "expiration time of " + expirationMillis + " ns was already set");
    }
    if (duration <= 0) {
      throw new IllegalArgumentException("invalid duration: " + duration);
    }
    this.expirationMillis = unit.toMillis(duration);
    useCustomMap = true;
    return this;
  }

  @Override
  MapMaker maximumSize(int maximumSize) {
    if (this.maximumSize != -1) {
      throw new IllegalStateException("maximum size of " + maximumSize + " was already set");
    }
    if (maximumSize < 0) {
      throw new IllegalArgumentException("invalid maximum size: " + maximumSize);
    }
    this.maximumSize = maximumSize;
    useCustomMap = true;
    return this;
  }

  @Override
  public MapMaker concurrencyLevel(int concurrencyLevel) {
    if (concurrencyLevel < 1) {
      throw new IllegalArgumentException("GWT only supports a concurrency level of 1");
    }
    // GWT technically only supports concurrencyLevel == 1, but we silently
    // ignore other positive values.
    return this;
  }

  @Override
  public <K, V> ConcurrentMap<K, V> makeMap() {
    return useCustomMap
        ? new ExpiringComputingMap<K, V>(
            expirationMillis, null, maximumSize, initialCapacity, loadFactor)
        : new ConcurrentHashMap<K, V>(initialCapacity, loadFactor);
  }

  @Override
  public <K, V> ConcurrentMap<K, V> makeComputingMap(Function<? super K, ? extends V> computer) {
    return new ExpiringComputingMap<K, V>(
        expirationMillis, computer, maximumSize, initialCapacity, loadFactor);
  }
}