001package jmri;
002
003import java.util.ResourceBundle;
004
005/**
006 * Defines a simple place to get the JMRI version string.
007 * <p>
008 * JMRI version strings are of the form x.y.z-m:
009 * <ul>
010 * <li>x, called the "major" number, is a small integer that increases with time
011 * <li>y, called the "minor" number, is a small integer that increases with time
012 * <li>z, called the "test" or "build" number, is a small integer increasing
013 * with time, perhaps followed by a couple of modifier characters. As a special
014 * case, this is omitted for Production Releases.
015 * <li>m, called the modifier, is a string that further describes the build. A
016 * common modifier is "plus" which denotes an unofficial build.
017 * </ul>
018 * Hence you expect to see JMRI versions called things like "4.7.2", "4.6",
019 * "4.7.3plus", "4.7.2-pjc", "4.7.2plus-pjc".
020 * <p>
021 * The version string shown by a JMRI program or used to label a download comes
022 * in two forms, depending on whether it was built by an "official" process or
023 * not, which in turn is determined by the "release.official" property:
024 * <dl>
025 * <dt>Official<dd>
026 * <ul>
027 * <li>If the revision number e.g. 123abc (git hash) is available in
028 * release.revision_id, then "4.1.1+R123abc". Note the "R".
029 * <li>Else "4.1.1+(date)", where the date comes from the release.build_date
030 * property.
031 * </ul>
032 * <dt>Unofficial<dd>
033 * Unofficial releases are marked by "plus" after the version number, and
034 * inclusion of the building user's ID.
035 * <ul>
036 * <li>If the revision number e.g. 123abc (git hash) is available in
037 * release.revision_id, then "4.1.1plus+(user)+(date)+R123abc". Note the "R".
038 * <li>Else "4.1.1+(user)+(date)", where the date comes from the
039 * release.build_date property.
040 * </ul>
041 * </dl>
042 * The release.revision_id, release.build_user and release.build_date properties
043 * are set at build time by Ant.
044 * <p>
045 * Generally, JMRI updates its version string in the code repository right
046 * <b>after</b> a release. Between formal release 1.2.3 and 1.2.4, the string
047 * will be 1.2.4plus.
048 * <hr>
049 * This file is part of JMRI.
050 * <p>
051 * JMRI is free software; you can redistribute it and/or modify it under the
052 * terms of version 2 of the GNU General Public License as published by the Free
053 * Software Foundation. See the "COPYING" file for a copy of this license.
054 * <p>
055 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
056 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
057 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
058 *
059 * @author Bob Jacobsen Copyright 1997-2022
060 */
061public class Version {
062
063    static final private ResourceBundle VERSION_BUNDLE = ResourceBundle.getBundle("jmri.Version"); // NOI18N
064
065    /**
066     * Major number changes with large incompatible changes in requirements or
067     * API.
068     */
069    static final public int major = Integer.parseInt(VERSION_BUNDLE.getString("release.major")); // NOI18N
070
071    /**
072     * Minor number changes with each production versionBundle. Odd is
073     * development, even is production.
074     */
075    static final public int minor = Integer.parseInt(VERSION_BUNDLE.getString("release.minor")); // NOI18N
076
077    /**
078     * Test number changes with individual releases, generally fastest for test
079     * releases. In production releases, if non-zero, indicates a bug fix only
080     * release.
081     */
082    static final public int test = Integer.parseInt(VERSION_BUNDLE.getString("release.build")); // NOI18N
083
084    /**
085     * The additional MODIFIER for the release. Used to indicate a parallel
086     * release of a feature that has not been accepted into main stream
087     * development.
088     */
089    static final public String MODIFIER = VERSION_BUNDLE.getString("release.modifier"); // NOI18N
090
091    /**
092     * Descriptor for non-official build. Included in {@link #name()}, but not
093     * in {@link #getCanonicalVersion()}.
094     */
095    static final public String NON_OFFICIAL = "plus"; // NOI18N
096
097    /**
098     * The user who built this versionBundle, as determined by the build
099     * machine.
100     */
101    static final public String buildUser = VERSION_BUNDLE.getString("release.build_user"); // NOI18N
102
103    /**
104     * The Git revision ID for this versionBundle (if known).
105     */
106    static final public String revisionId = VERSION_BUNDLE.getString("release.revision_id"); // NOI18N
107
108    /**
109     * The date/time of this build.
110     */
111    static final public String buildDate = VERSION_BUNDLE.getString("release.build_date"); // NOI18N
112
113    /**
114     * Has this build been created as a possible "official" versionBundle?
115     */
116    static final public boolean official = Boolean.parseBoolean(VERSION_BUNDLE.getString("release.official")); // NOI18N
117
118    /**
119     * Get the MODIFIER in the 1.2.3-MODIFIER version name. Non-official
120     * versions include {@value #NON_OFFICIAL} in the MODIFIER.
121     *
122     * @return the third term, possibly an empty String if {@link #test} is 0
123     */
124    public static String getModifier() {
125        StringBuilder modifier = new StringBuilder();
126        if (!official) {
127            modifier.append(NON_OFFICIAL);
128        }
129        if (!MODIFIER.isEmpty()) {
130            modifier.append("-").append(MODIFIER); // NOI18N
131        }
132        return modifier.toString().replace("--", "-");
133    }
134
135    /**
136     * Provide the current version string.
137     * <p>
138     * This string is built using various known build parameters, including the
139     * versionBundle.{major,minor,build} values, the MODIFIER, the Git revision
140     * ID (if known) and the official property
141     *
142     * @return The current version string
143     */
144    static public String name() {
145        String version = major + "." + minor;
146        if (test != 0) {
147            version = version + "." + test;
148        }
149        String addOn;
150        if (official) {
151            if ("unknown".equals(revisionId)) {
152                addOn = buildDate;
153            } else {
154                addOn = "R" + revisionId;
155            }
156        } else { // not official, so a development build that gets a user name
157            if ("unknown".equals(revisionId)) {
158                addOn = buildUser + "+" + buildDate;
159            } else {
160                addOn = buildUser + "+" + buildDate + "+R" + revisionId;
161            }
162        }
163        return version + getModifier() + "+" + addOn;
164    }
165
166    /**
167     * Tests that a string contains a canonical version string.
168     * <p>
169     * A canonical version string is a string in the form x.y.z[-a[-b[-...]]]
170     * where parts x, y, and z are integers and parts a, b, ... are free-form
171     * text and is different than the version string displayed using
172     * {@link #name()}. The canonical version string for a JMRI instance is
173     * available using {@link #getCanonicalVersion()}. The canonical version
174     * will not include official indicators or build metadata.
175     *
176     * @param version version string to check
177     * @return true if version is a canonical version string
178     */
179    static public boolean isCanonicalVersion(String version) {
180        String[] parts = version.split("\\+");
181        if (parts.length > 1) {
182            return false;
183        }
184        parts = version.split("-");
185        String[] versions = parts[0].split("\\.");
186        if (versions.length != 3) {
187            return false;
188        }
189        try {
190            for (String part : versions) {
191                if (Integer.parseInt(part) < 0) {
192                    return false;
193                }
194            }
195        } catch (NumberFormatException ex) {
196            return false;
197        }
198        return true;
199    }
200
201    /**
202     * Compares a canonical version string to the JMRI canonical version and
203     * returns an integer indicating if the string is less than, equal to, or
204     * greater than the JMRI canonical version.
205     *
206     * @param version version string to compare
207     * @return -1, 0, or 1 if version is less than, equal to, or greater than
208     *         JMRI canonical version
209     * @throws IllegalArgumentException if version is not a canonical version
210     *                                  string
211     * @see java.lang.Comparable#compareTo(java.lang.Object)
212     */
213    static public int compareCanonicalVersions(String version) throws IllegalArgumentException {
214        return compareCanonicalVersions(version, getCanonicalVersion());
215    }
216
217    /**
218     * Compares two canonical version strings and returns an integer indicating
219     * if the first string is less than, equal to, or greater than the second
220     * string. This comparison ignores modifiers.
221     *
222     * @param version1 a canonical version string
223     * @param version2 a canonical version string
224     * @return -1, 0, or 1 if version1 is less than, equal to, or greater than
225     *         version2
226     * @throws IllegalArgumentException if either version string is not a
227     *                                  canonical version string
228     * @see java.lang.Comparable#compareTo(java.lang.Object)
229     */
230    static public int compareCanonicalVersions(String version1, String version2) throws IllegalArgumentException {
231        int result = 0;
232        if (!isCanonicalVersion(version1)) {
233            throw new IllegalArgumentException("Parameter version1 (" + version1 + ") is not a canonical version string.");
234        }
235        if (!isCanonicalVersion(version2)) {
236            throw new IllegalArgumentException("Parameter version2 (" + version2 + ") is not a canonical version string.");
237        }
238        String[] p1 = version1.split("-");
239        String[] p2 = version2.split("-");
240        String[] v1 = p1[0].split("\\.");
241        String[] v2 = p2[0].split("\\.");
242        for (int i = 0; i < 3; i++) {
243            result = v1[i].compareTo(v2[i]);
244            if (result != 0) {
245                return result;
246            }
247        }
248        return result;
249    }
250
251    /**
252     * Return the version as major.minor.test-modifiers. The test value is
253     * always present. The {@value #NON_OFFICIAL} modifier is not present in the
254     * canonical version.
255     *
256     * @return the canonical version
257     */
258    static public String getCanonicalVersion() {
259        String version = major + "." + minor + "." + test;
260        String modifiers = getModifier().replace(NON_OFFICIAL, ""); // remove "ish"
261        if (!modifiers.isEmpty()) {
262            version = version + modifiers;
263        }
264        if (version.endsWith("-")) {
265            version = version.substring(0, version.length() - 2);
266        }
267        return version;
268    }
269
270    /**
271     * Return the application copyright as a String.
272     *
273     * @return the copyright
274     */
275    static public String getCopyright() {
276        return Bundle.getMessage("Copyright", VERSION_BUNDLE.getString("jmri.copyright.year"));
277    }
278
279    /**
280     * Standalone print of version string and exit.
281     *
282     * This is used in the build.xml to generate parts of the installer
283     * versionBundle file name, so take care in altering this code to make sure
284     * the ant recipes are also suitably modified.
285     *
286     * @param args command-line arguments
287     */
288    static public void main(String[] args) {
289        System.out.println(name());
290    }
291
292}