View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.trinidad.context;
20  
21  import java.util.Arrays;
22  import java.util.regex.Pattern;
23  
24  import org.apache.myfaces.trinidad.logging.TrinidadLogger;
25  import org.apache.myfaces.trinidad.util.Range;
26  
27  /**
28   * Immutable Representation of a dot-separated version.
29   *
30   * This representation
31   * allows individual sections of the version to be wild-carded and allows
32   * for comparisons between Versions with different numbers of version
33   * subsections to be compared.  When comparing Versions, each version
34   * subsection is compared from left to right.  If one Version doesn't have
35   * a version subsection at the current index, the value of versionPadding
36   * is used for this comparison.  Version subsections with the wild-card value "*"
37   * are considered equal.  The value returned by compareTo() is the value of the
38   * first non-equal version subsection or zero if all subsections match.
39   *
40   * Due to the support for wild-cards, this class has a natural ordering
41   * that is inconsistent with equals.  For example,
42   * <code>Version("5", "*").compareTo(Version("5.0", "*") == 0</code>
43   * <code>Version("5", "*").equals(Version("5.0", "*") == false;</code>
44   * 
45   * The concrete versions produced by toMinimumVersion() and toMaximumVersion()
46   * do have consistent compareTo()/equals() behavior, as these versions are
47   * guaranteed to not contain wildcards.
48   * 
49   * @author Blake Sullivan
50   */
51  public final class Version implements Comparable<Version>
52  {
53  
54    /**
55     * A constant value holding the minimum value a version can have: 0.
56     */
57    public static final Version MIN_VERSION;
58    
59    /**
60     * A constant value holding a maximum upper bound for versions.
61     *
62     * In theory there is no upper limit to version string values, ie. version
63     * strings could be infinitely long.  However, in practice it can be
64     * helpful to have some way to identify a concrete maximum upper bound to
65     * a range of versions.  Version.MAX_VERSION specifies the Integer.MAX_VALUE
66     * version for this purpose.
67     */
68    public static final Version MAX_VERSION;
69    
70    /**
71     * A range of versions from MIN_VERSION to MAX_VERSION.
72     */
73    public static final Range<Version> ALL_VERSIONS;
74  
75    /**
76     * Creates a Version instance from the dot-separated Version String using null as the padding
77     * @param version The dot-separated version to represent
78     * @throws NullPointerException if the version is null
79     * @throws IllegalArgumentException if the version is an empty String
80     * @see #Version(String, String)
81     */
82    public Version(String version)
83    {
84      this(version, null);
85    }
86    
87    /**
88     * Creates a Version instance from the dot-separated Version String and the
89     * versionPadding.
90     * @param version The dot-separated version to represent
91     * @param versionPadding The value to return for sub-version sections
92     * requested beyond the sub-version sections present in the version String.
93     * If null or empty, no padding will be performed.
94     * @throws NullPointerException if version is null
95     * @throws IllegalArgumentException if version is the empty String
96     */
97    public Version(String version, String versionPadding)
98    {
99      _checkNonEmptyString(version, "version");
100     if (versionPadding == null)
101     {
102       versionPadding = "";
103     }
104     
105     // build the array of subversions
106     _versions = _DOT_SPLITTER.split(version, 0);
107     
108     // We also store away int representations of version strings, since
109     // this is necessary for more accurate comparison - ie. when comparing
110     // version segments, we want to compare int 9 vs 10, since comparing
111     // "9".compareTo("10") produces undesirable results.
112     _intVersions = _toIntVersions(_versions, version);
113 
114     _versionPadding = versionPadding;
115     
116     // since we're immutable, we might as well calculate this up front
117     // while we still have the String version around
118     _hashCode = version.hashCode() * 37 + versionPadding.hashCode();
119   }
120 
121   /**
122    * When comparing Versions, each version
123    * subsection is compared from left to right.  If one Version doesn't have
124    * a version subsection at the current index, the value of versionPadding
125    * is used for this comparison.  Version subsections with the wild-card value "*"
126    * care considered equal.  The value returned by compareTo() is the value of the
127    * first non-equal version subsection or zero if all subsections match.
128    * @param otherVersion The Version object to compare this Version Object with
129    * @return a negative integer, zero, or a positive integer as this object
130    *         is less than, equal to, or greater than the specified object.
131    */
132   public int compareTo(Version otherVersion)
133   {
134     int ourVersionCount = _versions.length;
135     int otherVersionCount = otherVersion._versions.length;
136     
137     int compareCount = (ourVersionCount > otherVersionCount)
138                          ? ourVersionCount
139                          : otherVersionCount;
140     
141     for (int versionIndex = 0; versionIndex < compareCount; versionIndex++)
142     {
143       int result = _compareVersions(otherVersion, versionIndex);
144 
145       // not equal, so return the result
146       if (result != 0)
147         return result;
148     }
149     
150     // equivalent
151     return 0;
152   }
153   
154   /**
155    * Converts this Version to an equivalent "minimum" instance.
156    * 
157    * Interior wildcard segements are replaced with "0".
158    * The trailing wildcard segment (if present) is dropped.
159    * Wildcard version padding is replaced with null padding.
160    * 
161    * If no wilcards are present, returns this Version instance.
162    */
163   public Version toMinimumVersion()
164   {
165     if (!_containsWildcard() && !_isWildcard(_versionPadding))
166     {
167       return this;
168     }
169     
170     return new Version(_toString("0", true));
171   }
172 
173   /**
174    * Converts this Version to an equivalent "maximum" instance.
175    * 
176    * Both wildcard segements and wilcard padding are replaced with
177    * Integer.MAX_VALUE.
178    * If no wilcards are present, returns this Version instance.
179    */
180   public Version toMaximumVersion()
181   {
182     if (!_containsWildcard() && !_isWildcard(_versionPadding))
183     {
184       return this;
185     }
186     
187     return new Version(_toString(_MAX_STRING, false), _MAX_STRING);
188   }
189 
190   @Override
191   public String toString()
192   {
193     return _toString(_WILDCARD, false);
194   }
195   
196   @Override
197   public boolean equals(Object o)
198   {
199     if (o == this)
200       return true;
201     else if (!(o instanceof Version))
202       return false;
203     else
204     {
205       Version otherVersion = (Version)o;
206       
207       // we are equal if all of version content and padding are equal
208       return _versionPadding.equals(otherVersion._versionPadding) &&
209              Arrays.equals(_versions, otherVersion._versions);
210     }
211   }
212   
213   @Override
214   public int hashCode()
215   {
216     // used cached version
217     return _hashCode;
218   }
219 
220   // Converts an array of String version segments to
221   // an array of int versions more suitable for use in
222   // comparisons.
223   // 
224   // Note that not all version segments can be converted
225   // to an int.  For example, the version could include
226   // non-numeric characters.  Also wildcard segments will 
227   // fail to convert.  We identify these segments by setting
228   // the int value to _NON_INT_VERSION.  We can then fall
229   // back on using String comparisions for these segments.
230   //
231   // The fullVersion is provided for error handling only.
232   private static int[] _toIntVersions(String[] versions, String fullVersion)
233   {
234     assert(versions != null);
235     int[] intVersions = new int[versions.length];
236     
237     for (int i = 0; i < versions.length; i++)
238     {
239       intVersions[i] = _toIntVersion(versions[i], fullVersion);
240     }
241     
242     return intVersions;
243   }
244   
245   // Converts a String version segment to the corresponding
246   // int.  Returns _NON_INT_VERSION if the String cannot be
247   // converted (eg. wildcard character).
248   //
249   // The fullVersion is provided for error handling only.
250   private static int _toIntVersion(String versionSegment, String fullVersion)
251   {
252     if (_DIGITS_PATTERN.matcher(versionSegment).matches())
253     {
254       try
255       {
256         return Integer.parseInt(versionSegment);
257       }
258       catch (NumberFormatException e)
259       {
260         // Since we already filtered out version strings
261         // that didn't match the _DIGITS_PATTERN, the only
262         // case where we should arrive here is if the version
263         // string overflows int.
264         _LOG.warning("UNEXPECTED_VERSION_VALUE", new Object[] { versionSegment, fullVersion });                
265       }
266     }
267     
268     return _NON_INT_VERSION;
269   }
270 
271   /**
272    * Compares the version segment at the specified index.
273    * 
274    * @param otherVersion the Version instance to which we are comparing.
275    * @param versionIndex the index of the version segment that we are testing
276    * @return < 0 if this Version's segment is < otherVersion's segment.  0 if 
277    *   equal.  Otherwise, > 1.
278    */
279   private int _compareVersions(Version otherVersion, int versionIndex)
280   {
281     if (_isIntComparable(otherVersion, versionIndex))
282     {
283       return _compareIntVersions(otherVersion, versionIndex);
284     }
285     
286     return _compareStringVersions(otherVersion, versionIndex);
287   }
288   
289   /**
290    * Tests whether an int comparison can be performed for a particular
291    * version segment.
292    * 
293    * @param otherVersion the Version instance to which we are comparing.
294    * @param versionIndex the index of the version segment that we are testing
295    * @return true if both this Version and otherVersion have an int value
296    *   for the specified version index.
297    */
298   private boolean _isIntComparable(Version otherVersion, int versionIndex)
299   {
300     int[] ourIntVersions = _intVersions;
301     int[] otherIntVersions = otherVersion._intVersions;
302     
303     return ((versionIndex < ourIntVersions.length)             &&
304             (versionIndex < otherIntVersions.length)           &&
305             (ourIntVersions[versionIndex] != _NON_INT_VERSION) &&
306             (otherIntVersions[versionIndex] != _NON_INT_VERSION));
307   }
308 
309   /**
310    * Compares the int version segments at the specified index.
311    * 
312    * @param otherVersion the Version instance to which we are comparing.
313    * @param versionIndex the index of the version segment that we are testing.
314    * @return < 0 if this Version's segment is < otherVersion's segment.  0 if 
315    *   equal.  Otherwise, > 1.
316    */
317   private int _compareIntVersions(Version otherVersion, int versionIndex)
318   {
319     assert(_isIntComparable(otherVersion, versionIndex));
320     int ourIntVersion = _intVersions[versionIndex];
321     int otherIntVersion = otherVersion._intVersions[versionIndex];
322     
323     return (ourIntVersion < otherIntVersion) ? -1 : (ourIntVersion > otherIntVersion ? 1 : 0);
324   }
325 
326   /**
327    * Compares the String version segment at the specified index.
328    * 
329    * @param otherVersion the Version instance to which we are comparing.
330    * @param versionIndex the index of the version segment that we are testing
331    * @return < 0 if this Version's segment is < otherVersion's segment.  0 if 
332    *   equal.  Otherwise, > 1.
333    */
334   private int _compareStringVersions(Version otherVersion, int versionIndex)
335   {
336     String ourSubVersion = _getSubVersion(versionIndex);
337     String otherSubVersion = otherVersion._getSubVersion(versionIndex);
338       
339     // treat "*" wildcard as equals
340     if (_isWildcard(ourSubVersion) || _isWildcard(otherSubVersion))
341     {
342       return 0;
343     }
344     
345     return ourSubVersion.compareTo(otherSubVersion);
346   }
347 
348   /**
349    * Returns the string representation of the this Version, replacing
350    * wildcards with the specified value.
351    *
352    * @param wildcardReplacement non-null String to substitute for wildcard
353    *   version segments.
354    * @param dropTrailingWildcard flag indicating whether trailing wildcards
355    *   should be dropped in the returned string.
356    */
357   private String _toString(
358     String  wildcardReplacement,
359     boolean dropTrailingWildcard
360     )
361   {
362     assert(wildcardReplacement != null);
363 
364     // rebuild the initial version string from the split array
365     StringBuilder versionBuilder = new StringBuilder();
366     int versionCount = _versions.length;
367     
368     for (int i = 0;;)
369     {
370       String subVersion = _versions[i];
371       if (_isWildcard(subVersion))
372       {
373         subVersion = wildcardReplacement;
374       }
375 
376       versionBuilder.append(subVersion);
377       
378       i++;
379       
380       if (i != versionCount)
381       {
382         if (dropTrailingWildcard && (i == versionCount - 1) && _isWildcard(_versions[i]))
383         {
384           break;
385         }
386 
387         versionBuilder.append('.');
388       }
389       else
390         break;
391     }
392         
393     return versionBuilder.toString();    
394   }
395 
396   /**
397    * Returns the contents of the sub-version section of the overall version,
398    * padding the result with the version padding if the version section
399    * index is greater than the number of actual version sections in this
400    * version
401    * @param versionIndex index of the "." version section from the left side
402    * of the version string.
403    * @return The content of the version section if available, otehrwise the
404    * versionPadding
405    */
406   private String _getSubVersion(int versionIndex)
407   {
408     if (versionIndex >= _versions.length)
409       return _versionPadding;
410     else
411       return _versions[versionIndex];
412   }
413 
414   
415   private void _checkNonEmptyString(String checkedString, String identifier)
416   {
417     if (checkedString == null)
418       throw new NullPointerException(identifier + " must be non-null");
419     
420     if (checkedString.length() == 0)
421       throw new IllegalArgumentException(identifier + " must be non-empty");
422   }
423   
424   // Tests wheter this version contains a wildcard segment
425   private boolean _containsWildcard()
426   {
427     for (int i = 0; i < _versions.length; i++)
428     {
429       if (_isWildcard(_versions[i]))
430       {
431         return true;
432       }
433     }
434     
435     return false;
436   }
437   
438   // Tests whether the specified version segment is the wildcard.
439   private static boolean _isWildcard(String subVersion)
440   {
441     return _WILDCARD.equals(subVersion);
442   }
443 
444   private final String[] _versions;
445   private final int[] _intVersions;
446   private final String _versionPadding;
447   private final int _hashCode;
448 
449   // Constant for the wildcard character  
450   private static final String _WILDCARD = "*";
451 
452   // cache the compiled splitter
453   private static final Pattern _DOT_SPLITTER = Pattern.compile("\\.");
454   
455   // A pattern for testing whether a version string is numeric.
456   private static final Pattern _DIGITS_PATTERN = Pattern.compile("\\d+");
457   
458   // Placeholder used by _intVersions[] for non-numeric/non-int segments.  Note
459   // that we can use -1 to mark non-int versions since the _DIGITS_PATTERN will
460   // filter out any negative values - ie. _intVersions[] will only contain
461   // non-negative version ints, plus _NON_INT_VERSION.
462   private static final int _NON_INT_VERSION = -1;
463   
464   private static final String _MAX_STRING = Integer.toString(Integer.MAX_VALUE);
465   
466   private static final TrinidadLogger _LOG =
467     TrinidadLogger.createTrinidadLogger(Version.class);
468 
469   static
470   {
471     MIN_VERSION = new Version("0");
472     MAX_VERSION = new Version(_MAX_STRING, _MAX_STRING);
473     ALL_VERSIONS = Range.of(MIN_VERSION, MAX_VERSION);
474   }
475 
476 }