IuComponentVersion.java

/*
 * Copyright © 2025 Indiana University
 * All rights reserved.
 *
 * BSD 3-Clause License
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * - Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 * 
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * 
 * - Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package edu.iu.type;

import java.util.HashMap;
import java.util.Hashtable;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * <strong>Refers</strong> to a {@link IuComponent component's}
 * <strong>version</strong>.
 * 
 * <p>
 * All {@link IuComponent components} are <strong>named</strong> and may be
 * <strong>referred to</strong> by <strong>version</strong>. <strong>Version
 * references</strong> are immutable, {@link Comparable comparable}, and
 * <em>must</em> implement {@link #hashCode()} and {@link #equals(Object)}.
 * <a href="https://semver.org/">Semantic Versioning</a> <em>must</em> be used
 * to ensure consistency across a variety of <strong>components</strong>.
 * </p>
 *
 * <p>
 * A {@link IuComponent component's} <strong>implementation version</strong> is
 * defined by its {@link IuComponent component archive}. A {@link IuComponent
 * dependency} <em>may</em> require a specific {@link #implementationVersion()
 * implementation version}, or a minimum {@link #specificationVersion()
 * specification version}, to be present on the {@link IuComponent component's
 * path}.
 * </p>
 * 
 * @see #implementationVersion()
 * @see #specificationVersion()
 */
public interface IuComponentVersion extends Comparable<IuComponentVersion> {

	/**
	 * Regular expression for validating a semantic version.
	 * 
	 * @see <a href="https://semver.org/">Semantic Versioning</a>
	 */
	static final Pattern SEMANTIC_VERSION_PATTERN = Pattern.compile(
			"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$");

	/**
	 * Gets the component <strong>name</strong>.
	 * 
	 * <p>
	 * The component <strong>name</strong> <em>should</em> be universally unique,
	 * and <em>must</em> be unique within the {@link IuComponent component's path}.
	 * The <strong>name</strong> is part of of the {@link IuComponent component's}
	 * version.
	 * </p>
	 * 
	 * @return name
	 */
	String name();

	/**
	 * Gets the <strong>implementation version</strong> of a {@link IuComponent
	 * component} or <strong>dependency</strong>.
	 * 
	 * <p>
	 * <em>Must</em> return a value for which
	 * {@link #SEMANTIC_VERSION_PATTERN}{@link Pattern#matcher(CharSequence)
	 * .matcher(implementationVersion())}{@link Matcher#matches() .matches()}
	 * returns {@code true}, or {@code null}.
	 * </p>
	 * 
	 * @return Full <a href="https://semver.org/">Semantic</a>
	 *         <strong>implementation version</strong> of the {@link IuComponent
	 *         component}.<br>
	 *         <em>May</em> be {@code null} when referring to a
	 *         <strong>dependency</strong> on the <strong>component's specification
	 *         version</strong>.<br>
	 *         <em>Must</em> {@link String#startsWith(String) start with}
	 *         {@code major() + '.' + minor() + '.'} when {@code non-null}.
	 */
	String implementationVersion();

	/**
	 * Gets the <strong>major</strong> version number as defined by
	 * <a href="https://semver.org/">Semantic Versioning</a>.
	 * 
	 * @return <strong>Major</strong> version; <em>must</em> be non-negative,
	 *         <em>should</em> be positive. Non-development applications
	 *         <em>may</em> reject <strong>versions</strong> with a major version of
	 *         {@code 0}.
	 */
	int major();

	/**
	 * Gets the <strong>minor</strong> version number as defined by
	 * <a href="https://semver.org/">Semantic Versioning</a>.
	 * 
	 * @return <strong>Minor</strong> version; <em>must</em> be non-negative
	 */
	int minor();

	/**
	 * Gets the <strong>specification version</strong> implied by this
	 * <strong>version reference</strong>.
	 * 
	 * <p>
	 * The <strong>specification version</strong> relates to the
	 * <strong>minor</strong> version of the <strong>implementation</strong>. For
	 * example, if the <strong>implementation version</strong> is {@code
	 * 1.2.34-SNAPSHOT}, then the <strong>specification version</strong> is
	 * {@code 1.2}.
	 * </p>
	 * 
	 * @return <strong>Specification version</strong> implied by this
	 *         <strong>version reference</strong>. For an <strong>implementation
	 *         version</strong>, the version of the <strong>implemented
	 *         specification</strong> is returned. For a <strong>specification
	 *         version</strong>, {@code this} is returned.
	 * @see #major()
	 * @see #minor()
	 */
	default IuComponentVersion specificationVersion() {
		var implementationVersion = implementationVersion();
		if (implementationVersion == null)
			return this;
		else
			return new IuComponentVersion() {
				@Override
				public String name() {
					return IuComponentVersion.this.name();
				}

				@Override
				public String implementationVersion() {
					return null;
				}

				@Override
				public int major() {
					return IuComponentVersion.this.major();
				}

				@Override
				public int minor() {
					return IuComponentVersion.this.minor();
				}

				@Override
				public int hashCode() {
					return Objects.hash(major(), minor(), name(), null);
				}

				@Override
				public boolean equals(Object obj) {
					if (this == obj)
						return true;
					if (obj == null)
						return false;
					if (!(obj instanceof IuComponentVersion))
						return false;

					IuComponentVersion other = (IuComponentVersion) obj;
					return major() == other.major() && minor() == other.minor() && Objects.equals(name(), other.name())
							&& other.implementationVersion() == null;
				}

				@Override
				public String toString() {
					return name() + '-' + major() + '.' + minor() + '+';
				}
			};
	}

	/**
	 * Determines if the <strong>version</strong> implied by {@code this}
	 * <strong>version reference</strong> meets the <strong>version</strong>
	 * required by another <strong>version reference</strong>.
	 * 
	 * <p>
	 * Note that {@link #compareTo(IuComponentVersion) comparing} two
	 * <strong>version references</strong> is not sufficient to determine if a
	 * <strong>dependency</strong> requirement is met since the {@link #major()
	 * major versions} <em>must</em> be identical in order to consider the
	 * requirement met.
	 * </p>
	 * 
	 * @param requiredVersion <strong>Reference</strong> to the required
	 *                        <strong>version</strong>.
	 * @return True if the {@code this} <strong>refers to</strong> a
	 *         <strong>version</strong> with the same {@link #name() name}, and
	 *         either the same {@link #implementationVersion() implementation
	 *         version} or same {@link #major() major version} and the same or
	 *         higher {@link #minor() minor version} as the <strong>version
	 *         reference</strong> passed as an argument to the
	 *         {@code requiredVersion} parameter.
	 */
	default boolean meets(IuComponentVersion requiredVersion) {
		if (!name().equals(requiredVersion.name()))
			return false;

		var requiredImplementationVersion = requiredVersion.implementationVersion();
		if (requiredImplementationVersion != null)
			return requiredImplementationVersion.equals(implementationVersion());
		else
			return major() == requiredVersion.major() && minor() >= requiredVersion.minor();
	}

	/**
	 * Compares two <strong>version references</strong>.
	 * 
	 * <ul>
	 * <li><strong>Version references</strong> with different {@link #name() names}
	 * <em>must not</em> return {@code 0}; otherwise, the return value enforces
	 * natural ordering of the {@link #name() component name}.</li>
	 * <li>An {@link #implementationVersion() implementation version} compared to
	 * another {@link #implementationVersion() implementation version} will return a
	 * value that:
	 * <ul>
	 * <li>enforces the numeric order of the {@link #major() major}, {@link #minor()
	 * minor}, and <a href="https://semver.org/">patch</a> version numbers</li>
	 * <li>enforces the natural order (i.e. +build.12, -SNAPSHOT, -alpha.2, -beta.1)
	 * of the combined <a href="https://semver.org/">build and pre-release
	 * identifiers</a></li>
	 * <li>is {@code 0} when {@link #implementationVersion()} strings are
	 * identical</li>
	 * </ul>
	 * </li>
	 * <li>A {@link #specificationVersion() specification version} compared to
	 * another <strong>version</strong> will return a value that enforces the
	 * numeric order of the {@link #major() major} and {@link #minor() minor}
	 * version numbers</li>
	 * <li>An {@link #implementationVersion() implementation version} is greater
	 * than a {@link #specificationVersion() specification version} with matching
	 * {@link #major() major} and {@link #minor() minor} version numbers</li>
	 * </ul>
	 * 
	 * <p>
	 * Note that comparing two <strong>version references</strong> is not sufficient
	 * to determine if a <strong>dependency</strong> requirement is
	 * {@link #meets(IuComponentVersion) met} since the {@link #major() major
	 * versions} <em>must</em> be identical in order to consider the requirement
	 * met.
	 * </p>
	 * 
	 * <p>
	 * Note that all <strong>versions</strong> are case-sensitive.
	 * </p>
	 * 
	 * {@inheritDoc}
	 */
	@Override
	default int compareTo(IuComponentVersion o) {
		int rv = name().compareTo(o.name());
		if (rv != 0)
			return rv;

		var iv1 = implementationVersion();
		var iv2 = o.implementationVersion();
		if (iv1 != null && iv1.equals(iv2))
			return 0;

		var x1 = major();
		var x2 = o.major();
		rv = Integer.compare(x1, x2);
		if (rv != 0)
			return rv;

		var y1 = minor();
		var y2 = o.minor();
		rv = Integer.compare(y1, y2);
		if (rv != 0)
			return rv;

		if (iv1 == iv2) // both null
			return 0;
		else if (iv1 == null)
			return -1;
		else if (iv2 == null)
			return 1;

		var m1 = SEMANTIC_VERSION_PATTERN.matcher(iv1);
		if (!m1.matches())
			throw new IllegalStateException();
		var z1 = Integer.parseInt(m1.group(3));

		var m2 = SEMANTIC_VERSION_PATTERN.matcher(iv2);
		if (!m2.matches())
			throw new IllegalStateException();
		var z2 = Integer.parseInt(m2.group(3));

		rv = Integer.compare(z1, z2);
		if (rv != 0)
			return rv;

		return iv1.compareTo(iv2);
	}

	/**
	 * <p>
	 * Note that when using {@link IuComponentVersion} in a {@link Set} or as a key
	 * in a {@link HashMap} or {@link Hashtable} that is important to be aware
	 * whether of whether or not the members and/or keys are expected to represent a
	 * {@link #specificationVersion() specification version}.
	 * </p>
	 * 
	 * {@inheritDoc}
	 */
	@Override
	int hashCode();

	/**
	 * <p>
	 * Two <strong>implementation versions</strong> are {@link Object#equals(Object)
	 * equal} if both {@link #name() name} and {@link #implementationVersion()
	 * implementation version} are equal. Two <strong>specification
	 * versions</strong> are {@link Object#equals(Object) equal} if {@link #name()},
	 * {@link #major()}, and {@link #minor()} are equal.
	 * </p>
	 * 
	 * <p>
	 * An <strong>implementation version</strong> is never
	 * {@link Object#equals(Object) equal} to a <strong>specification
	 * version</strong>.
	 * </p>
	 * 
	 * <p>
	 * Note that <strong>versions</strong> are case-sensitive.
	 * </p>
	 * 
	 * {@inheritDoc}
	 */
	@Override
	boolean equals(Object o);

}