package org.jabref.logic.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* Represents the Application Version with the major and minor number, the full Version String and if it's a developer
* version
*/
public class Version {
public static final String JABREF_DOWNLOAD_URL = "https://downloads.jabref.org";
private static final Log LOGGER = LogFactory.getLog(Version.class);
private static final Version UNKNOWN_VERSION = new Version();
private final static Pattern VERSION_PATTERN = Pattern.compile("(?<major>\\d+)(\\.(?<minor>\\d+))?(\\.(?<patch>\\d+))?(?<stage>-alpha|-beta)?(?<dev>-?dev)?.*");
private static final String JABREF_GITHUB_RELEASES = "https://api.github.com/repos/JabRef/JabRef/releases";
private String fullVersion = BuildInfo.UNKNOWN_VERSION;
private int major = -1;
private int minor = -1;
private int patch = -1;
private DevelopmentStage developmentStage = DevelopmentStage.UNKNOWN;
private boolean isDevelopmentVersion;
/**
* Dummy constructor to create a local object (and {@link Version#UNKNOWN_VERSION})
*/
private Version() {
}
/**
* @param version must be in form of following pattern: {@code (\d+)(\.(\d+))?(\.(\d+))?(-alpha|-beta)?(-?dev)?}
* (e.g., 3.3; 3.4-dev)
* @return the parsed version or {@link Version#UNKNOWN_VERSION} if an error occurred
*/
public static Version parse(String version) {
if ((version == null) || "".equals(version) || version.equals(BuildInfo.UNKNOWN_VERSION)
|| "${version}".equals(version)) {
return UNKNOWN_VERSION;
}
Version parsedVersion = new Version();
parsedVersion.fullVersion = version;
Matcher matcher = VERSION_PATTERN.matcher(version);
if (matcher.find()) {
try {
parsedVersion.major = Integer.parseInt(matcher.group("major"));
String minorString = matcher.group("minor");
parsedVersion.minor = minorString == null ? 0 : Integer.parseInt(minorString);
String patchString = matcher.group("patch");
parsedVersion.patch = patchString == null ? 0 : Integer.parseInt(patchString);
String versionStageString = matcher.group("stage");
parsedVersion.developmentStage = versionStageString == null ? DevelopmentStage.STABLE : DevelopmentStage.parse(versionStageString);
parsedVersion.isDevelopmentVersion = matcher.group("dev") != null;
} catch (NumberFormatException e) {
LOGGER.warn("Invalid version string used: " + version, e);
return UNKNOWN_VERSION;
} catch (IllegalArgumentException e) {
LOGGER.warn("Invalid version pattern is used", e);
return UNKNOWN_VERSION;
}
} else {
LOGGER.warn("Version could not be recognized by the pattern");
return UNKNOWN_VERSION;
}
return parsedVersion;
}
/**
* Grabs all the available releases from the GitHub repository
*/
public static List<Version> getAllAvailableVersions() throws IOException {
URLConnection connection = new URL(JABREF_GITHUB_RELEASES).openConnection();
connection.setRequestProperty("Accept-Charset", "UTF-8");
try (BufferedReader rd = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
List<Version> versions = new ArrayList<>();
JSONArray objects = new JSONArray(rd.readLine());
for (int i = 0; i < objects.length(); i++) {
JSONObject jsonObject = objects.getJSONObject(i);
Version version = Version.parse(jsonObject.getString("tag_name").replaceFirst("v", ""));
versions.add(version);
}
return versions;
}
}
/**
* @return true if this version is newer than the passed one
*/
public boolean isNewerThan(Version otherVersion) {
Objects.requireNonNull(otherVersion);
if (Objects.equals(this, otherVersion)) {
return false;
} else if (this.getFullVersion().equals(BuildInfo.UNKNOWN_VERSION)) {
return false;
} else if (otherVersion.getFullVersion().equals(BuildInfo.UNKNOWN_VERSION)) {
return false;
}
// compare the majors
if (this.getMajor() > otherVersion.getMajor()) {
return true;
} else if (this.getMajor() == otherVersion.getMajor()) {
// if the majors are equal compare the minors
if (this.getMinor() > otherVersion.getMinor()) {
return true;
} else if (this.getMinor() == otherVersion.getMinor()) {
// if the minors are equal compare the patch numbers
if (this.getPatch() > otherVersion.getPatch()) {
return true;
} else if (this.getPatch() == otherVersion.getPatch()) {
// if the patch numbers are equal compare the development stages
if (this.developmentStage.isMoreStableThan(otherVersion.developmentStage)) {
return true;
} else if (this.developmentStage == otherVersion.developmentStage) {
// if the stage is equal check if this version is in development and the other is not
return !this.isDevelopmentVersion && otherVersion.isDevelopmentVersion;
}
}
}
}
return false;
}
/**
* Checks if this version should be updated to one of the given ones.
* Ignoring the other Version if this one is Stable and the other one is not.
*
* @return The version this one should be updated to, or an empty Optional
*/
public Optional<Version> shouldBeUpdatedTo(List<Version> availableVersions) {
Optional<Version> newerVersion = Optional.empty();
for (Version version : availableVersions) {
if (this.shouldBeUpdatedTo(version)
&& (!newerVersion.isPresent() || version.isNewerThan(newerVersion.get()))) {
newerVersion = Optional.of(version);
}
}
return newerVersion;
}
/**
* Checks if this version should be updated to the given one.
* Ignoring the other Version if this one is Stable and the other one is not.
*
* @return True if this version should be updated to the given one
*/
public boolean shouldBeUpdatedTo(Version otherVersion) {
// ignoring the other version if it is not stable, except if this version itself is not stable
if (developmentStage == Version.DevelopmentStage.STABLE
&& otherVersion.developmentStage != Version.DevelopmentStage.STABLE) {
return false;
}
// check if the other version is newer than given one
return otherVersion.isNewerThan(this);
}
public String getFullVersion() {
return fullVersion;
}
public int getMajor() {
return major;
}
public int getMinor() {
return minor;
}
public int getPatch() {
return patch;
}
public boolean isDevelopmentVersion() {
return isDevelopmentVersion;
}
/**
* @return The link to the changelog on GitHub to this specific version (https://github.com/JabRef/jabref/blob/vX.X/CHANGELOG.md)
*/
public String getChangelogUrl() {
if (isDevelopmentVersion) {
return "https://github.com/JabRef/jabref/blob/master/CHANGELOG.md#unreleased";
} else {
StringBuilder changelogLink = new StringBuilder()
.append("https://github.com/JabRef/jabref/blob/v")
.append(this.getMajor())
.append(".")
.append(this.getMinor());
if (this.getPatch() != 0) {
changelogLink
.append(".")
.append(this.getPatch());
}
changelogLink
.append(this.developmentStage.stage)
.append("/CHANGELOG.md");
return changelogLink.toString();
}
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof Version)) {
return false;
}
// till all the information are stripped from the fullversion this should suffice
return this.getFullVersion().equals(((Version) other).getFullVersion());
}
@Override
public int hashCode() {
return getFullVersion().hashCode();
}
@Override
public String toString() {
return this.getFullVersion();
}
public enum DevelopmentStage {
UNKNOWN("", 0),
ALPHA("-alpha", 1),
BETA("-beta", 2),
STABLE("", 3);
/**
* describes how stable this stage is, the higher the better
*/
private final int stability;
private final String stage;
DevelopmentStage(String stage, int stability) {
this.stage = stage;
this.stability = stability;
}
public static DevelopmentStage parse(String stage) {
if (stage == null) {
LOGGER.warn("The stage cannot be null");
return UNKNOWN;
} else if (stage.equals(STABLE.stage)) {
return STABLE;
} else if (stage.equals(ALPHA.stage)) {
return ALPHA;
} else if (stage.equals(BETA.stage)) {
return BETA;
}
LOGGER.warn("Unknown development stage: " + stage);
return UNKNOWN;
}
/**
* @return true if this stage is more stable than the {@code otherStage}
*/
public boolean isMoreStableThan(DevelopmentStage otherStage) {
return this.stability > otherStage.stability;
}
}
}