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.StringUtils.*;
020import static org.apache.juneau.commons.utils.Utils.*;
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 */
031public class Version implements Comparable<Version> {
032
033   /**
034    * Static creator.
035    *
036    * @param value
037    *    A string of the form <js>"#.#..."</js> where there can be any number of parts.
038    *    <br>Valid values:
039    *    <ul>
040    *       <li><js>"1.2"</js>
041    *       <li><js>"1.2.3"</js>
042    *       <li><js>"0.1"</js>
043    *       <li><js>".1"</js>
044    *    </ul>
045    *    <br>Can be <jk>null</jk>.
046    * @return A new header bean, or <jk>null</jk> if the value is <jk>null</jk>.
047    */
048   public static Version of(String value) {
049      if (value == null)
050         return null;
051      return new Version(value);
052   }
053
054   private int[] parts;
055
056   /**
057    * Constructor
058    *
059    * @param value
060    *    A string of the form <js>"#.#..."</js> where there can be any number of parts.
061    *    <br>Valid values:
062    *    <ul>
063    *       <li><js>"1.2"</js>
064    *       <li><js>"1.2.3"</js>
065    *       <li><js>"0.1"</js>
066    *       <li><js>".1"</js>
067    *    </ul>
068    *    Any parts that are not numeric are interpreted as {@link Integer#MAX_VALUE}
069    */
070   public Version(String value) {
071      if (isEmpty(value))
072         value = "0";
073      String[] sParts = splita(value, '.');
074      parts = new int[sParts.length];
075      for (var i = 0; i < sParts.length; i++) {
076         try {
077            parts[i] = sParts[i].isEmpty() ? 0 : Integer.parseInt(sParts[i]);
078         } catch (@SuppressWarnings("unused") NumberFormatException e) {
079            parts[i] = Integer.MAX_VALUE;
080         }
081      }
082   }
083
084   @Override
085   public int compareTo(Version v) {
086      for (int i = 0; i < Math.min(parts.length, v.parts.length); i++) {
087         var c = parts[i] - v.parts[i];
088         if (c != 0)
089            return c;
090      }
091      return parts.length - v.parts.length;
092   }
093
094   @Override /* Overridden from Object */
095   public boolean equals(Object o) {
096      return o instanceof Version o2 && eq(this, o2, (x, y) -> x.equals(y));
097   }
098
099   /**
100    * Returns <jk>true</jk> if the specified version is equal to this version.
101    *
102    * <h5 class='section'>Example:</h5>
103    * <p class='bjava'>
104    *    <jsm>assertTrue</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).isEqualsTo(Version.<jsm>of</jsm>(<js>"1.2.3"</js>)));
105    *    <jsm>assertTrue</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).isEqualsTo(Version.<jsm>of</jsm>(<js>"1.2"</js>)));
106    * </p>
107    *
108    * @param v The version to compare to.
109    * @return <jk>true</jk> if the specified version is equal to this version.
110    */
111   public boolean equals(Version v) {
112      for (int i = 0; i < Math.min(parts.length, v.parts.length); i++)
113         if (v.parts[i] - parts[i] != 0)
114            return false;
115      return true;
116   }
117
118   /**
119    * Returns the maintenance version part (i.e. part at index 2).
120    *
121    * @return The version part, never <jk>null</jk>.
122    */
123   public Optional<Integer> getMaintenance() { return getPart(2); }
124
125   /**
126    * Returns the major version part (i.e. part at index 0).
127    *
128    * @return The version part, never <jk>null</jk>.
129    */
130   public Optional<Integer> getMajor() { return getPart(0); }
131
132   /**
133    * Returns the minor version part (i.e. part at index 1).
134    *
135    * @return The version part, never <jk>null</jk>.
136    */
137   public Optional<Integer> getMinor() { return getPart(1); }
138
139   /**
140    * Returns the version part at the specified zero-indexed value.
141    *
142    * @param index The index of the version part.
143    * @return The version part, never <jk>null</jk>.
144    */
145   public Optional<Integer> getPart(int index) {
146      if (index < 0 || parts.length <= index)
147         return opte();
148      return opt(parts[index]);
149   }
150
151   @Override /* Overridden from Object */
152   public int hashCode() {
153      return Arrays.hashCode(parts);
154   }
155
156   /**
157    * Returns <jk>true</jk> if the specified version is at least this version.
158    *
159    * <h5 class='section'>Example:</h5>
160    * <p class='bjava'>
161    *    <jsm>assertTrue</jsm>(Version.<jsm>of</jsm>(<js>"1.2"</js>).isAtLeast(Version.<jsm>of</jsm>(<js>"1"</js>)));
162    *    <jsm>assertFalse</jsm>(Version.<jsm>of</jsm>(<js>"1.2"</js>).isAtLeast(Version.<jsm>of</jsm>(<js>"2"</js>)));
163    *    <jsm>assertTrue</jsm>(Version.<jsm>of</jsm>(<js>"1.2"</js>).isAtLeast(Version.<jsm>of</jsm>(<js>"1.2.3"</js>)));
164    *    <jsm>assertFalse</jsm>(Version.<jsm>of</jsm>(<js>"1.2.0"</js>).isAtLeast(Version.<jsm>of</jsm>(<js>"1.2.3"</js>)));
165    * </p>
166    *
167    * @param v The version to compare to.
168    * @return <jk>true</jk> if the specified version is at least this version.
169    */
170   public boolean isAtLeast(Version v) {
171      return isAtLeast(v, false);
172   }
173
174   /**
175    * Returns <jk>true</jk> if the specified version is at least this version.
176    *
177    * <h5 class='section'>Example:</h5>
178    * <p class='bjava'>
179    *    <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>)));
180    *    <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>)));
181    * </p>
182    *
183    * @param v The version to compare to.
184    * @param exclusive Match down-to-version but not including.
185    * @return <jk>true</jk> if the specified version is at least this version.
186    */
187   public boolean isAtLeast(Version v, boolean exclusive) {
188      for (int i = 0; i < Math.min(parts.length, v.parts.length); i++) {
189         var c = v.parts[i] - parts[i];
190         if (c > 0)
191            return false;
192         else if (c < 0)
193            return true;
194      }
195      for (var i = parts.length; i < v.parts.length; i++)
196         if (v.parts[i] != 0)
197            return false;
198      // If this version has more parts than v, check if any extra parts are non-zero
199      // If they are, then this > v, so return true (this is greater than v)
200      for (var i = v.parts.length; i < parts.length; i++)
201         if (parts[i] > 0)
202            return true;  // this > v, so always return true
203      return ! exclusive;
204   }
205
206   /**
207    * Returns <jk>true</jk> if the specified version is at most this version.
208    *
209    * <h5 class='section'>Example:</h5>
210    * <p class='bjava'>
211    *    <jsm>assertFalse</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).isAtMost(Version.<jsm>of</jsm>(<js>"1"</js>)));
212    *    <jsm>assertTrue</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).isAtMost(Version.<jsm>of</jsm>(<js>"2"</js>)));
213    *    <jsm>assertTrue</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).isAtMost(Version.<jsm>of</jsm>(<js>"1.2"</js>)));
214    *    <jsm>assertFalse</jsm>(Version.<jsm>of</jsm>(<js>"1.2.3"</js>).isAtMost(Version.<jsm>of</jsm>(<js>"1.2.0"</js>)));
215    * </p>
216    *
217    * @param v The version to compare to.
218    * @return <jk>true</jk> if the specified version is at most this version.
219    */
220   public boolean isAtMost(Version v) {
221      return isAtMost(v, false);
222   }
223
224   /**
225    * Returns <jk>true</jk> if the specified version is at most this version.
226    *
227    * <h5 class='section'>Example:</h5>
228    * <p class='bjava'>
229    *    <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>)));
230    *    <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>)));
231    * </p>
232    *
233    * @param v The version to compare to.
234    * @param exclusive Match up-to-version but not including.
235    * @return <jk>true</jk> if the specified version is at most this version.
236    */
237   public boolean isAtMost(Version v, boolean exclusive) {
238      for (int i = 0; i < Math.min(parts.length, v.parts.length); i++) {
239         var c = parts[i] - v.parts[i];
240         if (c > 0)
241            return false;
242         else if (c < 0)
243            return true;
244      }
245      for (var i = parts.length; i < v.parts.length; i++)
246         if (v.parts[i] > 0)
247            return true;
248      return ! exclusive;
249   }
250
251   @Override /* Overridden from Object */
252   public String toString() {
253      return join(parts, '.');
254   }
255}