diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLJavaVersion.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLJavaVersion.java new file mode 100644 index 000000000..f61d92d78 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLJavaVersion.java @@ -0,0 +1,93 @@ +package org.jackhuang.hmcl.game; + +import org.jackhuang.hmcl.util.Range; +import org.jackhuang.hmcl.util.platform.JavaVersion; +import org.jackhuang.hmcl.util.versioning.VersionNumber; +import org.jetbrains.annotations.Nullable; + +import static org.jackhuang.hmcl.download.LibraryAnalyzer.LAUNCH_WRAPPER_MAIN; + +public enum HMCLJavaVersion { + + // Minecraft>=1.17 requires Java 16 + VANILLA_JAVA_16(HMCLJavaVersion.RULE_MANDATORY, versionRange("1.17", HMCLJavaVersion.MAX), versionRange("16", HMCLJavaVersion.MAX)), + // Minecraft>=1.13 requires Java 8 + VANILLA_JAVA_8(HMCLJavaVersion.RULE_MANDATORY, versionRange("1.13", HMCLJavaVersion.MAX), versionRange("8", HMCLJavaVersion.MAX)), + // Minecraft>=1.7.10+Forge accepts Java 8 + SUGGEST_JAVA_8(HMCLJavaVersion.RULE_SUGGESTED, versionRange("1.7.10", HMCLJavaVersion.MAX), versionRange("8", HMCLJavaVersion.MAX)), + // LaunchWrapper<=1.12 will crash because of assuming the system class loader is an instance of URLClassLoader (Java 8) + LAUNCH_WRAPPER(HMCLJavaVersion.RULE_MANDATORY, versionRange("0", "1.12"), versionRange("0", "8")) { + @Override + public boolean test(Version version) { + return LAUNCH_WRAPPER_MAIN.equals(version.getMainClass()) && + version.getLibraries().stream() + .filter(library -> "launchwrapper".equals(library.getArtifactId())) + .anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0); + } + }, + // Minecraft>=1.13 may crash when generating world on Java [1.8,1.8.0_51) + VANILLA_JAVA_8_51(HMCLJavaVersion.RULE_SUGGESTED, versionRange("1.13", HMCLJavaVersion.MAX), versionRange("1.8.0_51", HMCLJavaVersion.MAX)), + + ; + + private final int type; + private final Range gameVersion; + private final Range javaVersion; + + HMCLJavaVersion(int type, Range gameVersion, Range javaVersion) { + this.type = type; + this.gameVersion = gameVersion; + this.javaVersion = javaVersion; + } + + public boolean test(Version version) { + return true; + } + + @Nullable + public static JavaVersion findSuitableJavaVersion(VersionNumber gameVersion, Version version) throws InterruptedException { + Range mandatoryJavaRange = versionRange(MIN, MAX); + Range suggestedJavaRange = versionRange(MIN, MAX); + for (HMCLJavaVersion java : values()) { + if (java.gameVersion.contains(gameVersion) && java.test(version)) { + if (java.type == RULE_MANDATORY) { + mandatoryJavaRange = mandatoryJavaRange.intersectionWith(java.javaVersion); + suggestedJavaRange = suggestedJavaRange.intersectionWith(java.javaVersion); + } else if (java.type == RULE_SUGGESTED) { + suggestedJavaRange = suggestedJavaRange.intersectionWith(java.javaVersion); + } + } + } + + JavaVersion mandatory = null; + JavaVersion suggested = null; + for (JavaVersion javaVersion : JavaVersion.getJavas()) { + // select the latest java version that this version accepts. + if (mandatoryJavaRange.contains(javaVersion.getVersionNumber())) { + if (mandatory == null) mandatory = javaVersion; + else if (javaVersion.getVersionNumber().compareTo(mandatory.getVersionNumber()) > 0) { + mandatory = javaVersion; + } + } + if (suggestedJavaRange.contains(javaVersion.getVersionNumber())) { + if (suggested == null) suggested = javaVersion; + else if (javaVersion.getVersionNumber().compareTo(suggested.getVersionNumber()) > 0) { + suggested = javaVersion; + } + } + } + + if (suggested != null) return suggested; + else return mandatory; + } + + public static final int RULE_MANDATORY = 1; + public static final int RULE_SUGGESTED = 2; + + public static final String MIN = "0"; + public static final String MAX = "10000"; + + private static Range versionRange(String fromInclusive, String toExclusive) { + return Range.between(VersionNumber.asVersion(fromInclusive), VersionNumber.asVersion(toExclusive)); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java index a33ae1f75..379f94705 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java @@ -21,14 +21,13 @@ import com.google.gson.*; import com.google.gson.annotations.JsonAdapter; import javafx.beans.InvalidationListener; import javafx.beans.property.*; -import org.jackhuang.hmcl.game.GameDirectoryType; -import org.jackhuang.hmcl.game.NativesDirectoryType; -import org.jackhuang.hmcl.game.ProcessPriority; +import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.platform.JavaVersion; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.Platform; +import org.jackhuang.hmcl.util.versioning.VersionNumber; import java.io.IOException; import java.lang.reflect.Type; @@ -36,6 +35,8 @@ import java.nio.file.InvalidPathException; import java.nio.file.Paths; import java.util.List; import java.util.Optional; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; /** @@ -105,6 +106,15 @@ public final class VersionSetting implements Cloneable { setDefaultJavaPath(null); } + public boolean isJavaAutoSelected() { + return "Auto".equals(getJava()); + } + + public void setJavaAutoSelected() { + setJava("Auto"); + setDefaultJavaPath(null); + } + private final StringProperty defaultJavaPathProperty = new SimpleStringProperty(this, "defaultJavaPath", ""); /** @@ -571,38 +581,46 @@ public final class VersionSetting implements Cloneable { launcherVisibilityProperty.set(launcherVisibility); } - public JavaVersion getJavaVersion() throws InterruptedException { - return getJavaVersion(true); + public CompletableFuture getJavaVersion(String gameVersion, Version version) { + return getJavaVersion(gameVersion, version, true); } - public JavaVersion getJavaVersion(boolean checkJava) throws InterruptedException { - // TODO: lazy initialization may result in UI suspension. - if (StringUtils.isBlank(getJava())) - setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom"); - if ("Default".equals(getJava())) return JavaVersion.fromCurrentEnvironment(); - else if (isUsesCustomJavaDir()) { + public CompletableFuture getJavaVersion(String gameVersion, Version version, boolean checkJava) { + return CompletableFuture.supplyAsync(() -> { try { - if (checkJava) - return JavaVersion.fromExecutable(Paths.get(getJavaDir())); - else - return new JavaVersion(Paths.get(getJavaDir()), "", Platform.getPlatform()); - } catch (IOException | InvalidPathException e) { - return null; // Custom Java Directory not found, + if (StringUtils.isBlank(getJava())) + setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom"); + if ("Default".equals(getJava())) { + return JavaVersion.fromCurrentEnvironment(); + } else if (isJavaAutoSelected()) { + return HMCLJavaVersion.findSuitableJavaVersion(VersionNumber.asVersion(gameVersion), version); + } else if (isUsesCustomJavaDir()) { + try { + if (checkJava) + return JavaVersion.fromExecutable(Paths.get(getJavaDir())); + else + return new JavaVersion(Paths.get(getJavaDir()), "", Platform.getPlatform()); + } catch (IOException | InvalidPathException e) { + return null; // Custom Java Directory not found, + } + } else if (StringUtils.isNotBlank(getJava())) { + List matchedJava = JavaVersion.getJavas().stream() + .filter(java -> java.getVersion().equals(getJava())) + .collect(Collectors.toList()); + if (matchedJava.isEmpty()) { + setJava("Default"); + return JavaVersion.fromCurrentEnvironment(); + } else { + return matchedJava.stream() + .filter(java -> java.getBinary().toString().equals(getDefaultJavaPath())) + .findFirst() + .orElse(matchedJava.get(0)); + } + } else throw new Error(); + } catch (InterruptedException e) { + throw new CancellationException(); } - } else if (StringUtils.isNotBlank(getJava())) { - List matchedJava = JavaVersion.getJavas().stream() - .filter(java -> java.getVersion().equals(getJava())) - .collect(Collectors.toList()); - if (matchedJava.isEmpty()) { - setJava("Default"); - return JavaVersion.fromCurrentEnvironment(); - } else { - return matchedJava.stream() - .filter(java -> java.getBinary().toString().equals(getDefaultJavaPath())) - .findFirst() - .orElse(matchedJava.get(0)); - } - } else throw new Error(); + }); } public void setJavaVersion(JavaVersion java) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java index a54328a6d..eed9edcaa 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java @@ -115,6 +115,7 @@ public class MultiFileItem extends VBox { protected final String title; protected String subtitle; protected final T data; + protected final BooleanProperty selected = new SimpleBooleanProperty(); public Option(String title, T data) { this.title = title; @@ -138,6 +139,18 @@ public class MultiFileItem extends VBox { return this; } + public boolean isSelected() { + return selected.get(); + } + + public BooleanProperty selectedProperty() { + return selected; + } + + public void setSelected(boolean selected) { + this.selected.set(selected); + } + protected Node createItem(ToggleGroup group) { BorderPane pane = new BorderPane(); pane.setPadding(new Insets(3)); @@ -147,6 +160,7 @@ public class MultiFileItem extends VBox { BorderPane.setAlignment(left, Pos.CENTER_LEFT); left.setToggleGroup(group); left.setUserData(data); + selected.bind(left.selectedProperty()); pane.setLeft(left); if (StringUtils.isNotBlank(subtitle)) { @@ -208,6 +222,7 @@ public class MultiFileItem extends VBox { BorderPane.setAlignment(left, Pos.CENTER_LEFT); left.setToggleGroup(group); left.setUserData(data); + selected.bind(left.selectedProperty()); pane.setLeft(left); BorderPane.setAlignment(customField, Pos.CENTER_RIGHT); @@ -266,6 +281,7 @@ public class MultiFileItem extends VBox { BorderPane.setAlignment(left, Pos.CENTER_LEFT); left.setToggleGroup(group); left.setUserData(data); + selected.bind(left.selectedProperty()); pane.setLeft(left); selector.disableProperty().bind(left.selectedProperty().not()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java index e9cbd8c26..7d969c548 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java @@ -101,6 +101,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag private final OptionToggleButton useNativeOpenALPane; private final ComponentSublist javaSublist; private final MultiFileItem javaItem; + private final MultiFileItem.Option javaAutoDeterminedOption; private final MultiFileItem.FileOption javaCustomOption; private final ComponentSublist gameDirSublist; private final MultiFileItem gameDirItem; @@ -199,6 +200,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag javaSublist.getContent().add(javaItem); javaSublist.setTitle(i18n("settings.game.java_directory")); javaSublist.setHasSubtitle(true); + javaAutoDeterminedOption = new MultiFileItem.Option<>(i18n("settings.game.java_directory.auto"), null); javaCustomOption = new MultiFileItem.FileOption(i18n("settings.custom"), null) .setChooserTitle(i18n("settings.game.java_directory.choose")); @@ -541,6 +543,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag javaVersion.getPlatform().getBit()), javaVersion) .setSubtitle(javaVersion.getBinary().toString())) .collect(Collectors.toList()); + options.add(0, javaAutoDeterminedOption); options.add(javaCustomOption); javaItem.loadChildren(options); javaItemsLoaded = true; @@ -661,8 +664,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag enableSpecificSettings.set(!versionSetting.isUsesGlobal()); javaItem.setToggleSelectedListener(newValue -> { - if (newValue.getUserData() == null) { + if (javaCustomOption.isSelected()) { versionSetting.setUsesCustomJavaDir(); + } else if (javaAutoDeterminedOption.isSelected()) { + versionSetting.setJavaAutoSelected(); } else { versionSetting.setJavaVersion((JavaVersion) newValue.getUserData()); } @@ -711,7 +716,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag VersionSetting versionSetting = lastVersionSetting; if (versionSetting == null) return; - Task.supplyAsync(versionSetting::getJavaVersion) + Task.fromCompletableFuture(versionSetting.getJavaVersion()) .thenAcceptAsync(Schedulers.javafx(), javaVersion -> javaSublist.setSubtitle(Optional.ofNullable(javaVersion) .map(JavaVersion::getBinary).map(Path::toString).orElse("Invalid Java Path"))) .start(); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 7b71ee622..39ef5efe9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -424,6 +424,7 @@ launch.failed.decompressing_natives=Unable to decompress native libraries. launch.failed.download_library=Unable to download library %s. launch.failed.executable_permission=Unable to add permission to the launch script launch.failed.exited_abnormally=Game exited abnormally, please check the log, or ask someone for help. +launch.failed.no_accepted_java=Cannot find the Java installation suitable for current game. If you think you have installed a suitable Java VM, you can manually select it in game settings. launch.state.dependencies=Dependencies launch.state.done=Done launch.state.logging_in=Logging In @@ -765,6 +766,7 @@ settings.game.dimension=Game Window Dimension settings.game.exploration=Explore settings.game.fullscreen=Fullscreen settings.game.java_directory=Java Directory +settings.game.java_directory.auto=Automatically selected settings.game.java_directory.bit=, %s-Bit settings.game.java_directory.choose=Choose Java Directory. settings.game.management=Manage diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index b9833571e..e25c98656 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -424,6 +424,7 @@ launch.failed.decompressing_natives=無法解壓縮遊戲資源庫。 launch.failed.download_library=無法下載遊戲相依元件 %s。 launch.failed.executable_permission=無法為啟動檔案新增執行權限。 launch.failed.exited_abnormally=遊戲非正常退出,請查看記錄檔案,或聯絡他人尋求幫助。 +launch.failed.no_accepted_java=找不到適合當前遊戲使用的 Java。如果您認為實際存在合適的 Java,您可以在遊戲設置中手動設置 Java。 launch.state.dependencies=處理遊戲相依元件 launch.state.done=啟動完成 launch.state.logging_in=登入 @@ -764,6 +765,7 @@ settings.game.dimension=遊戲介面解析度大小 settings.game.exploration=瀏覽 settings.game.fullscreen=全螢幕 settings.game.java_directory=Java 路徑 +settings.game.java_directory.auto=自動選擇合適的 Java settings.game.java_directory.bit=,%s 位 settings.game.java_directory.choose=選擇 Java 路徑 settings.game.management=管理 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index fdf6cba74..d6b311ef9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -424,6 +424,7 @@ launch.failed.decompressing_natives=未能解压游戏本地库。 launch.failed.download_library=未能下载游戏依赖 %s. launch.failed.executable_permission=未能为启动文件添加执行权限。 launch.failed.exited_abnormally=游戏非正常退出,请查看日志文件,或联系他人寻求帮助。 +launch.failed.no_accepted_java=找不到适合当前游戏使用的 Java。如果您认为实际存在合适的 Java,您可以在游戏设置中手动设置 Java。 launch.state.dependencies=处理游戏依赖 launch.state.done=启动完成 launch.state.logging_in=登录 @@ -764,6 +765,7 @@ settings.game.dimension=游戏窗口分辨率 settings.game.exploration=浏览 settings.game.fullscreen=全屏 settings.game.java_directory=Java 路径 +settings.game.java_directory.auto=自动选择合适的 Java settings.game.java_directory.bit=,%s 位 settings.game.java_directory.choose=选择 Java 路径 settings.game.management=管理 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Range.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Range.java new file mode 100644 index 000000000..aa1feb5a1 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Range.java @@ -0,0 +1,510 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 huangyuhui and contributors + * + * 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 . + */ +package org.jackhuang.hmcl.util; + +import java.util.Comparator; +import java.util.Objects; + +public class Range { + + + @SuppressWarnings({"rawtypes", "unchecked"}) + private enum ComparableComparator implements Comparator { + INSTANCE; + + /** + * Comparable based compare implementation. + * + * @param obj1 left hand side of comparison + * @param obj2 right hand side of comparison + * @return negative, 0, positive comparison value + */ + @Override + public int compare(final Object obj1, final Object obj2) { + return ((Comparable) obj1).compareTo(obj2); + } + } + + /** + * Serialization version. + * + * @see java.io.Serializable + */ + private static final long serialVersionUID = 1L; + + /** + *

Obtains a range with the specified minimum and maximum values (both inclusive).

+ * + *

The range uses the natural ordering of the elements to determine where + * values lie in the range.

+ * + *

The arguments may be passed in the order (min,max) or (max,min). + * The getMinimum and getMaximum methods will return the correct values.

+ * + * @param the type of the elements in this range + * @param fromInclusive the first value that defines the edge of the range, inclusive + * @param toInclusive the second value that defines the edge of the range, inclusive + * @return the range object, not null + * @throws IllegalArgumentException if either element is null + * @throws ClassCastException if the elements are not {@code Comparable} + */ + public static > Range between(final T fromInclusive, final T toInclusive) { + return between(fromInclusive, toInclusive, null); + } + + /** + *

Obtains a range with the specified minimum and maximum values (both inclusive).

+ * + *

The range uses the specified {@code Comparator} to determine where + * values lie in the range.

+ * + *

The arguments may be passed in the order (min,max) or (max,min). + * The getMinimum and getMaximum methods will return the correct values.

+ * + * @param the type of the elements in this range + * @param fromInclusive the first value that defines the edge of the range, inclusive + * @param toInclusive the second value that defines the edge of the range, inclusive + * @param comparator the comparator to be used, null for natural ordering + * @return the range object, not null + * @throws IllegalArgumentException if either element is null + * @throws ClassCastException if using natural ordering and the elements are not {@code Comparable} + */ + public static Range between(final T fromInclusive, final T toInclusive, final Comparator comparator) { + return new Range<>(fromInclusive, toInclusive, comparator); + } + + /** + *

Obtains a range using the specified element as both the minimum + * and maximum in this range.

+ * + *

The range uses the natural ordering of the elements to determine where + * values lie in the range.

+ * + * @param the type of the elements in this range + * @param element the value to use for this range, not null + * @return the range object, not null + * @throws IllegalArgumentException if the element is null + * @throws ClassCastException if the element is not {@code Comparable} + */ + public static > Range is(final T element) { + return between(element, element, null); + } + + /** + *

Obtains a range using the specified element as both the minimum + * and maximum in this range.

+ * + *

The range uses the specified {@code Comparator} to determine where + * values lie in the range.

+ * + * @param the type of the elements in this range + * @param element the value to use for this range, must not be {@code null} + * @param comparator the comparator to be used, null for natural ordering + * @return the range object, not null + * @throws IllegalArgumentException if the element is null + * @throws ClassCastException if using natural ordering and the elements are not {@code Comparable} + */ + public static Range is(final T element, final Comparator comparator) { + return between(element, element, comparator); + } + + /** + * The ordering scheme used in this range. + */ + private final Comparator comparator; + + /** + * Cached output hashCode (class is immutable). + */ + private transient int hashCode; + + /** + * The maximum value in this range (inclusive). + */ + private final T maximum; + + /** + * The minimum value in this range (inclusive). + */ + private final T minimum; + + /** + * Cached output toString (class is immutable). + */ + private transient String toString; + + /** + * Creates an instance. + * + * @param element1 the first element, not null + * @param element2 the second element, not null + * @param comp the comparator to be used, null for natural ordering + */ + @SuppressWarnings("unchecked") + private Range(final T element1, final T element2, final Comparator comp) { + if (element1 == null || element2 == null) { + throw new IllegalArgumentException("Elements in a range must not be null: element1=" + + element1 + ", element2=" + element2); + } + if (comp == null) { + this.comparator = ComparableComparator.INSTANCE; + } else { + this.comparator = comp; + } + if (this.comparator.compare(element1, element2) < 1) { + this.minimum = element1; + this.maximum = element2; + } else { + this.minimum = element2; + this.maximum = element1; + } + } + + public boolean isEmpty() { + return comparator.compare(minimum, maximum) > 0; + } + + /** + *

Checks whether the specified element occurs within this range.

+ * + * @param element the element to check for, null returns false + * @return true if the specified element occurs within this range + */ + public boolean contains(final T element) { + if (element == null) { + return false; + } + return comparator.compare(element, minimum) > -1 && comparator.compare(element, maximum) < 1; + } + + /** + *

Checks whether this range contains all the elements of the specified range.

+ * + *

This method may fail if the ranges have two different comparators or element types.

+ * + * @param otherRange the range to check, null returns false + * @return true if this range contains the specified range + * @throws RuntimeException if ranges cannot be compared + */ + public boolean containsRange(final Range otherRange) { + if (otherRange == null) { + return false; + } + return contains(otherRange.minimum) + && contains(otherRange.maximum); + } + + /** + *

Checks where the specified element occurs relative to this range.

+ * + *

The API is reminiscent of the Comparable interface returning {@code -1} if + * the element is before the range, {@code 0} if contained within the range and + * {@code 1} if the element is after the range.

+ * + * @param element the element to check for, not null + * @return -1, 0 or +1 depending on the element's location relative to the range + */ + public int elementCompareTo(final T element) { + // Comparable API says throw NPE on null + Objects.requireNonNull(element); + if (isAfter(element)) { + return -1; + } + if (isBefore(element)) { + return 1; + } + return 0; + } + + // Element tests + //-------------------------------------------------------------------- + + /** + *

Compares this range to another object to test if they are equal.

. + * + *

To be equal, the minimum and maximum values must be equal, which + * ignores any differences in the comparator.

+ * + * @param obj the reference object with which to compare + * @return true if this object is equal + */ + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != getClass()) { + return false; + } + @SuppressWarnings("unchecked") // OK because we checked the class above + final Range range = (Range) obj; + return minimum.equals(range.minimum) && + maximum.equals(range.maximum); + } + + /** + *

Gets the comparator being used to determine if objects are within the range.

+ * + *

Natural ordering uses an internal comparator implementation, thus this + * method never returns null. See {@link #isNaturalOrdering()}.

+ * + * @return the comparator being used, not null + */ + public Comparator getComparator() { + return comparator; + } + + /** + *

Gets the maximum value in this range.

+ * + * @return the maximum value in this range, not null + */ + public T getMaximum() { + return maximum; + } + + /** + *

Gets the minimum value in this range.

+ * + * @return the minimum value in this range, not null + */ + public T getMinimum() { + return minimum; + } + + /** + *

Gets a suitable hash code for the range.

+ * + * @return a hash code value for this object + */ + @Override + public int hashCode() { + int result = hashCode; + if (hashCode == 0) { + result = 17; + result = 37 * result + getClass().hashCode(); + result = 37 * result + minimum.hashCode(); + result = 37 * result + maximum.hashCode(); + hashCode = result; + } + return result; + } + + /** + * Calculate the intersection of {@code this} and an overlapping Range. + * + * @param other overlapping Range + * @return range representing the intersection of {@code this} and {@code other} ({@code this} if equal) + * @throws IllegalArgumentException if {@code other} does not overlap {@code this} + * @since 3.0.1 + */ + public Range intersectionWith(final Range other) { + if (!this.isOverlappedBy(other)) { + throw new IllegalArgumentException(String.format( + "Cannot calculate intersection with non-overlapping range %s", other)); + } + if (this.equals(other)) { + return this; + } + final T min = getComparator().compare(minimum, other.minimum) < 0 ? other.minimum : minimum; + final T max = getComparator().compare(maximum, other.maximum) < 0 ? maximum : other.maximum; + return between(min, max, getComparator()); + } + + /** + *

Checks whether this range is after the specified element.

+ * + * @param element the element to check for, null returns false + * @return true if this range is entirely after the specified element + */ + public boolean isAfter(final T element) { + if (element == null) { + return false; + } + return comparator.compare(element, minimum) < 0; + } + + /** + *

Checks whether this range is completely after the specified range.

+ * + *

This method may fail if the ranges have two different comparators or element types.

+ * + * @param otherRange the range to check, null returns false + * @return true if this range is completely after the specified range + * @throws RuntimeException if ranges cannot be compared + */ + public boolean isAfterRange(final Range otherRange) { + if (otherRange == null) { + return false; + } + return isAfter(otherRange.maximum); + } + + /** + *

Checks whether this range is before the specified element.

+ * + * @param element the element to check for, null returns false + * @return true if this range is entirely before the specified element + */ + public boolean isBefore(final T element) { + if (element == null) { + return false; + } + return comparator.compare(element, maximum) > 0; + } + + /** + *

Checks whether this range is completely before the specified range.

+ * + *

This method may fail if the ranges have two different comparators or element types.

+ * + * @param otherRange the range to check, null returns false + * @return true if this range is completely before the specified range + * @throws RuntimeException if ranges cannot be compared + */ + public boolean isBeforeRange(final Range otherRange) { + if (otherRange == null) { + return false; + } + return isBefore(otherRange.minimum); + } + + /** + *

Checks whether this range ends with the specified element.

+ * + * @param element the element to check for, null returns false + * @return true if the specified element occurs within this range + */ + public boolean isEndedBy(final T element) { + if (element == null) { + return false; + } + return comparator.compare(element, maximum) == 0; + } + + /** + *

Whether or not the Range is using the natural ordering of the elements.

+ * + *

Natural ordering uses an internal comparator implementation, thus this + * method is the only way to check if a null comparator was specified.

+ * + * @return true if using natural ordering + */ + public boolean isNaturalOrdering() { + return comparator == ComparableComparator.INSTANCE; + } + + /** + *

Checks whether this range is overlapped by the specified range.

+ * + *

Two ranges overlap if there is at least one element in common.

+ * + *

This method may fail if the ranges have two different comparators or element types.

+ * + * @param otherRange the range to test, null returns false + * @return true if the specified range overlaps with this + * range; otherwise, {@code false} + * @throws RuntimeException if ranges cannot be compared + */ + public boolean isOverlappedBy(final Range otherRange) { + if (otherRange == null) { + return false; + } + return otherRange.contains(minimum) + || otherRange.contains(maximum) + || contains(otherRange.minimum); + } + + /** + *

Checks whether this range starts with the specified element.

+ * + * @param element the element to check for, null returns false + * @return true if the specified element occurs within this range + */ + public boolean isStartedBy(final T element) { + if (element == null) { + return false; + } + return comparator.compare(element, minimum) == 0; + } + + /** + *

+ * Fits the given element into this range by returning the given element or, if out of bounds, the range minimum if + * below, or the range maximum if above. + *

+ *
+     * Range<Integer> range = Range.between(16, 64);
+     * range.fit(-9) -->  16
+     * range.fit(0)  -->  16
+     * range.fit(15) -->  16
+     * range.fit(16) -->  16
+     * range.fit(17) -->  17
+     * ...
+     * range.fit(63) -->  63
+     * range.fit(64) -->  64
+     * range.fit(99) -->  64
+     * 
+ * + * @param element the element to check for, not null + * @return the minimum, the element, or the maximum depending on the element's location relative to the range + * @since 3.10 + */ + public T fit(final T element) { + Objects.requireNonNull(element); + if (isAfter(element)) { + return minimum; + } + if (isBefore(element)) { + return maximum; + } + return element; + } + + /** + *

Gets the range as a {@code String}.

+ * + *

The format of the String is '[min..max]'.

+ * + * @return the {@code String} representation of this range + */ + @Override + public String toString() { + if (toString == null) { + toString = "[" + minimum + ".." + maximum + "]"; + } + return toString; + } + + /** + *

Formats the receiver using the given format.

+ * + *

This uses {@link java.util.Formattable} to perform the formatting. Three variables may + * be used to embed the minimum, maximum and comparator. + * Use {@code %1$s} for the minimum element, {@code %2$s} for the maximum element + * and {@code %3$s} for the comparator. + * The default format used by {@code toString()} is {@code [%1$s..%2$s]}.

+ * + * @param format the format string, optionally containing {@code %1$s}, {@code %2$s} and {@code %3$s}, not null + * @return the formatted string, not null + */ + public String toString(final String format) { + return String.format(format, minimum, maximum, comparator); + } + +}