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.length() > 0) {
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      
229      @Override
230      public String toString() {
231        return Objects.toStringHelper(this).addValue(toParsableString()).toString();
232      }
233    
234      
235      @Override
236      public int hashCode() {
237        return Objects.hashCode(
238            initialCapacity,
239            maximumSize,
240            maximumWeight,
241            concurrencyLevel,
242            keyStrength,
243            valueStrength,
244            durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
245            durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
246            durationInNanos(refreshDuration, refreshTimeUnit));
247      }
248    
249      
250      @Override
251      public boolean equals(Object obj) {
252        if (this == obj) {
253          return true;
254        }
255        if (!(obj instanceof CacheBuilderSpec)) {
256          return false;
257        }
258        CacheBuilderSpec that = (CacheBuilderSpec) obj;
259        return Objects.equal(initialCapacity, that.initialCapacity)
260            && Objects.equal(maximumSize, that.maximumSize)
261            && Objects.equal(maximumWeight, that.maximumWeight)
262            && Objects.equal(concurrencyLevel, that.concurrencyLevel)
263            && Objects.equal(keyStrength, that.keyStrength)
264            && Objects.equal(valueStrength, that.valueStrength)
265            && Objects.equal(durationInNanos(writeExpirationDuration, writeExpirationTimeUnit),
266                durationInNanos(that.writeExpirationDuration, that.writeExpirationTimeUnit))
267            && Objects.equal(durationInNanos(accessExpirationDuration, accessExpirationTimeUnit),
268                durationInNanos(that.accessExpirationDuration, that.accessExpirationTimeUnit))
269            && Objects.equal(durationInNanos(refreshDuration, refreshTimeUnit),
270                durationInNanos(that.refreshDuration, that.refreshTimeUnit));
271      }
272    
273      /**
274       * Converts an expiration duration/unit pair into a single Long for hashing and equality.
275       * Uses nanos to match CacheBuilder implementation.
276       */
277      @Nullable private static Long durationInNanos(long duration, @Nullable TimeUnit unit) {
278        return (unit == null) ? null : unit.toNanos(duration);
279      }
280    
281      /** Base class for parsing integers. */
282      abstract static class IntegerParser implements ValueParser {
283        protected abstract void parseInteger(CacheBuilderSpec spec, int value);
284    
285        public void parse(CacheBuilderSpec spec, String key, String value) {
286          checkArgument(value != null && value.length() > 0, "value of key %s omitted", key);
287          try {
288            parseInteger(spec, Integer.parseInt(value));
289          } catch (NumberFormatException e) {
290            throw new IllegalArgumentException(
291                String.format("key %s value set to %s, must be integer", key, value), e);
292          }
293        }
294      }
295    
296      /** Base class for parsing integers. */
297      abstract static class LongParser implements ValueParser {
298        protected abstract void parseLong(CacheBuilderSpec spec, long value);
299    
300        public void parse(CacheBuilderSpec spec, String key, String value) {
301          checkArgument(value != null && value.length() > 0, "value of key %s omitted", key);
302          try {
303            parseLong(spec, Long.parseLong(value));
304          } catch (NumberFormatException e) {
305            throw new IllegalArgumentException(
306                String.format("key %s value set to %s, must be integer", key, value), e);
307          }
308        }
309      }
310    
311      /** Parse initialCapacity */
312      static class InitialCapacityParser extends IntegerParser {
313        
314        @Override
315        protected void parseInteger(CacheBuilderSpec spec, int value) {
316          checkArgument(spec.initialCapacity == null,
317              "initial capacity was already set to ", spec.initialCapacity);
318          spec.initialCapacity = value;
319        }
320      }
321    
322      /** Parse maximumSize */
323      static class MaximumSizeParser extends LongParser {
324        
325        @Override
326        protected void parseLong(CacheBuilderSpec spec, long value) {
327          checkArgument(spec.maximumSize == null,
328              "maximum size was already set to ", spec.maximumSize);
329          checkArgument(spec.maximumWeight == null,
330              "maximum weight was already set to ", spec.maximumWeight);
331          spec.maximumSize = value;
332        }
333      }
334    
335      /** Parse maximumWeight */
336      static class MaximumWeightParser extends LongParser {
337        
338        @Override
339        protected void parseLong(CacheBuilderSpec spec, long value) {
340          checkArgument(spec.maximumWeight == null,
341              "maximum weight was already set to ", spec.maximumWeight);
342          checkArgument(spec.maximumSize == null,
343              "maximum size was already set to ", spec.maximumSize);
344          spec.maximumWeight = value;
345        }
346      }
347    
348      /** Parse concurrencyLevel */
349      static class ConcurrencyLevelParser extends IntegerParser {
350        
351        @Override
352        protected void parseInteger(CacheBuilderSpec spec, int value) {
353          checkArgument(spec.concurrencyLevel == null,
354              "concurrency level was already set to ", spec.concurrencyLevel);
355          spec.concurrencyLevel = value;
356        }
357      }
358    
359      /** Parse weakKeys */
360      static class KeyStrengthParser implements ValueParser {
361        private final Strength strength;
362    
363        public KeyStrengthParser(Strength strength) {
364          this.strength = strength;
365        }
366    
367        public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
368          checkArgument(value == null, "key %s does not take values", key);
369          checkArgument(spec.keyStrength == null, "%s was already set to %s", key, spec.keyStrength);
370          spec.keyStrength = strength;
371        }
372      }
373    
374      /** Parse weakValues and softValues */
375      static class ValueStrengthParser implements ValueParser {
376        private final Strength strength;
377    
378        public ValueStrengthParser(Strength strength) {
379          this.strength = strength;
380        }
381    
382        public void parse(CacheBuilderSpec spec, String key, @Nullable String value) {
383          checkArgument(value == null, "key %s does not take values", key);
384          checkArgument(spec.valueStrength == null,
385            "%s was already set to %s", key, spec.valueStrength);
386    
387          spec.valueStrength = strength;
388        }
389      }
390    
391      /** Base class for parsing times with durations */
392      abstract static class DurationParser implements ValueParser {
393        protected abstract void parseDuration(
394            CacheBuilderSpec spec,
395            long duration,
396            TimeUnit unit);
397    
398        public void parse(CacheBuilderSpec spec, String key, String value) {
399          checkArgument(value != null && value.length() > 0, "value of key %s omitted", key);
400          try {
401            char lastChar = value.charAt(value.length() - 1);
402            TimeUnit timeUnit;
403            long multiplier = 1;
404            switch (lastChar) {
405              case 'd':
406                multiplier *= 24;
407              case 'h':
408                multiplier *= 60;
409              case 'm':
410                multiplier *= 60;
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 = multiplier * 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        
432        @Override
433        protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
434          checkArgument(spec.accessExpirationTimeUnit == null, "expireAfterAccess already set");
435          spec.accessExpirationDuration = duration;
436          spec.accessExpirationTimeUnit = unit;
437        }
438      }
439    
440      /** Parse expireAfterWrite */
441      static class WriteDurationParser extends DurationParser {
442        
443        @Override
444        protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
445          checkArgument(spec.writeExpirationTimeUnit == null, "expireAfterWrite already set");
446          spec.writeExpirationDuration = duration;
447          spec.writeExpirationTimeUnit = unit;
448        }
449      }
450    
451      /** Parse refreshAfterWrite */
452      static class RefreshDurationParser extends DurationParser {
453        
454        @Override
455        protected void parseDuration(CacheBuilderSpec spec, long duration, TimeUnit unit) {
456          checkArgument(spec.refreshTimeUnit == null, "refreshAfterWrite already set");
457          spec.refreshDuration = duration;
458          spec.refreshTimeUnit = unit;
459        }
460      }
461    }