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    
017    package com.google.common.cache;
018    
019    import static com.google.common.base.Preconditions.checkArgument;
020    
021    import com.google.common.annotations.Beta;
022    import com.google.common.annotations.VisibleForTesting;
023    import com.google.common.base.Objects;
024    import com.google.common.base.Splitter;
025    import com.google.common.cache.LocalCache.Strength;
026    import com.google.common.collect.ImmutableList;
027    import com.google.common.collect.ImmutableMap;
028    
029    import java.util.List;
030    import java.util.concurrent.TimeUnit;
031    
032    import 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
081    public 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(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    }