diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index 250c00cfb..04a600b18 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -296,7 +296,7 @@ public final class LauncherHelper { } // Minecraft 1.13 may crash when generating world on Java 8 earlier than 1.8.0_51 - VersionNumber JAVA_8 = VersionNumber.asVersion("1.8.0.51"); + VersionNumber JAVA_8 = VersionNumber.asVersion("1.8.0_51"); if (!flag && gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0 && java.getParsedVersion() == JavaVersion.JAVA_8 && java.getVersionNumber().compareTo(JAVA_8) < 0) { Optional java8 = JavaVersion.getJavas().stream() .filter(javaVersion -> javaVersion.getVersionNumber().compareTo(JAVA_8) >= 0) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java index 7feb78c20..70b80d1d0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MainPage.java @@ -34,6 +34,7 @@ import javafx.scene.layout.StackPane; import javafx.scene.shape.Rectangle; import javafx.util.Duration; import org.jackhuang.hmcl.game.HMCLGameRepository; +import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Theme; @@ -49,6 +50,7 @@ import org.jackhuang.hmcl.upgrade.UpdateHandler; import org.jackhuang.hmcl.util.javafx.MultiStepBinding; import org.jackhuang.hmcl.util.versioning.VersionNumber; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -130,7 +132,7 @@ public final class MainPage extends StackPane implements DecoratorPage { HMCLGameRepository repository = profile.getRepository(); List children = repository.getVersions().parallelStream() .filter(version -> !version.isHidden()) - .sorted((a, b) -> VersionNumber.COMPARATOR.compare(VersionNumber.asVersion(a.getId()), VersionNumber.asVersion(b.getId()))) + .sorted(Comparator.comparing(Version::getReleaseTime).thenComparing(a -> VersionNumber.asVersion(a.getId()))) .map(version -> { StackPane pane = new StackPane(); GameItem item = new GameItem(profile, version.getId()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameList.java index 94799aed5..8f2bd35c7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameList.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameList.java @@ -25,9 +25,9 @@ import javafx.scene.control.Control; import javafx.scene.control.Skin; import javafx.scene.control.ToggleGroup; import org.jackhuang.hmcl.event.EventBus; -import org.jackhuang.hmcl.event.RefreshedVersionsEvent; import org.jackhuang.hmcl.event.RefreshingVersionsEvent; import org.jackhuang.hmcl.game.HMCLGameRepository; +import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.ui.Controllers; @@ -37,6 +37,7 @@ import org.jackhuang.hmcl.ui.download.DownloadWizardProvider; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.versioning.VersionNumber; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -65,7 +66,7 @@ public class GameList extends Control implements DecoratorPage { toggleGroup.getProperties().put("ReferenceHolder", listenerHolder); List children = repository.getVersions().parallelStream() .filter(version -> !version.isHidden()) - .sorted((a, b) -> VersionNumber.COMPARATOR.compare(VersionNumber.asVersion(a.getId()), VersionNumber.asVersion(b.getId()))) + .sorted(Comparator.comparing(Version::getReleaseTime).thenComparing(a -> VersionNumber.asVersion(a.getId()))) .map(version -> new GameListItem(toggleGroup, profile, version.getId())) .collect(Collectors.toList()); JFXUtilities.runInFX(() -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java index 7854b52df..6f8e25a80 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java @@ -11,7 +11,6 @@ import javafx.stage.FileChooser; import org.jackhuang.hmcl.game.World; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider; -import org.jackhuang.hmcl.util.versioning.IntVersionNumber; import org.jackhuang.hmcl.util.versioning.VersionNumber; import java.io.File; @@ -67,7 +66,7 @@ public class WorldListItem extends Control { public void manageDatapacks() { if (world.getGameVersion() == null || // old game will not write game version to level.dat - (IntVersionNumber.isIntVersionNumber(world.getGameVersion()) // we don't parse snapshot version + (VersionNumber.isIntVersionNumber(world.getGameVersion()) // we don't parse snapshot version && VersionNumber.asVersion(world.getGameVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0)) { Controllers.dialog(i18n("world.datapack.1_13")); return; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java index 8f691dd63..0ad273eed 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java @@ -66,9 +66,7 @@ public final class ForgeVersionList extends VersionList { versions.clear(); for (Map.Entry entry : root.getGameVersions().entrySet()) { - Optional gameVersion = VersionNumber.parseVersion(entry.getKey()); - if (!gameVersion.isPresent()) - continue; + String gameVersion = VersionNumber.normalize(entry.getKey()); for (int v : entry.getValue()) { ForgeVersion version = root.getNumber().get(v); if (version == null) @@ -84,7 +82,7 @@ public final class ForgeVersionList extends VersionList { if (jar == null) continue; - versions.put(gameVersion.get(), new ForgeRemoteVersion( + versions.put(gameVersion, new ForgeRemoteVersion( version.getGameVersion(), version.getVersion(), jar )); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBMCLVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBMCLVersionList.java index f42e0645c..a1866a0f4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBMCLVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBMCLVersionList.java @@ -71,11 +71,10 @@ public final class LiteLoaderBMCLVersionList extends VersionList entry : root.getVersions().entrySet()) { String gameVersion = entry.getKey(); LiteLoaderGameVersions liteLoader = entry.getValue(); - Optional gg = VersionNumber.parseVersion(gameVersion); - if (!gg.isPresent()) - continue; - doBranch(gg.get(), gameVersion, liteLoader.getRepoitory(), liteLoader.getArtifacts(), false); - doBranch(gg.get(), gameVersion, liteLoader.getRepoitory(), liteLoader.getSnapshots(), true); + + String gg = VersionNumber.normalize(gameVersion); + doBranch(gg, gameVersion, liteLoader.getRepoitory(), liteLoader.getArtifacts(), false); + doBranch(gg, gameVersion, liteLoader.getRepoitory(), liteLoader.getSnapshots(), true); } } finally { lock.writeLock().unlock(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderVersionList.java index 391cf87ac..6b4be0eec 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderVersionList.java @@ -71,11 +71,10 @@ public final class LiteLoaderVersionList extends VersionList entry : root.getVersions().entrySet()) { String gameVersion = entry.getKey(); LiteLoaderGameVersions liteLoader = entry.getValue(); - Optional gg = VersionNumber.parseVersion(gameVersion); - if (!gg.isPresent()) - continue; - doBranch(gg.get(), gameVersion, liteLoader.getRepoitory(), liteLoader.getArtifacts(), false); - doBranch(gg.get(), gameVersion, liteLoader.getRepoitory(), liteLoader.getSnapshots(), true); + + String gg = VersionNumber.normalize(gameVersion); + doBranch(gg, gameVersion, liteLoader.getRepoitory(), liteLoader.getArtifacts(), false); + doBranch(gg, gameVersion, liteLoader.getRepoitory(), liteLoader.getSnapshots(), true); } } finally { lock.writeLock().unlock(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineBMCLVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineBMCLVersionList.java index 1b02e6827..3323b8fa4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineBMCLVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineBMCLVersionList.java @@ -70,8 +70,9 @@ public final class OptiFineBMCLVersionList extends VersionList versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, () -> mirror, isPre))); + + String gameVersion = VersionNumber.normalize(element.getGameVersion()); + versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, () -> mirror, isPre)); } } }; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java index dedaf50b8..3eb8eef96 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java @@ -41,10 +41,12 @@ public final class ModManager { File modsDirectory = new File(repository.getRunDirectory(id), "mods"); Consumer puter = modFile -> Lang.ignoringException(() -> modCache.put(id, ModInfo.fromFile(modFile))); Optional.ofNullable(modsDirectory.listFiles()).map(Arrays::stream).ifPresent(files -> files.forEach(modFile -> { - if (modFile.isDirectory() && VersionNumber.parseVersion(modFile.getName()).isPresent()) + if (modFile.isDirectory() && VersionNumber.isIntVersionNumber(modFile.getName())) { + // If the folder name is game version, forge will search mod in this subdirectory Optional.ofNullable(modFile.listFiles()).map(Arrays::stream).ifPresent(x -> x.forEach(puter)); - else + } else { puter.accept(modFile); + } })); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/JavaVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/JavaVersion.java index b30a8f42e..a69430f2d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/JavaVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/JavaVersion.java @@ -70,7 +70,7 @@ public final class JavaVersion { } public VersionNumber getVersionNumber() { - return VersionNumber.asVersion(longVersion.replace('_', '.')); + return VersionNumber.asVersion(longVersion); } /** diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/ComposedVersionNumber.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/ComposedVersionNumber.java deleted file mode 100644 index b37258819..000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/ComposedVersionNumber.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2017 huangyuhui - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see {http://www.gnu.org/licenses/}. - */ -package org.jackhuang.hmcl.util.versioning; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -/** - * If a version string contains '-', a {@link ComposedVersionNumber} - * will be generated. - * - * Formats like 1.7.10-OptiFine, 1.12.2-Forge - * - * @author huangyuhui - */ -public final class ComposedVersionNumber extends VersionNumber { - List composed; - - public static boolean isComposedVersionNumber(String version) { - return version.contains("-"); - } - - ComposedVersionNumber(String version) { - composed = Arrays.stream(version.split("-")) - .map(VersionNumber::asVersion) - .collect(Collectors.toList()); - } - - @Override - public String toString() { - return composed.stream().map(VersionNumber::toString).collect(Collectors.joining("-")); - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/IntVersionNumber.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/IntVersionNumber.java deleted file mode 100644 index 3da7dbb48..000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/IntVersionNumber.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see {http://www.gnu.org/licenses/}. - */ -package org.jackhuang.hmcl.util.versioning; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -import org.jackhuang.hmcl.util.StringUtils; - -/** - * If a version string formats x.x.x.x, a {@code IntVersionNumber} - * will be generated. - * - * @author huangyuhui - */ -public final class IntVersionNumber extends VersionNumber { - - final List version; - - public static boolean isIntVersionNumber(String version) { - if (version.chars().noneMatch(ch -> ch != '.' && (ch < '0' || ch > '9')) - && !version.contains("..") && StringUtils.isNotBlank(version)) { - String[] arr = version.split("\\."); - for (String str : arr) - if (str.length() > 9) - // Numbers which are larger than 1e9 cannot be stored as integer. - return false; - return true; - } else { - return false; - } - } - - IntVersionNumber(String version) { - if (!isIntVersionNumber(version)) - throw new IllegalArgumentException("The version " + version + " is malformed, only dots and digits are allowed."); - - List versions = Arrays.stream(version.split("\\.")) - .map(Integer::parseInt) - .collect(Collectors.toList()); - while (!versions.isEmpty() && versions.get(versions.size() - 1) == 0) - versions.remove(versions.size() - 1); - - this.version = versions; - } - - public int get(int index) { - return version.get(index); - } - - @Override - public String toString() { - return version.stream().map(Object::toString).collect(Collectors.joining(".")); - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/StringVersionNumber.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/StringVersionNumber.java deleted file mode 100644 index 9c981dac4..000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/StringVersionNumber.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see {http://www.gnu.org/licenses/}. - */ -package org.jackhuang.hmcl.util.versioning; - -import java.util.Objects; - -/** - * If a version string contains alphabets, a {@code StringVersionNumber} - * will be constructed. - * - * @author huangyuhui - */ -public final class StringVersionNumber extends VersionNumber { - - private final String version; - - StringVersionNumber(String version) { - Objects.requireNonNull(version); - this.version = version; - } - - public String getVersion() { - return version; - } - - @Override - public String toString() { - return version; - } - -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/VersionNumber.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/VersionNumber.java index 47385ac60..86cc25613 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/VersionNumber.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/VersionNumber.java @@ -1,108 +1,339 @@ -/* - * Hello Minecraft! Launcher. - * Copyright (C) 2018 huangyuhui - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see {http://www.gnu.org/licenses/}. - */ package org.jackhuang.hmcl.util.versioning; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; +import org.jackhuang.hmcl.util.StringUtils; + +import java.math.BigInteger; +import java.util.*; /** - * The formatted version number represents a version string. - * - * @author huangyuhui + * Copied from org.apache.maven.artifact.versioning.ComparableVersion + * Apache License 2.0 */ -public abstract class VersionNumber implements Comparable { +public class VersionNumber implements Comparable { public static VersionNumber asVersion(String version) { Objects.requireNonNull(version); - if (ComposedVersionNumber.isComposedVersionNumber(version)) - return new ComposedVersionNumber(version); - else if (IntVersionNumber.isIntVersionNumber(version)) - return new IntVersionNumber(version); - else - return new StringVersionNumber(version); + return new VersionNumber(version); } - public static Optional parseVersion(String str) { - if (IntVersionNumber.isIntVersionNumber(str)) - return Optional.of(new IntVersionNumber(str).toString()); - else - return Optional.empty(); + public static String normalize(String str) { + return new VersionNumber(str).getCanonical(); + } + + public static boolean isIntVersionNumber(String version) { + if (version.chars().noneMatch(ch -> ch != '.' && (ch < '0' || ch > '9')) + && !version.contains("..") && StringUtils.isNotBlank(version)) { + String[] arr = version.split("\\."); + for (String str : arr) + if (str.length() > 9) + // Numbers which are larger than 1e9 cannot be stored as integer. + return false; + return true; + } else { + return false; + } + } + + private String value; + private String canonical; + private ListItem items; + + private interface Item { + int INTEGER_ITEM = 0; + int STRING_ITEM = 1; + int LIST_ITEM = 2; + + int compareTo(Item item); + + int getType(); + + boolean isNull(); + } + + /** + * Represents a numeric item in the version item list. + */ + private static class IntegerItem + implements Item { + private final BigInteger value; + + public static final IntegerItem ZERO = new IntegerItem(); + + private IntegerItem() { + this.value = BigInteger.ZERO; + } + + IntegerItem(String str) { + this.value = new BigInteger(str); + } + + public int getType() { + return INTEGER_ITEM; + } + + public boolean isNull() { + return BigInteger.ZERO.equals(value); + } + + public int compareTo(Item item) { + if (item == null) { + return BigInteger.ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1 + } + + switch (item.getType()) { + case INTEGER_ITEM: + return value.compareTo(((IntegerItem) item).value); + + case STRING_ITEM: + return 1; // 1.1 > 1-sp + + case LIST_ITEM: + return 1; // 1.1 > 1-1 + + default: + throw new RuntimeException("invalid item: " + item.getClass()); + } + } + + public String toString() { + return value.toString(); + } + } + + /** + * Represents a string in the version item list, usually a qualifier. + */ + private static class StringItem + implements Item { + private String value; + + StringItem(String value) { + this.value = value; + } + + public int getType() { + return STRING_ITEM; + } + + public boolean isNull() { + return value.isEmpty(); + } + + public int compareTo(Item item) { + if (item == null) { + // 1-string > 1 + return 1; + } + switch (item.getType()) { + case INTEGER_ITEM: + return -1; // 1.any < 1.1 ? + + case STRING_ITEM: + return value.compareTo(((StringItem) item).value); + + case LIST_ITEM: + return -1; // 1.any < 1-1 + + default: + throw new RuntimeException("invalid item: " + item.getClass()); + } + } + + public String toString() { + return value; + } + } + + /** + * Represents a version list item. This class is used both for the global item list and for sub-lists (which start + * with '-(number)' in the version specification). + */ + private static class ListItem + extends ArrayList + implements Item { + Character separator; + + public ListItem() {} + + public ListItem(char separator) { + this.separator = separator; + } + + public int getType() { + return LIST_ITEM; + } + + public boolean isNull() { + return (size() == 0); + } + + void normalize() { + for (int i = size() - 1; i >= 0; i--) { + Item lastItem = get(i); + + if (lastItem.isNull()) { + // remove null trailing items: 0, "", empty list + remove(i); + } else if (!(lastItem instanceof ListItem)) { + break; + } + } + } + + public int compareTo(Item item) { + if (item == null) { + if (size() == 0) { + return 0; // 1-0 = 1- (normalize) = 1 + } + Item first = get(0); + return first.compareTo(null); + } + switch (item.getType()) { + case INTEGER_ITEM: + return -1; // 1-1 < 1.0.x + + case STRING_ITEM: + return 1; // 1-1 > 1-sp + + case LIST_ITEM: + Iterator left = iterator(); + Iterator right = ((ListItem) item).iterator(); + + while (left.hasNext() || right.hasNext()) { + Item l = left.hasNext() ? left.next() : null; + Item r = right.hasNext() ? right.next() : null; + + // if this is shorter, then invert the compare and mul with -1 + int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r); + + if (result != 0) { + return result; + } + } + + return 0; + + default: + throw new RuntimeException("invalid item: " + item.getClass()); + } + } + + public String toString() { + StringBuilder buffer = new StringBuilder(); + for (Item item : this) { + if (buffer.length() > 0) { + if (!(item instanceof ListItem)) + buffer.append('.'); + } + buffer.append(item); + } + if (separator != null) + return separator + buffer.toString(); + else + return buffer.toString(); + } + } + + public VersionNumber(String version) { + parseVersion(version); + } + + private void parseVersion(String version) { + this.value = version; + + items = new ListItem(); + + version = version.toLowerCase(Locale.ENGLISH); + + ListItem list = items; + + Stack stack = new Stack<>(); + stack.push(list); + + boolean isDigit = false; + + int startIndex = 0; + + for (int i = 0; i < version.length(); i++) { + char c = version.charAt(i); + + if (c == '.') { + if (i == startIndex) { + list.add(IntegerItem.ZERO); + } else { + list.add(parseItem(version.substring(startIndex, i))); + } + startIndex = i + 1; + } else if ("!\"#$%&'()*+,-/:;<=>?@[\\]^_`{|}~".indexOf(c) != -1) { + if (i == startIndex) { + list.add(IntegerItem.ZERO); + } else { + list.add(parseItem(version.substring(startIndex, i))); + } + startIndex = i + 1; + + list.add(list = new ListItem(c)); + stack.push(list); + } else if (Character.isDigit(c)) { + if (!isDigit && i > startIndex) { + list.add(parseItem(version.substring(startIndex, i))); + startIndex = i; + + list.add(list = new ListItem()); + stack.push(list); + } + + isDigit = true; + } else { + if (isDigit && i > startIndex) { + list.add(parseItem(version.substring(startIndex, i))); + startIndex = i; + + list.add(list = new ListItem()); + stack.push(list); + } + + isDigit = false; + } + } + + if (version.length() > startIndex) { + list.add(parseItem(version.substring(startIndex))); + } + + while (!stack.isEmpty()) { + list = (ListItem) stack.pop(); + list.normalize(); + } + + canonical = items.toString(); + } + + private static Item parseItem(String buf) { + return buf.chars().allMatch(Character::isDigit) ? new IntegerItem(buf) : new StringItem(buf); } @Override public int compareTo(VersionNumber o) { - return COMPARATOR.compare(this, o); + return items.compareTo(o.items); } @Override - public boolean equals(Object another) { - return another instanceof VersionNumber && this.toString().equals(another.toString()); + public String toString() { + return value; + } + + public String getCanonical() { + return canonical; + } + + @Override + public boolean equals(Object o) { + return o instanceof VersionNumber && canonical.equals(((VersionNumber) o).canonical); } @Override public int hashCode() { - return toString().hashCode(); + return canonical.hashCode(); } - - private static > int compareTo(List a, List b) { - int i; - for (i = 0; i < a.size() && i < b.size(); ++i) { - int res = a.get(i).compareTo(b.get(i)); - if (res != 0) - return res; - } - if (i < a.size()) return 1; - else if (i < b.size()) return -1; - else return 0; - } - - public static final Comparator COMPARATOR = new Comparator() { - @Override - public int compare(VersionNumber a, VersionNumber b) { - if (a == null || b == null) - return 0; - else { - if (a instanceof ComposedVersionNumber) { - if (b instanceof ComposedVersionNumber) - return compareTo(((ComposedVersionNumber) a).composed, ((ComposedVersionNumber) b).composed); - else - return compare(((ComposedVersionNumber) a).composed.get(0), b); - } else if (a instanceof IntVersionNumber) { - if (b instanceof ComposedVersionNumber) - return -compare(b, a); - else if (b instanceof IntVersionNumber) - return compareTo(((IntVersionNumber) a).version, ((IntVersionNumber) b).version); - else if (b instanceof StringVersionNumber) - return a.toString().compareTo(b.toString()); - } else if (a instanceof StringVersionNumber) { - if (b instanceof ComposedVersionNumber) - return -compare(b, a); - else if (b instanceof StringVersionNumber) - return a.toString().compareTo(b.toString()); - else if (b instanceof IntVersionNumber) - return a.toString().compareTo(b.toString()); - } - - throw new IllegalArgumentException("Unrecognized VersionNumber " + a + " and " + b); - } - } - - }; } diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/VersionNumberTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/VersionNumberTest.java new file mode 100644 index 000000000..ad37dfc9d --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/VersionNumberTest.java @@ -0,0 +1,98 @@ +package org.jackhuang.hmcl.util; + +import org.jackhuang.hmcl.util.versioning.VersionNumber; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +public class VersionNumberTest { + + @Test + public void testCanonical() { + VersionNumber u, v; + + v = VersionNumber.asVersion("3.2.0.0"); + Assert.assertEquals("3.2", v.getCanonical()); + + v = VersionNumber.asVersion("3.2.0.0-5"); + Assert.assertEquals("3.2-5", v.getCanonical()); + + v = VersionNumber.asVersion("3.2.0.0-0"); + Assert.assertEquals("3.2", v.getCanonical()); + + v = VersionNumber.asVersion("3.2--------"); + Assert.assertEquals("3.2", v.getCanonical()); + + v = VersionNumber.asVersion("1.7.2$%%^@&snapshot-3.1.1"); + Assert.assertEquals("1.7.2$%%^@&snapshot-3.1.1", v.getCanonical()); + } + + @Test + public void testComparator() { + VersionNumber u, v; + + u = VersionNumber.asVersion("1.7.10forge1614_FTBInfinity"); + v = VersionNumber.asVersion("1.12.2"); + Assert.assertTrue(u.compareTo(v) < 0); + + u = VersionNumber.asVersion("1.8.0_51"); + v = VersionNumber.asVersion("1.8.0.51"); + Assert.assertTrue(u.compareTo(v) < 0); + + u = VersionNumber.asVersion("1.8.0_151"); + v = VersionNumber.asVersion("1.8.0_77"); + Assert.assertTrue(u.compareTo(v) > 0); + + u = VersionNumber.asVersion("1.6.0_22"); + v = VersionNumber.asVersion("1.8.0_11"); + Assert.assertTrue(u.compareTo(v) < 0); + } + + @Test + public void testSorting() { + List input = Arrays.asList( + "1.10", + "1.10.2", + "1.10.2-All the Mods", + "1.10.2-AOE", + "1.10.2-AOE-1.1.5", + "1.10.2-forge2511-Age_of_Progression", + "1.10.2-forge2511-AOE-1.1.2", + "1.10.2-forge2511-ATM-E", + "1.10.2-forge2511-simple_life_2", + "1.10.2-forge2511_bxztest", + "1.10.2-forge2511_Farming_Valley", + "1.10.2-forge2511简单生活BXZ", + "1.10.2-FTB_Beyond", + "1.10.2-LiteLoader1.10.2", + "1.12.2", + "1.12.2_Modern_Skyblock-3.4.2", + "1.13.1", + "1.6.4", + "1.6.4-Forge9.11.1.1345", + "1.7.10", + "1.7.10-1614", + "1.7.10-1614-test", + "1.7.10-F1614-L", + "1.7.10-FL1614_04", + "1.7.10-Forge10.13.4.1614-1.7.10", + "1.7.10-Forge1614", + "1.7.10-Forge1614.1", + "1.7.10Agrarian_Skies_2", + "1.7.10forge1614test", + "1.7.10forge1614_ATlauncher", + "1.7.10forge1614_FTBInfinity", + "1.7.10Forge1614_FTBInfinity-2.6.0", + "1.7.10Forge1614_FTBInfinity-3.0.1", + "1.7.10forge1614_FTBInfinity_server", + "1.8", + "1.8-forge1577", + "1.8.9", + "1.8.9-forge1902", + "1.9"); + input.sort(Comparator.comparing(VersionNumber::asVersion)); + } +}