001/*
002 * Copyright (C) 2011 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.google.common.cache;
018
019import static com.google.common.base.Preconditions.checkArgument;
020
021import com.google.common.annotations.Beta;
022import com.google.common.annotations.VisibleForTesting;
023import com.google.common.base.Objects;
024import com.google.common.base.Splitter;
025import com.google.common.cache.LocalCache.Strength;
026import com.google.common.collect.ImmutableList;
027import com.google.common.collect.ImmutableMap;
028
029import java.util.List;
030import java.util.concurrent.TimeUnit;
031
032import javax.annotation.Nullable;
033
034/**
035 * A specification of a {@link CacheBuilder} configuration.
036 *
037 * <p>{@code CacheBuilderSpec} supports parsing configuration off of a string, which
038 * makes it especially useful for command-line configuration of a {@code CacheBuilder}.
039 *
040 * <p>The string syntax is a series of comma-separated keys or key-value pairs,
041 * each corresponding to a {@code CacheBuilder} method.
042 * <ul>
043 * <li>{@code concurrencyLevel=[integer]}: sets {@link CacheBuilder#concurrencyLevel}.
044 * <li>{@code initialCapacity=[integer]}: sets {@link CacheBuilder#initialCapacity}.
045 * <li>{@code maximumSize=[long]}: sets {@link CacheBuilder#maximumSize}.
046 * <li>{@code maximumWeight=[long]}: sets {@link CacheBuilder#maximumWeight}.
047 * <li>{@code expireAfterAccess=[duration]}: sets {@link CacheBuilder#expireAfterAccess}.
048 * <li>{@code expireAfterWrite=[duration]}: sets {@link CacheBuilder#expireAfterWrite}.
049 * <li>{@code refreshAfterWrite=[duration]}: sets {@link CacheBuilder#refreshAfterWrite}.
050 * <li>{@code weakKeys}: sets {@link CacheBuilder#weakKeys}.
051 * <li>{@code softValues}: sets {@link CacheBuilder#softValues}.
052 * <li>{@code weakValues}: sets {@link CacheBuilder#weakValues}.
053 * </ul>
054 *
055 * The set of supported keys will grow as {@code CacheBuilder} evolves, but existing keys
056 * will never be removed.
057 *
058 * <p>Durations are represented by an integer, followed by one of "d", "h", "m",
059 * or "s", representing days, hours, minutes, or seconds respectively.  (There
060 * is currently no syntax to request expiration in milliseconds, microseconds,
061 * or nanoseconds.)
062 *
063 * <p>Whitespace before and after commas and equal signs is ignored.  Keys may
064 * not be repeated;  it is also illegal to use the following pairs of keys in
065 * a single value:
066 * <ul>
067 * <li>{@code maximumSize} and {@code maximumWeight}
068 * <li>{@code softValues} and {@code weakValues}
069 * </ul>
070 *
071 * <p>{@code CacheBuilderSpec} does not support configuring {@code CacheBuilder} methods
072 * with non-value parameters.  These must be configured in code.
073 *
074 * <p>A new {@code CacheBuilder} can be instantiated from a {@code CacheBuilderSpec} using
075 * {@link CacheBuilder#from(CacheBuilderSpec)} or {@link CacheBuilder#from(String)}.
076 *
077 * @author Adam Winer
078 * @since 12.0
079 */
080@Beta
081public final class CacheBuilderSpec {
082  /** Parses a single value. */
083  private interface ValueParser {
084    void parse(CacheBuilderSpec spec, String key, @Nullable String value);
085  }
086
087  /** Splits each key-value pair. */
088  private static final Splitter KEYS_SPLITTER = Splitter.on(',').trimResults();
089
090  /** Splits the key from the value. */
091  private static final Splitter KEY_VALUE_SPLITTER = Splitter.on('=').trimResults();
092
093  /** Map of names to ValueParser. */
094  private static final ImmutableMap<String, ValueParser> VALUE_PARSERS =
095      ImmutableMap.<String, ValueParser>builder()
096          .put("initialCapacity", new InitialCapacityParser())
097          .put("maximumSize", new MaximumSizeParser())
098          .put("maximumWeight", new MaximumWeightParser())
099          .put("concurrencyLevel", new ConcurrencyLevelParser())
100          .put("weakKeys", new KeyStrengthParser(Strength.WEAK))
101          .put("softValues", new ValueStrengthParser(Strength.SOFT))
102          .put("weakValues", new ValueStrengthParser(Strength.WEAK))
103          .put("expireAfterAccess", new AccessDurationParser())
104          .put("expireAfterWrite", new WriteDurationParser())
105          .put("refreshAfterWrite", new RefreshDurationParser())
106          .put("refreshInterval", new RefreshDurationParser())
107          .build();
108
109  @VisibleForTesting Integer initialCapacity;
110  @VisibleForTesting Long maximumSize;
111  @VisibleForTesting Long maximumWeight;
112  @VisibleForTesting Integer concurrencyLevel;
113  @VisibleForTesting Strength keyStrength;
114  @VisibleForTesting Strength valueStrength;
115  @VisibleForTesting long writeExpirationDuration;
116  @VisibleForTesting TimeUnit writeExpirationTimeUnit;
117  @VisibleForTesting long accessExpirationDuration;
118  @VisibleForTesting TimeUnit accessExpirationTimeUnit;
119  @VisibleForTesting long refreshDuration;
120  @VisibleForTesting TimeUnit refreshTimeUnit;
121  /** Specification;  used for toParseableString(). */
122  private final String specification;
123
124  private CacheBuilderSpec(String specification) {
125    this.specification = specification;
126  }
127
128  /**
129   * Creates a CacheBuilderSpec from a string.
130   *
131   * @param cacheBuilderSpecification the string form
132   */
133  public static CacheBuilderSpec parse(String cacheBuilderSpecification) {
134    CacheBuilderSpec spec = new CacheBuilderSpec(cacheBuilderSpecification);
135    if (!cacheBuilderSpecification.isEmpty()) {
136      for (String keyValuePair : KEYS_SPLITTER.split(cacheBuilderSpecification)) {
137        List<String> keyAndValue = ImmutableList.copyOf(KEY_VALUE_SPLITTER.split(keyValuePair));
138        checkArgument(!keyAndValue.isEmpty(), "blank key-value pair");
139        checkArgument(keyAndValue.size() <= 2,
140            "key-value pair %s with more than one equals sign", keyValuePair);
141
142        // Find the ValueParser for the current key.
143        String key = keyAndValue.get(0);
144        ValueParser valueParser = VALUE_PARSERS.get(key);
145        checkArgument(valueParser != null, "unknown key %s", key);
146
147        String value = keyAndValue.size() == 1 ? null : keyAndValue.get(1);
148        valueParser.parse(spec, key, value);
149      }
150    }
151
152    return spec;
153  }
154
155  /**
156   * Returns a CacheBuilderSpec that will prevent caching.
157   */
158  public static CacheBuilderSpec disableCaching() {
159    // Maximum size of zero is one way to block caching
160    return CacheBuilderSpec.parse("maximumSize=0");
161  }
162
163  /**
164   * Returns a CacheBuilder configured according to this instance's specification.
165   */
166  CacheBuilder<Object, Object> toCacheBuilder() {
167    CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();
168    if (initialCapacity != null) {
169      builder.initialCapacity(initialCapacity);
170    }
171    if (maximumSize != null) {
172      builder.maximumSize(maximumSize);
173    }
174    if (maximumWeight != null) {
175      builder.maximumWeight(maximumWeight);
176    }
177    if (concurrencyLevel != null) {
178      builder.concurrencyLevel(concurrencyLevel);
179    }
180    if (keyStrength != null) {
181      switch (keyStrength) {
182        case WEAK:
183          builder.weakKeys();
184          break;
185        default:
186          throw new AssertionError();
187      }
188    }
189    if (valueStrength != null) {
190      switch (valueStrength) {
191        case SOFT:
192          builder.softValues();
193          break;
194        case WEAK:
195          builder.weakValues();
196          break;
197        default:
198          throw new AssertionError();
199      }
200    }
201    if (writeExpirationTimeUnit != null) {
202      builder.expireAfterWrite(writeExpirationDuration, writeExpirationTimeUnit);
203    }
204    if (accessExpirationTimeUnit != null) {
205      builder.expireAfterAccess(accessExpirationDuration, accessExpirationTimeUnit);
206    }
207    if (refreshTimeUnit != null) {
208      builder.refreshAfterWrite(refreshDuration, refreshTimeUnit);
209    }
210
211    return builder;
212  }
213
214  /**
215   * Returns a string that can be used to parse an equivalent
216   * {@code CacheBuilderSpec}.  The order and form of this representation is
217   * not guaranteed, except that reparsing its output will produce
218   * a {@code CacheBuilderSpec} equal to this instance.
219   */
220  public String toParsableString() {
221    return specification;
222  }
223
224  /**
225   * Returns a string representation for this CacheBuilderSpec instance.
226   * The form of this representation is not guaranteed.
227   */
228  @Override
229  public String toString() {
230    return Objects.toStringHelper(this).addValue(toParsableString()).toString();
231  }
232
233  @Override
234  public int hashCode() {
235    return Objects.hashCode(
236        initialCapacity,
237        maximumSize,
238        maximumWeight,
239        concurrencyLevel,
240        keyStrength,
241        valueStrength,
242        durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
243        durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
244        durationInNanos(refreshDuration, refreshTimeUnit));
245  }
246
247  @Override
248  public boolean equals(@Nullable Object obj) {
249    if (this == obj) {
250      return true;
251    }
252    if (!(obj instanceof CacheBuilderSpec)) {
253      return false;
254    }
255    CacheBuilderSpec that = (CacheBuilderSpec) obj;
256    return Objects.equal(initialCapacity, that.initialCapacity)
257        && Objects.equal(maximumSize, that.maximumSize)
258        && Objects.equal(maximumWeight, that.maximumWeight)
259        && Objects.equal(concurrencyLevel, that.concurrencyLevel)
260        && Objects.equal(keyStrength, that.keyStrength)
261        && Objects.equal(valueStrength, that.valueStrength)
262        && Objects.equal(durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
263            durationInNanos(that.writeExpirationDuration, that.writeExpirationTimeUnit))
264        && Objects.equal(durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
265            durationInNanos(that.accessExpirationDuration, that.accessExpirationTimeUnit))
266        && Objects.equal(durationInNanos(refreshDuration, refreshTimeUnit),
267            durationInNanos(that.refreshDuration, that.refreshTimeUnit));
268  }
269
270  /**
271   * Converts an expiration duration/unit pair into a single Long for hashing and equality.
272   * Uses nanos to match CacheBuilder implementation.
273   */
274  @Nullable private static Long durationInNanos(long duration, @Nullable TimeUnit unit) {
275    return (unit == null) ? null : unit.toNanos(duration);
276  }
277
278  /** Base class for parsing integers. */
279  abstract static class IntegerParser implements ValueParser {
280    protected abstract void parseInteger(CacheBuilderSpec spec, int value);
281
282    @Override
283    public void parse(CacheBuilderSpec spec, String key, String value) {
284      checkArgument(value != null && !value.isEmpty(), "value of key %s omitted", key);
285      try {
286        parseInteger(spec, Integer.parseInt(value));
287      } catch (NumberFormatException e) {
288        throw new IllegalArgumentException(
289            String.format("key %s value set to %s, must be integer", key, value), e);
290      }
291    }
292  }
293
294  /** Base class for parsing integers. */
295  abstract static class LongParser implements ValueParser {
296    protected abstract void parseLong(CacheBuilderSpec spec, long value);
297
298    @Override
299    public void parse(CacheBuilderSpec spec, String key, String value) {
300      checkArgument(value != null && !value.isEmpty(), "value of key %s omitted", key);
301      try {
302        parseLong(spec, Long.parseLong(value));
303      } catch (NumberFormatException e) {
304        throw new IllegalArgumentException(
305            String.format("key %s value set to %s, must be integer", key, value), e);
306      }
307    }
308  }
309
310  /** Parse initialCapacity */
311  static class InitialCapacityParser extends IntegerParser {
312    @Override
313    protected void parseInteger(CacheBuilderSpec spec, int value) {
314      checkArgument(spec.initialCapacity == null,
315          "initial capacity was already set to ", spec.initialCapacity);
316      spec.initialCapacity = value;
317    }
318  }
319
320  /** Parse maximumSize */
321  static class MaximumSizeParser extends LongParser {
322    @Override
323    protected void parseLong(CacheBuilderSpec spec, long value) {
324      checkArgument(spec.maximumSize == null,
325          "maximum size was already set to ", spec.maximumSize);
326      checkArgument(spec.maximumWeight == null,
327          "maximum weight was already set to ", spec.maximumWeight);
328      spec.maximumSize = value;
329    }
330  }
331
332  /** Parse maximumWeight */
333  static class MaximumWeightParser extends LongParser {
334    @Override
335    protected void parseLong(CacheBuilderSpec spec, long value) {
336      checkArgument(spec.maximumWeight == null,
337          "maximum weight was already set to ", spec.maximumWeight);
338      checkArgument(spec.maximumSize == null,
339          "maximum size was already set to ", spec.maximumSize);
340      spec.maximumWeight = value;
341    }
342  }
343
344  /** Parse concurrencyLevel */
345  static class ConcurrencyLevelParser extends IntegerParser {
346    @Override
347    protected void parseInteger(CacheBuilderSpec spec, int value) {
348      checkArgument(spec.concurrencyLevel == null,
349          "concurrency level was already set to ", spec.concurrencyLevel);
350      spec.concurrencyLevel = value;
351    }
352  }
353
354  /** Parse weakKeys */
355  static class KeyStrengthParser implements ValueParser {
356    private final Strength strength;
357
358    public KeyStrengthParser(Strength strength) {
359      this.strength = strength;
360    }
361
362    @Override
363    public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
364      checkArgument(value == null, "key %s does not take values", key);
365      checkArgument(spec.keyStrength == null, "%s was already set to %s", key, spec.keyStrength);
366      spec.keyStrength = strength;
367    }
368  }
369
370  /** Parse weakValues and softValues */
371  static class ValueStrengthParser implements ValueParser {
372    private final Strength strength;
373
374    public ValueStrengthParser(Strength strength) {
375      this.strength = strength;
376    }
377
378    @Override
379    public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
380      checkArgument(value == null, "key %s does not take values", key);
381      checkArgument(spec.valueStrength == null,
382        "%s was already set to %s", key, spec.valueStrength);
383
384      spec.valueStrength = strength;
385    }
386  }
387
388  /** Base class for parsing times with durations */
389  abstract static class DurationParser implements ValueParser {
390    protected abstract void parseDuration(
391        CacheBuilderSpec spec,
392        long duration,
393        TimeUnit unit);
394
395    @Override
396    public void parse(CacheBuilderSpec spec, String key, String value) {
397      checkArgument(value != null && !value.isEmpty(), "value of key %s omitted", key);
398      try {
399        char lastChar = value.charAt(value.length() - 1);
400        TimeUnit timeUnit;
401        switch (lastChar) {
402          case 'd':
403            timeUnit = TimeUnit.DAYS;
404            break;
405          case 'h':
406            timeUnit = TimeUnit.HOURS;
407            break;
408          case 'm':
409            timeUnit = TimeUnit.MINUTES;
410            break;
411          case 's':
412            timeUnit = TimeUnit.SECONDS;
413            break;
414          default:
415            throw new IllegalArgumentException(
416                String.format("key %s invalid format.  was %s, must end with one of [dDhHmMsS]",
417                    key, value));
418        }
419
420        long duration = Long.parseLong(value.substring(0, value.length() - 1));
421        parseDuration(spec, duration, timeUnit);
422      } catch (NumberFormatException e) {
423        throw new IllegalArgumentException(
424            String.format("key %s value set to %s, must be integer", key, value));
425      }
426    }
427  }
428
429  /** Parse expireAfterAccess */
430  static class AccessDurationParser extends DurationParser {
431    @Override protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
432      checkArgument(spec.accessExpirationTimeUnit == null, "expireAfterAccess already set");
433      spec.accessExpirationDuration = duration;
434      spec.accessExpirationTimeUnit = unit;
435    }
436  }
437
438  /** Parse expireAfterWrite */
439  static class WriteDurationParser extends DurationParser {
440    @Override protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
441      checkArgument(spec.writeExpirationTimeUnit == null, "expireAfterWrite already set");
442      spec.writeExpirationDuration = duration;
443      spec.writeExpirationTimeUnit = unit;
444    }
445  }
446
447  /** Parse refreshAfterWrite */
448  static class RefreshDurationParser extends DurationParser {
449    @Override protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
450      checkArgument(spec.refreshTimeUnit == null, "refreshAfterWrite already set");
451      spec.refreshDuration = duration;
452      spec.refreshTimeUnit = unit;
453    }
454  }
455}