001/*
002 * SonarQube
003 * Copyright (C) 2009-2016 SonarSource SA
004 * mailto:contact 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.util.Objects.requireNonNull;
028import static org.apache.commons.lang.StringUtils.substringAfter;
029import static org.apache.commons.lang.StringUtils.substringBefore;
030import static org.apache.commons.lang.StringUtils.trimToEmpty;
031
032/**
033 * Version composed of 3 integer-sequences (major, minor and patch fields) and optionally a qualifier.
034 * <p>
035 * Examples: 1.0, 1.0.0, 1.2.3, 1.2-beta1, 1.2.1-beta-1
036 * <p>
037 * <h3>IMPORTANT NOTE</h3>
038 * Qualifier is ignored when comparing objects (methods {@link #equals(Object)}, {@link #hashCode()}
039 * and {@link #compareTo(Version)}).
040 * <p>
041 * <pre>
042 *   assertThat(Version.parse("1.2")).isEqualTo(Version.parse("1.2-beta1"));
043 *   assertThat(Version.parse("1.2").compareTo(Version.parse("1.2-beta1"))).isZero();
044 * </pre>
045 *
046 * @since 5.5
047 */
048@Immutable
049public class Version implements Comparable<Version> {
050
051  private static final String QUALIFIER_SEPARATOR = "-";
052  private static final char SEQUENCE_SEPARATOR = '.';
053  private static final Splitter SEQUENCE_SPLITTER = Splitter.on(SEQUENCE_SEPARATOR);
054
055  private final int major;
056  private final int minor;
057  private final int patch;
058  private final String qualifier;
059
060  private Version(int major, int minor, int patch, String qualifier) {
061    requireNonNull(qualifier, "Version qualifier must not be null");
062    this.major = major;
063    this.minor = minor;
064    this.patch = patch;
065    this.qualifier = qualifier;
066  }
067
068  public int major() {
069    return major;
070  }
071
072  public int minor() {
073    return minor;
074  }
075
076  public int patch() {
077    return patch;
078  }
079
080  /**
081   * @return non-null suffix. Empty if absent, else the suffix without the first character "-"
082   */
083  public String qualifier() {
084    return qualifier;
085  }
086
087  /**
088   * Convert a {@link String} to a Version. Supported formats:
089   * <ul>
090   *   <li>1</li>
091   *   <li>1.2</li>
092   *   <li>1.2.3</li>
093   *   <li>1-beta-1</li>
094   *   <li>1.2-beta-1</li>
095   *   <li>1.2.3-beta-1</li>
096   * </ul>
097   * Note that the optional qualifier is the part after the first "-".
098   *
099   * @throws IllegalArgumentException if parameter is badly formatted, for example
100   * if it defines 4 integer-sequences.
101   */
102  public static Version parse(String text) {
103    String s = trimToEmpty(text);
104    String qualifier = substringAfter(s, QUALIFIER_SEPARATOR);
105    if (!qualifier.isEmpty()) {
106      s = substringBefore(s, QUALIFIER_SEPARATOR);
107    }
108    List<String> split = SEQUENCE_SPLITTER.splitToList(s);
109    int major = 0;
110    int minor = 0;
111    int patch = 0;
112    int size = split.size();
113    if (size > 0) {
114      major = parseSequence(split.get(0));
115      if (size > 1) {
116        minor = parseSequence(split.get(1));
117        if (size > 2) {
118          patch = parseSequence(split.get(2));
119          if (size > 3) {
120            throw new IllegalArgumentException("Only 3 sequences are accepted");
121          }
122        }
123      }
124    }
125    return new Version(major, minor, patch, qualifier);
126  }
127
128  public static Version create(int major, int minor) {
129    return new Version(major, minor, 0, "");
130  }
131
132  public static Version create(int major, int minor, int patch) {
133    return new Version(major, minor, patch, "");
134  }
135
136  public static Version create(int major, int minor, int patch, String qualifier) {
137    return new Version(major, minor, patch, qualifier);
138  }
139
140  private static int parseSequence(String sequence) {
141    if (sequence.isEmpty()) {
142      return 0;
143    }
144    return parseInt(sequence);
145  }
146
147  public boolean isGreaterThanOrEqual(Version than) {
148    return this.compareTo(than) >= 0;
149  }
150
151  @Override
152  public boolean equals(Object o) {
153    if (this == o) {
154      return true;
155    }
156    if (!(o instanceof Version)) {
157      return false;
158    }
159    Version other = (Version) o;
160    return major == other.major && minor == other.minor && patch == other.patch;
161  }
162
163  @Override
164  public int hashCode() {
165    int result = major;
166    result = 31 * result + minor;
167    result = 31 * result + patch;
168    return result;
169  }
170
171  @Override
172  public int compareTo(Version other) {
173    int c = major - other.major;
174    if (c == 0) {
175      c = minor - other.minor;
176      if (c == 0) {
177        c = patch - other.patch;
178      }
179    }
180    return c;
181  }
182
183  @Override
184  public String toString() {
185    StringBuilder sb = new StringBuilder();
186    sb.append(major).append(SEQUENCE_SEPARATOR).append(minor);
187    if (patch > 0) {
188      sb.append(SEQUENCE_SEPARATOR).append(patch);
189    }
190    if (!qualifier.isEmpty()) {
191      sb.append(QUALIFIER_SEPARATOR).append(qualifier);
192    }
193    return sb.toString();
194  }
195}