001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.commons.lang;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021
022import java.util.*;
023
024/**
025 * Represents a version string such as <js>"1.2"</js> or <js>"1.2.3"</js>
026 *
027 * <p>
028 * Used to compare version numbers.
029 *
030 * <h5 class='section'>Equality vs. matching:</h5>
031 * <p>
032 * {@link #equals(Object)}, {@link #hashCode()} and {@link #compareTo(Version)} are <b>strict</b> and
033 * mutually consistent &mdash; two versions are only equal when their full part sequences are identical
034 * (e.g. <js>"1.2"</js> is <b>not</b> equal to <js>"1.2.3"</js> or <js>"1.2.0"</js>).  For the looser
035 * prefix/compatibility comparison (where trailing parts are ignored), use {@link #matches(Version)}.
036 */
037public class Version implements Comparable<Version> {
038
039   /**
040    * Static creator.
041    *
042    * @param value
043    *    A string of the form <js>"#.#..."</js> where there can be any number of parts.
044    *    <br>Valid values:
045    *    <ul>
046    *       <li><js>"1.2"</js>
047    *       <li><js>"1.2.3"</js>
048    *       <li><js>"0.1"</js>
049    *       <li><js>".1"</js>
050    *    </ul>
051    *    <br>Can be <jk>null</jk>.
052    * @return A new header bean, or <jk>null</jk> if the value is <jk>null</jk>.
053    */
054   public static Version of(String value) {
055      if (value == null)
056         return null;
057      return new Version(value);
058   }
059
060   private final int[] parts;
061
062   /**
063    * Constructor
064    *
065    * @param value
066    *    A string of the form <js>"#.#..."</js> where there can be any number of parts.
067    *    <br>Valid values:
068    *    <ul>
069    *       <li><js>"1.2"</js>
070    *       <li><js>"1.2.3"</js>
071    *       <li><js>"0.1"</js>
072    *       <li><js>".1"</js>
073    *    </ul>
074    *    Any parts that are not numeric are interpreted as {@link Integer#MAX_VALUE}
075    *    <br>Can be <jk>null</jk> or empty (treated as version <js>"0"</js>).
076    */
077   public Version(String value) {
078      if (isEmpty(value))
079         value = "0";
080      String[] sParts = splita(value, '.');
081      parts = new int[sParts.length];
082      for (var i = 0; i < sParts.length; i++) {
083         try {
084            parts[i] = sParts[i].isEmpty() ? 0 : Integer.parseInt(sParts[i]);
085         } catch (@SuppressWarnings("unused") NumberFormatException e) {
086            parts[i] = Integer.MAX_VALUE;
087         }
088      }
089   }
090
091   @Override
092   public int compareTo(Version v) {
093      for (int i = 0; i < Math.min(parts.length, v.parts.length); i++) {
094         var c = parts[i] - v.parts[i];
095         if (c != 0)
096            return c;
097      }
098      return parts.length - v.parts.length;
099   }
100
101   @Override /* Overridden from Object */
102   public boolean equals(Object o) {
103      return o instanceof Version o2 && Arrays.equals(parts, o2.parts);
104   }
105
106   /**
107    * Returns the maintenance version part (i.e. part at index 2).
108    *
109    * @return The version part, never <jk>null</jk>.
110    */
111   public Optional<Integer> getMaintenance() { return getPart(2); }
112
113   /**
114    * Returns the major version part (i.e. part at index 0).
115    *
116    * @return The version part, never <jk>null</jk>.
117    */
118   public Optional<Integer> getMajor() { return getPart(0); }
119
120   /**
121    * Returns the minor version part (i.e. part at index 1).
122    *
123    * @return The version part, never <jk>null</jk>.
124    */
125   public Optional<Integer> getMinor() { return getPart(1); }
126
127   /**
128    * Returns the version part at the specified zero-indexed value.
129    *
130    * @param index The index of the version part.
131    * @return The version part, never <jk>null</jk>.
132    */
133   public Optional<Integer> getPart(int index) {
134      if (index < 0 || parts.length <= index)
135         return oe();
136      return o(parts[index]);
137   }
138
139   @Override /* Overridden from Object */
140   public int hashCode() {
141      return Arrays.hashCode(parts);
142   }
143
144   /**
145    * Returns <jk>true</jk> if the specified version is at least this version.
146    *
147    * <h5 class='section'>Example:</h5>
148    * <p class='bjava'>
149    *    <jsm>assertTrue</jsm>(Version.<jsm>of</jsm>(<js>"1.2"</js>).isAtLeast(Version.<jsm>of</jsm>(<js>"1"</js>)));
150    *    <jsm>assertFalse</jsm>(Version.<jsm>of</jsm>(<js>"1.2"</js>).isAtLeast(Version.<jsm>of</jsm>(<js>"2"</js>)));
151    *    <jsm>assertTrue</jsm>(Version.<jsm>of</jsm>(<js>"1.2"</js>).isAtLeast(Version.<jsm>of</jsm>(<js>"1.2.3"</js>)));
152    *    <jsm>assertFalse</jsm>(Version.<jsm>of</jsm>(<js>"1.2.0"</js>).isAtLeast(Version.<jsm>of</jsm>(<js>"1.2.3"</js>)));
153    * </p>
154    *
155    * @param v The version to compare to.  Must not be <jk>null</jk>.
156    * @return <jk>true</jk> if the specified version is at least this version.
157    */
158   public boolean isAtLeast(Version v) {
159      return isAtLeast(v, false);
160   }
161
162   /**
163    * Returns <jk>true</jk> if the specified version is at least this version.
164    *
165    * <h5 class='section'>Example:</h5>
166    * <p class='bjava'>
167    *    <jsm>assertTrue</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).isAtLeast(Version.<jsm>of</jsm>(<js>"1.2.3"</js>, <jk>false</jk>)));
168    *    <jsm>assertFalse</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).isAtLeast(Version.<jsm>of</jsm>(<js>"1.2.3"</js>, <jk>true</jk>)));
169    * </p>
170    *
171    * @param v The version to compare to.  Must not be <jk>null</jk>.
172    * @param exclusive Match down-to-version but not including.
173    * @return <jk>true</jk> if the specified version is at least this version.
174    */
175   public boolean isAtLeast(Version v, boolean exclusive) {
176      for (int i = 0; i < Math.min(parts.length, v.parts.length); i++) {
177         var c = v.parts[i] - parts[i];
178         if (c > 0)
179            return false;
180         else if (c < 0)
181            return true;
182      }
183      for (var i = parts.length; i < v.parts.length; i++)
184         if (v.parts[i] != 0)
185            return false;
186      // If this version has more parts than v, check if any extra parts are non-zero
187      // If they are, then this > v, so return true (this is greater than v)
188      for (var i = v.parts.length; i < parts.length; i++)
189         if (parts[i] > 0)
190            return true;  // this > v, so always return true
191      return ! exclusive;
192   }
193
194   /**
195    * Returns <jk>true</jk> if the specified version is at most this version.
196    *
197    * <h5 class='section'>Example:</h5>
198    * <p class='bjava'>
199    *    <jsm>assertFalse</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).isAtMost(Version.<jsm>of</jsm>(<js>"1"</js>)));
200    *    <jsm>assertTrue</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).isAtMost(Version.<jsm>of</jsm>(<js>"2"</js>)));
201    *    <jsm>assertTrue</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).isAtMost(Version.<jsm>of</jsm>(<js>"1.2"</js>)));
202    *    <jsm>assertFalse</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).isAtMost(Version.<jsm>of</jsm>(<js>"1.2.0"</js>)));
203    * </p>
204    *
205    * @param v The version to compare to.  Must not be <jk>null</jk>.
206    * @return <jk>true</jk> if the specified version is at most this version.
207    */
208   public boolean isAtMost(Version v) {
209      return isAtMost(v, false);
210   }
211
212   /**
213    * Returns <jk>true</jk> if the specified version is at most this version.
214    *
215    * <h5 class='section'>Example:</h5>
216    * <p class='bjava'>
217    *    <jsm>assertTrue</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).isAtMost(Version.<jsm>of</jsm>(<js>"1.2.3"</js>, <jk>false</jk>)));
218    *    <jsm>assertFalse</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).isAtMost(Version.<jsm>of</jsm>(<js>"1.2.3"</js>, <jk>true</jk>)));
219    * </p>
220    *
221    * @param v The version to compare to.  Must not be <jk>null</jk>.
222    * @param exclusive Match up-to-version but not including.
223    * @return <jk>true</jk> if the specified version is at most this version.
224    */
225   public boolean isAtMost(Version v, boolean exclusive) {
226      for (int i = 0; i < Math.min(parts.length, v.parts.length); i++) {
227         var c = parts[i] - v.parts[i];
228         if (c > 0)
229            return false;
230         else if (c < 0)
231            return true;
232      }
233      for (var i = parts.length; i < v.parts.length; i++)
234         if (v.parts[i] > 0)
235            return true;
236      return ! exclusive;
237   }
238
239   /**
240    * Returns <jk>true</jk> if this version matches the specified version, comparing only the parts they have in
241    * common (i.e. one version is a prefix of the other).
242    *
243    * <p>
244    * This is a looser comparison than {@link #equals(Object)}, which requires the full version part sequences to be
245    * identical.  Two versions <i>match</i> when every part up to the length of the shorter version is identical; any
246    * trailing parts on the longer version are ignored.  Unlike {@link #equals(Object)}, this relation is <b>not</b>
247    * consistent with {@link #hashCode()} and must never be used as the basis for hash- or tree-based collection
248    * membership.
249    *
250    * <h5 class='section'>Example:</h5>
251    * <p class='bjava'>
252    *    <jsm>assertTrue</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).matches(Version.<jsm>of</jsm>(<js>"1.2.3"</js>)));
253    *    <jsm>assertTrue</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).matches(Version.<jsm>of</jsm>(<js>"1.2"</js>)));
254    *    <jsm>assertTrue</jsm>(Version.<jsm>of</jsm>(<js>"1.2"</js>).matches(Version.<jsm>of</jsm>(<js>"1.2.3"</js>)));
255    *    <jsm>assertFalse</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).matches(Version.<jsm>of</jsm>(<js>"1.2.4"</js>)));
256    * </p>
257    *
258    * @param v The version to compare to.  Must not be <jk>null</jk>.
259    * @return <jk>true</jk> if this version matches the specified version on their common parts.
260    */
261   public boolean matches(Version v) {
262      for (int i = 0; i < Math.min(parts.length, v.parts.length); i++)
263         if (v.parts[i] - parts[i] != 0)
264            return false;
265      return true;
266   }
267
268   @Override /* Overridden from Object */
269   public String toString() {
270      return join(parts, '.');
271   }
272}