blob: 385ff28df39a2a8a67eb416f179dbce7f715cec2 [file] [log] [blame]
/*
* Copyright (c) 2010-2020 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.sdk.core.apidef;
import static java.util.stream.Collectors.joining;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.eclipse.scout.sdk.core.util.Ensure;
import org.eclipse.scout.sdk.core.util.Strings;
/**
* Represents an API version consisting of up to three integers followed by an optional {@link String} suffix.
*/
public class ApiVersion implements Comparable<ApiVersion> {
/**
* {@link ApiVersion} indicating the latest (newest) version of an API.
*/
public static final ApiVersion LATEST = new ApiVersion(Integer.MAX_VALUE);
/**
* Regex of a valid API pattern.
*/
public static final Pattern VERSION_PATTERN = Pattern.compile("(\\d+)(\\.(\\d*))?(\\.(\\d*))?(-.*)?");
private final int[] m_segments;
private final String m_suffix;
public ApiVersion(int... version) {
this(version, null);
}
public ApiVersion(String suffix, int... version) {
this(version, suffix);
}
public ApiVersion(int[] version, String suffix) {
Ensure.isTrue(Ensure.notNull(version).length > 0);
m_segments = Arrays.copyOf(version, version.length);
m_suffix = suffix;
}
/**
* Parses the value of an {@link ApiLevel} annotation on the given class.
*
* @param clazz
* The class whose {@link ApiLevel} annotation should be parsed.
* @return An {@link Optional} with the {@link ApiVersion} of the given class or an empty {@link Optional} if the
* given class is {@code null} or the annotation does not exist.
* @see #requireApiLevelOf(Class)
*/
public static Optional<ApiVersion> apiLevelOf(Class<?> clazz) {
return Optional.ofNullable(clazz)
.map(c -> c.getAnnotation(ApiLevel.class))
.map(ApiLevel::value)
.map(ApiVersion::new);
}
/**
* Parses the value of an {@link ApiLevel} annotation on the given class. This method fails if the given class does
* not have an {@link ApiLevel} annotation.
*
* @param clazz
* The class whose {@link ApiLevel} annotation should be parsed.
* @return The {@link ApiVersion} value of the annotation.
* @throws IllegalArgumentException
* if the clazz is {@code null} or the clazz does not have the {@link ApiLevel} annotation.
* @see #apiLevelOf(Class)
*/
public static ApiVersion requireApiLevelOf(Class<?> clazz) {
return apiLevelOf(clazz)
.orElseThrow(() -> Ensure.newFail("{} is missing required annotation '{}'.", clazz, ApiLevel.class.getName()));
}
/**
* Parses the given {@link CharSequence} into an {@link ApiVersion}.
*
* @param version
* The {@link CharSequence} to parse. It must fulfill {@link #VERSION_PATTERN} to be successfully parsed.
* @return The {@link ApiVersion} or an empty {@link Optional} if the given {@link CharSequence} cannot be parsed.
*/
public static Optional<ApiVersion> parse(CharSequence version) {
if (Strings.isBlank(version)) {
return Optional.empty();
}
var matcher = VERSION_PATTERN.matcher(version);
if (!matcher.find()) {
return Optional.empty();
}
var segments = Stream.of(1, 3, 5)
.map(matcher::group)
.filter(Strings::hasText)
.mapToInt(Integer::parseInt)
.toArray();
var suffix = matcher.group(6);
return Optional.of(new ApiVersion(segments, suffix));
}
/**
* @return The suffix {@link String} or {@code null} if this {@link ApiVersion} has no suffix.
*/
public String suffix() {
return m_suffix;
}
/**
* @return A copy of all number segments of the version. Never returns {@code null}.
*/
public int[] segments() {
return Arrays.copyOf(m_segments, m_segments.length);
}
/**
* @return A {@link String} representation of the version. Never returns {@code null}.
*/
public String asString() {
var numberPart = Arrays.stream(m_segments)
.mapToObj(Integer::toString)
.collect(joining("."));
var suffix = suffix();
if (Strings.isEmpty(suffix)) {
return numberPart;
}
return numberPart + suffix;
}
@Override
public String toString() {
return ApiVersion.class.getSimpleName() + " " + asString();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
var that = (ApiVersion) o;
return Arrays.equals(m_segments, that.m_segments)
&& Objects.equals(m_suffix, that.m_suffix);
}
@Override
public int hashCode() {
return 31 * Arrays.hashCode(m_segments)
+ (m_suffix != null ? m_suffix.hashCode() : 0);
}
@Override
public int compareTo(ApiVersion o) {
if (o == null) {
return 1;
}
if (this == o) {
return 0;
}
// also compare missing segments. missing segments are treated as zero
var numSegments = Math.max(o.m_segments.length, m_segments.length);
for (var i = 0; i < numSegments; i++) {
var me = positionValue(m_segments, i);
var other = positionValue(o.m_segments, i);
var dif = Integer.compare(me, other);
if (dif != 0) {
return dif;
}
}
return Comparator.nullsFirst(String::compareTo).compare(suffix(), o.suffix());
}
private static int positionValue(int[] arr, int index) {
if (index >= arr.length) {
return 0;
}
return arr[index];
}
}