001/*
002 * SonarQube
003 * Copyright (C) 2009-2017 SonarSource SA
004 * mailto:info AT sonarsource DOT com
005 *
006 * This program is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019 */
020package org.sonar.api.utils;
021
022import com.google.common.base.Splitter;
023import java.util.List;
024import javax.annotation.concurrent.Immutable;
025
026import static java.lang.Integer.parseInt;
027import static java.lang.Long.parseLong;
028import static java.util.Objects.requireNonNull;
029import static org.apache.commons.lang.StringUtils.substringAfter;
030import static org.apache.commons.lang.StringUtils.substringBefore;
031import static org.apache.commons.lang.StringUtils.trimToEmpty;
032
033/**
034 * Version composed of maximum four fields (major, minor, patch and build ID numbers) and optionally a qualifier.
035 * <p>
036 * Examples: 1.0, 1.0.0, 1.2.3, 1.2-beta1, 1.2.1-beta-1, 1.2.3.4567
037 * <p>
038 * <h3>IMPORTANT NOTE</h3>
039 * Qualifier is ignored when comparing objects (methods {@link #equals(Object)}, {@link #hashCode()}
040 * and {@link #compareTo(Version)}).
041 * <p>
042 * <pre>
043 *   assertThat(Version.parse("1.2")).isEqualTo(Version.parse("1.2-beta1"));
044 *   assertThat(Version.parse("1.2").compareTo(Version.parse("1.2-beta1"))).isZero();
045 * </pre>
046 *
047 * @since 5.5
048 */
049@Immutable
050public class Version implements Comparable<Version> {
051
052  private static final long DEFAULT_BUILD_NUMBER = 0L;
053  private static final int DEFAULT_PATCH = 0;
054  private static final String DEFAULT_QUALIFIER = "";
055  private static final String QUALIFIER_SEPARATOR = "-";
056  private static final char SEQUENCE_SEPARATOR = '.';
057  private static final Splitter SEQUENCE_SPLITTER = Splitter.on(SEQUENCE_SEPARATOR);
058
059  private final int major;
060  private final int minor;
061  private final int patch;
062  private final long buildNumber;
063  private final String qualifier;
064
065  private Version(int major, int minor, int patch, long buildNumber, String qualifier) {
066    this.major = major;
067    this.minor = minor;
068    this.patch = patch;
069    this.buildNumber = buildNumber;
070    this.qualifier = requireNonNull(qualifier, "Version qualifier must not be null");
071  }
072
073  public int major() {
074    return major;
075  }
076
077  public int minor() {
078    return minor;
079  }
080
081  public int patch() {
082    return patch;
083  }
084
085  /**
086   * Build number if the fourth field, for example {@code 12345} for "6.3.0.12345".
087   * If absent, then value is zero.
088   * @since 6.3
089   */
090  public long buildNumber() {
091    return buildNumber;
092  }
093
094  /**
095   * @return non-null suffix. Empty if absent, else the suffix without the first character "-"
096   */
097  public String qualifier() {
098    return qualifier;
099  }
100
101  /**
102   * Convert a {@link String} to a Version. Supported formats:
103   * <ul>
104   *   <li>1</li>
105   *   <li>1.2</li>
106   *   <li>1.2.3</li>
107   *   <li>1-beta-1</li>
108   *   <li>1.2-beta-1</li>
109   *   <li>1.2.3-beta-1</li>
110   *   <li>1.2.3.4567</li>
111   *   <li>1.2.3.4567-beta-1</li>
112   * </ul>
113   * Note that the optional qualifier is the part after the first "-".
114   *
115   * @throws IllegalArgumentException if parameter is badly formatted, for example
116   * if it defines 5 integer-sequences.
117   */
118  public static Version parse(String text) {
119    String s = trimToEmpty(text);
120    String qualifier = substringAfter(s, QUALIFIER_SEPARATOR);
121    if (!qualifier.isEmpty()) {
122      s = substringBefore(s, QUALIFIER_SEPARATOR);
123    }
124    List<String> fields = SEQUENCE_SPLITTER.splitToList(s);
125    int major = 0;
126    int minor = 0;
127    int patch = DEFAULT_PATCH;
128    long buildNumber = DEFAULT_BUILD_NUMBER;
129    int size = fields.size();
130    if (size > 0) {
131      major = parseFieldAsInt(fields.get(0));
132      if (size > 1) {
133        minor = parseFieldAsInt(fields.get(1));
134        if (size > 2) {
135          patch = parseFieldAsInt(fields.get(2));
136          if (size > 3) {
137            buildNumber = parseFieldAsLong(fields.get(3));
138            if (size > 4) {
139              throw new IllegalArgumentException("Maximum 4 fields are accepted: " + text);
140            }
141          }
142        }
143      }
144    }
145    return new Version(major, minor, patch, buildNumber, qualifier);
146  }
147
148  public static Version create(int major, int minor) {
149    return new Version(major, minor, DEFAULT_PATCH, DEFAULT_BUILD_NUMBER, DEFAULT_QUALIFIER);
150  }
151
152  public static Version create(int major, int minor, int patch) {
153    return new Version(major, minor, patch, DEFAULT_BUILD_NUMBER, DEFAULT_QUALIFIER);
154  }
155
156  /**
157   * @deprecated in 6.3 to avoid ambiguity with build number (see {@link #buildNumber()}
158   */
159  @Deprecated
160  public static Version create(int major, int minor, int patch, String qualifier) {
161    return new Version(major, minor, patch, DEFAULT_BUILD_NUMBER, qualifier);
162  }
163
164  private static int parseFieldAsInt(String field) {
165    if (field.isEmpty()) {
166      return 0;
167    }
168    return parseInt(field);
169  }
170
171  private static long parseFieldAsLong(String field) {
172    if (field.isEmpty()) {
173      return 0L;
174    }
175    return parseLong(field);
176  }
177
178  public boolean isGreaterThanOrEqual(Version than) {
179    return this.compareTo(than) >= 0;
180  }
181
182  @Override
183  public boolean equals(Object o) {
184    if (this == o) {
185      return true;
186    }
187    if (o == null || getClass() != o.getClass()) {
188      return false;
189    }
190    Version version = (Version) o;
191    if (major != version.major) {
192      return false;
193    }
194    if (minor != version.minor) {
195      return false;
196    }
197    if (patch != version.patch) {
198      return false;
199    }
200    return buildNumber == version.buildNumber;
201  }
202
203  @Override
204  public int hashCode() {
205    int result = major;
206    result = 31 * result + minor;
207    result = 31 * result + patch;
208    result = 31 * result + (int) (buildNumber ^ (buildNumber >>> 32));
209    return result;
210  }
211
212  @Override
213  public int compareTo(Version other) {
214    int c = major - other.major;
215    if (c == 0) {
216      c = minor - other.minor;
217      if (c == 0) {
218        c = patch - other.patch;
219        if (c == 0) {
220          long diff = buildNumber - other.buildNumber;
221          c = (diff > 0) ? 1 : ((diff < 0) ? -1 : 0);
222        }
223      }
224    }
225    return c;
226  }
227
228  @Override
229  public String toString() {
230    StringBuilder sb = new StringBuilder();
231    sb.append(major).append(SEQUENCE_SEPARATOR).append(minor);
232    if (patch > 0 || buildNumber > 0) {
233      sb.append(SEQUENCE_SEPARATOR).append(patch);
234      if (buildNumber > 0) {
235        sb.append(SEQUENCE_SEPARATOR).append(buildNumber);
236      }
237    }
238    if (!qualifier.isEmpty()) {
239      sb.append(QUALIFIER_SEPARATOR).append(qualifier);
240    }
241    return sb.toString();
242  }
243}