Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support arbitrary Java feature versions with JRE conditions #3931

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

sbrannen
Copy link
Member

@sbrannen sbrannen commented Aug 20, 2024

Overview

This PR currently serves as a proof of concept for supporting arbitrary Java feature versions in @EnabledOnJre, @EnabledForJreRange, and the JRE enum.

Related Issues


Definition of Done

This commit serves as a proof of concept for supporting arbitrary Java
feature numbers in @⁠EnabledOnJre, @⁠EnabledForJreRange, and the JRE enum.

See junit-team#3930
@sbrannen
Copy link
Member Author

The current generated source for JRE is as follows.

public enum JRE {

	/**
	 * Java 8.
	 */
	JAVA_8(8),

	/**
	 * Java 9.
	 */
	JAVA_9(9),

	/**
	 * Java 10.
	 */
	JAVA_10(10),

	/**
	 * Java 11.
	 */
	JAVA_11(11),

	/**
	 * Java 12.
	 *
	 * @since 5.4
	 */
	@API(status = STABLE, since = "5.4")
	JAVA_12(12),

	/**
	 * Java 13.
	 *
	 * @since 5.4
	 */
	@API(status = STABLE, since = "5.4")
	JAVA_13(13),

	/**
	 * Java 14.
	 *
	 * @since 5.5
	 */
	@API(status = STABLE, since = "5.5")
	JAVA_14(14),

	/**
	 * Java 15.
	 *
	 * @since 5.6
	 */
	@API(status = STABLE, since = "5.6")
	JAVA_15(15),

	/**
	 * Java 16.
	 *
	 * @since 5.7
	 */
	@API(status = STABLE, since = "5.7")
	JAVA_16(16),

	/**
	 * Java 17.
	 *
	 * @since 5.7.1
	 */
	@API(status = STABLE, since = "5.7.1")
	JAVA_17(17),

	/**
	 * Java 18.
	 *
	 * @since 5.8.1
	 */
	@API(status = STABLE, since = "5.8.1")
	JAVA_18(18),

	/**
	 * Java 19.
	 *
	 * @since 5.9
	 */
	@API(status = STABLE, since = "5.9")
	JAVA_19(19),

	/**
	 * Java 20.
	 *
	 * @since 5.9
	 */
	@API(status = STABLE, since = "5.9")
	JAVA_20(20),

	/**
	 * Java 21.
	 *
	 * @since 5.9.2
	 */
	@API(status = STABLE, since = "5.9.2")
	JAVA_21(21),

	/**
	 * Java 22.
	 *
	 * @since 5.10
	 */
	@API(status = STABLE, since = "5.10")
	JAVA_22(22),

	/**
	 * Java 23.
	 *
	 * @since 5.11
	 */
	@API(status = STABLE, since = "5.11")
	JAVA_23(23),

	/**
	 * Java 24.
	 *
	 * @since 5.11
	 */
	@API(status = STABLE, since = "5.11")
	JAVA_24(24),

	/**
	 * A JRE version other than {@link #JAVA_8}, {@link #JAVA_9},
	 * {@link #JAVA_10}, {@link #JAVA_11}, {@link #JAVA_12},
	 * {@link #JAVA_13}, {@link #JAVA_14}, {@link #JAVA_15},
	 * {@link #JAVA_16}, {@link #JAVA_17}, {@link #JAVA_18},
	 * {@link #JAVA_19}, {@link #JAVA_20}, {@link #JAVA_21},
	 * {@link #JAVA_22}, {@link #JAVA_23}, or {@link #JAVA_24}.
	 */
	OTHER(Integer.MAX_VALUE);

	private static final Logger logger = LoggerFactory.getLogger(JRE.class);

	private static final int UNKNOWN_FEATURE_VERSION = -1;

	private static final int CURRENT_FEATURE_VERSION = determineCurrentFeatureVersion();

	private static final JRE CURRENT_VERSION = determineCurrentVersion(CURRENT_FEATURE_VERSION);

	private static int determineCurrentFeatureVersion() {
		String javaVersion = System.getProperty("java.version");
		boolean javaVersionIsBlank = StringUtils.isBlank(javaVersion);

		if (javaVersionIsBlank) {
			logger.debug(
				() -> "JVM system property 'java.version' is undefined. It is therefore not possible to detect Java 8.");
		}

		if (!javaVersionIsBlank && javaVersion.startsWith("1.8")) {
			return 8;
		}

		try {
			// java.lang.Runtime.version() is a static method available on Java 9+
			// that returns an instance of java.lang.Runtime.Version which has the
			// following method: public int major()
			Method versionMethod = Runtime.class.getMethod("version");
			Object version = ReflectionSupport.invokeMethod(versionMethod, null);
			Method majorMethod = version.getClass().getMethod("major");
			return (int) ReflectionSupport.invokeMethod(majorMethod, version);
		}
		catch (Exception ex) {
			logger.debug(ex, () -> "Failed to determine the current JRE version via java.lang.Runtime.Version.");
		}

		return UNKNOWN_FEATURE_VERSION;
	}

	private static JRE determineCurrentVersion(int currentFeatureVersion) {
		switch (currentFeatureVersion) {
			case UNKNOWN_FEATURE_VERSION:
				// null signals that the current JRE version is "unknown"
				return null;
			case 8:
				return JAVA_8;
			case 9:
				return JAVA_9;
			case 10:
				return JAVA_10;
			case 11:
				return JAVA_11;
			case 12:
				return JAVA_12;
			case 13:
				return JAVA_13;
			case 14:
				return JAVA_14;
			case 15:
				return JAVA_15;
			case 16:
				return JAVA_16;
			case 17:
				return JAVA_17;
			case 18:
				return JAVA_18;
			case 19:
				return JAVA_19;
			case 20:
				return JAVA_20;
			case 21:
				return JAVA_21;
			case 22:
				return JAVA_22;
			case 23:
				return JAVA_23;
			case 24:
				return JAVA_24;
			default:
				return OTHER;
		}
	}

	private final int featureVersion;

	private JRE(int featureVersion) {
		this.featureVersion = featureVersion;
	}

	/**
	 * @return {@code true} if <em>this</em> {@code JRE} is known to be the
	 * Java Runtime Environment version for the currently executing JVM or if
	 * the version is {@link #OTHER}
	 */
	public boolean isCurrentVersion() {
		return this == CURRENT_VERSION;
	}

	/**
	 * Get the feature version of <em>this</em> {@code JRE}.
	 *
	 * @return the feature version of this {@code JRE}, or
	 * {@link Integer#MAX_VALUE} if this {@code JRE} is {@link #OTHER}
	 *
	 * @since 5.12
	 */
	@API(status = EXPERIMENTAL, since = "5.12")
	public int featureVersion() {
		return this.featureVersion;
	}

	/**
	 * @return the {@link JRE} for the currently executing JVM, potentially
	 * {@link #OTHER}
	 *
	 * @since 5.7
	 */
	@API(status = STABLE, since = "5.7")
	public static JRE currentVersion() {
		return CURRENT_VERSION;
	}

	/**
	 * @return the feature version for the currently executing JVM, or
	 * {@code -1} to signal that the feature version is unknown
	 *
	 * @since 5.12
	 */
	@API(status = EXPERIMENTAL, since = "5.12")
	public static int currentFeatureVersion() {
		return CURRENT_FEATURE_VERSION;
	}

	/**
	 * @return {@code true} if the supplied feature version is known to be
	 * the Java Runtime Environment version for the currently executing JVM
	 * or if the supplied feature version is {@code -1} and the feature
	 * version of the current JVM is unknown
	 *
	 * @since 5.12
	 */
	@API(status = EXPERIMENTAL, since = "5.12")
	public static boolean isCurrentFeatureVersion(int featureVersion) {
		return featureVersion == CURRENT_FEATURE_VERSION;
	}

	static boolean isCurrentVersionWithinRange(JRE min, JRE max) {
		return EnumSet.range(min, max).contains(CURRENT_VERSION);
	}

	static boolean isCurrentVersionWithinRange(int min, int max) {
		return CURRENT_FEATURE_VERSION >= min && CURRENT_FEATURE_VERSION <= max;
	}

}

@sbrannen
Copy link
Member Author

As can be inferred from the demo tests in the documentation module, the following are supported with the current proof of concept.

  • @EnabledOnJre(featureVersions = 24)

  • @EnabledOnJre(featureVersions = {23, 24})

  • @EnabledOnJre(value = JAVA_21, featureVersions = {22, 23})

  • @EnabledForJreRange(minFeatureVersion = 25)

  • @EnabledForJreRange(minFeatureVersion = 20, maxFeatureVersion = 25)

  • @EnabledForJreRange(min = JAVA_21, maxFeatureVersion = 24)

Copy link

@wilkinsona wilkinsona left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extensibility of this looks good to me, @sbrannen. Thanks for looking at it.

* <p>Defaults to {@code -1} to signal that {@link #min()} should be used instead.
*
* @since 5.12
* @see #min()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be useful to @see JRE#featureVersion() here to connect the two in the reader's mind.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. I'll improve the Javadoc once we've determined if we want to implement this feature.

*/
JRE max() default JRE.OTHER;

/**
* Java Runtime Environment feature version which should be used as the lower

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linking "feature version" to java.lang.Runtime.Version.feature() would help to connect some dots. Perhaps you can't given your Java baseline?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was planning on documenting that more thoroughly, though I'm glad you made the connection.

We're currently invoking the deprecated major() method defensively via reflection for support on Java 9+, and I picked the "feature" terminology because of the new feature() method in Java 10+.

So I plan to explain that to some extent.

As for linking to the newer API, I think that actually might not be an issue, because I believe we are generating Javadoc with a JDK version later than JDK 8 (perhaps JDK 11 or later -- I'll have to check).

@sbrannen
Copy link
Member Author

The extensibility of this looks good to me, @sbrannen.

Glad you like it, and thanks for reviewing it, @wilkinsona! 👍

@sbrannen sbrannen changed the title Support arbitrary Java feature numbers with JRE conditions Support arbitrary Java feature versions with JRE conditions Aug 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants