+ * Note:This method may be called concurrently by multiple threads if the analysis uses parallel execution + *
+ * + * + * @param groupId The library's group ID + * @param artifactId The library's artifact ID + */ + protected void beforeLibraryStart(String groupId, String artifactId) { + log.debug("Start processing library {}:{}", groupId, artifactId); + } + + /** + * Analysis lifecycle hook that is executed after a library has been processed, i.e., after all releases have been + * discovered and the analysis implementation has been executed. + *+ * Note:This method may be called concurrently by multiple threads if the analysis uses parallel execution + *
+ * + * @param groupId The library's group ID + * @param artifactId The library's artifact ID + * @param artifacts The set of artifacts for the given library + */ + protected void afterLibraryEnd(String groupId, String artifactId, List+ * Note:This method may be called concurrently by multiple threads if the analysis uses parallel execution + *
+ * + * @param groupId The library's group ID + * @param artifactId The library's artifact ID + * @param cause The exception that caused the failure + */ + protected void onVersionListError(String groupId, String artifactId, Exception cause){} + + /** + * Main analysis implementation. Defines how a single library shall be analyzed. All artifacts for a given + * library will have information annotated corresponding to the protected attributes' values. + * @param libraryGA The library GA tuple (i.e., the library name) + * @param releases A list of artifacts as ordered by Maven Central's metadata.xml file + */ + protected abstract void analyzeLibrary(String libraryGA, List+ * Retrieves a list of all class files contained in JAR file referenced by this ArtifactInformation. The class + * files are returned as represented by the OPAL framework. + *
+ *+ * Note: OPAL classes are not designed to be used in large scale analyses. Over time, OPAL's internal caches + * will continue to claim heap space that is never freed, so eventually the program will crash with an + * OutOfMemory error. There is currently no good way to avoid this, so use project instances only if you + * really need them. + *
+ * @return List of OPAL class file representations, or null if no JAR file was found + * @throws IOException If reading the JAR file fails + */ + public List+ * Retrieves all class files for the JAR referenced by this ArtifactInformation, and uses them to initialize an + * OPAL project instance. This instance can be used to conduct complex static program analyses. + *
+ *+ * Note: OPAL projects are not designed to be used in large scale analyses. Over time, OPAL's internal caches + * will continue to claim heap space that is never freed, so eventually the program will crash with an + * OutOfMemory error. There is currently no good way to avoid this, so use project instances only if you + * really need them. + *
+ * @return The OPAL project instance, or null if no JAR file was found + * @throws IOException If reading the JAR file fails + */ + public Project+ * Retrieves all class files for the JAR referenced by this ArtifactInformation, and uses them to initialize an + * OPAL project instance. This instance can be used to conduct complex static program analyses. + *
+ *+ * Note: OPAL projects are not designed to be used in large scale analyses. Over time, OPAL's internal caches + * will continue to claim heap space that is never freed, so eventually the program will crash with an + * OutOfMemory error. There is currently no good way to avoid this, so use project instances only if you + * really need them. + *
+ * @return The OPAL project instance, or null if no JAR file was found + * @param projectLogger A custom logger instance to configure the amount of output that is produced by OPAL. + * @throws IOException If reading the JAR file fails + */ + public Project
+ final Element body = document.getElementsByTag("body").first();
+ listElem = body == null ? null : body.getElementsByTag("pre").first();
+
+ if(listElem == null)
+ throw new IOException("Could not locate HTML element with ID 'contents' in version list HTML");
+ }
+
+ List versions = new ArrayList();
+ listElem.getElementsByTag("a").forEach( linkElem -> {
+ String hrefValue = linkElem.attr("href");
+
+ if(!hrefValue.equals("../") && hrefValue.endsWith("/")){
+ versions.add(hrefValue.substring(0, hrefValue.length() - 1));
+ }
+ });
+
+ return versions;
+ } else {
+ throw new IOException("Failed to open HTML connection to " + libraryBase);
+ }
+
+ } catch (URISyntaxException | FileNotFoundException x){
+ throw new IOException(x);
+ } finally {
+ if(content != null)
+ content.close();
+ }
+ }
+
+}
diff --git a/src/main/java/org/tudo/sse/resolution/releases/IReleaseListProvider.java b/src/main/java/org/tudo/sse/resolution/releases/IReleaseListProvider.java
index 01f56e8..812deb2 100644
--- a/src/main/java/org/tudo/sse/resolution/releases/IReleaseListProvider.java
+++ b/src/main/java/org/tudo/sse/resolution/releases/IReleaseListProvider.java
@@ -4,6 +4,7 @@
import java.io.IOException;
import java.util.List;
+import java.util.Objects;
/**
* Interface defining functionality to obtain a list of Maven Central releases (i.e. version numbers) for a given
@@ -19,6 +20,19 @@ public interface IReleaseListProvider {
* @return List of version numbers as ordered by the underlying source
* @throws IOException If a connection error occurs
*/
- List getReleases(ArtifactIdent identifier) throws IOException;
+ default List getReleases(ArtifactIdent identifier) throws IOException{
+ Objects.requireNonNull(identifier);
+ return getReleases(identifier.getGroupID(), identifier.getArtifactID());
+ }
+
+ /**
+ * Gets the ordered list of releases (i.e. version numbers) for the given library.
+ *
+ * @param groupId The library groupId
+ * @param artifactId The library artifactId
+ * @return List of version numbers as ordered by the underlying source
+ * @throws IOException If a connection error occurs
+ */
+ List getReleases(String groupId, String artifactId) throws IOException;
}
diff --git a/src/main/java/org/tudo/sse/semver/MavenVersionRangeParser.java b/src/main/java/org/tudo/sse/semver/MavenVersionRangeParser.java
new file mode 100644
index 0000000..062b45d
--- /dev/null
+++ b/src/main/java/org/tudo/sse/semver/MavenVersionRangeParser.java
@@ -0,0 +1,107 @@
+package org.tudo.sse.semver;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Parser for semantic version ranges as specified by the Maven build system. See the specification
+ * for details on the supported range types and syntaxes.
+ *
+ * @author Johannes Düsing
+ */
+public class MavenVersionRangeParser {
+
+ private static final char START_INCLUSIVE = '[';
+ private static final char END_INCLUSIVE = ']';
+
+ private static final char START_EXCLUSIVE = '(';
+ private static final char END_EXCLUSIVE = ')';
+
+ private static final char RANGE_SEPARATOR = ',';
+
+ private MavenVersionRangeParser(){}
+
+ /**
+ * Parses the given textual representation of a Maven version range into the corresponding version range object.
+ * @param value Value to parse
+ * @return Parsed version range, if successful
+ * @throws SemanticVersionParsingException If either the range syntax or the version number syntax were invalid.
+ */
+ public static SemanticVersionRange parseRange(String value) throws SemanticVersionParsingException {
+ final char[] chars = value.toCharArray();
+ final StringBuilder currentValue = new StringBuilder();
+
+ boolean inRange = false;
+ boolean sawSeparator = false;
+ boolean lowerInclusive = false;
+ SemanticVersionNumber lowerBound = null;
+
+ List ranges = new ArrayList<>();
+
+ for(int i = 0; i < chars.length; i++){
+ char currentChar = chars[i];
+
+ // Ignore whitespaces
+ if(Character.isWhitespace(currentChar)){
+ continue;
+ }
+
+ if(!inRange){
+ if(currentChar == START_INCLUSIVE){
+ inRange = true;
+ lowerInclusive = true;
+ } else if(currentChar == START_EXCLUSIVE){
+ inRange = true;
+ lowerInclusive = false;
+ } else if(currentChar != RANGE_SEPARATOR){
+ throw new SemanticVersionParsingException(value, "Expected begin of new range or range separator but got " + currentChar, i);
+ }
+ } else {
+ if(currentChar == RANGE_SEPARATOR){
+ String lowerBoundStr = currentValue.toString();
+ currentValue.setLength(0);
+
+ sawSeparator = true;
+
+ if(lowerBoundStr.isBlank()) lowerBound = null;
+ else lowerBound = SemanticVersionNumber.parse(lowerBoundStr);
+ } else if(currentChar == END_INCLUSIVE || currentChar == END_EXCLUSIVE){
+ String upperBoundStr = currentValue.toString();
+ currentValue.setLength(0);
+
+ SemanticVersionNumber upperBound = null;
+ if(!upperBoundStr.isBlank()) upperBound = SemanticVersionNumber.parse(upperBoundStr);
+ boolean upperInclusive = (currentChar == END_INCLUSIVE);
+
+ // If we do not see a separator (e.g. range '[1.0]'), we have a hard requirement for one specific
+ // version. This means lower and upper bound are equal, and both bounds must be inclusive
+ if(!sawSeparator){
+ lowerBound = upperBound;
+
+ if(!lowerInclusive || !upperInclusive){
+ throw new SemanticVersionParsingException(value, "Hard requirements for one specific version must have inclusive range delimiters.", i);
+ }
+ }
+
+ SimpleSemanticVersionRange range = new SimpleSemanticVersionRange(lowerBound, lowerInclusive, upperBound, upperInclusive);
+ ranges.add(range);
+
+ inRange = false;
+ sawSeparator = false;
+ } else {
+ currentValue.append(currentChar);
+ }
+ }
+ }
+
+ if(inRange)
+ throw new SemanticVersionParsingException(value, "Input ended with range not being closed");
+
+ if(ranges.isEmpty())
+ throw new SemanticVersionParsingException(value, "No ranges found in expression: " + value);
+ else if(ranges.size() == 1)
+ return ranges.get(0);
+ else
+ return new MultiPartSemanticVersionRange(ranges);
+ }
+}
diff --git a/src/main/java/org/tudo/sse/semver/MultiPartSemanticVersionRange.java b/src/main/java/org/tudo/sse/semver/MultiPartSemanticVersionRange.java
new file mode 100644
index 0000000..5bee54c
--- /dev/null
+++ b/src/main/java/org/tudo/sse/semver/MultiPartSemanticVersionRange.java
@@ -0,0 +1,46 @@
+package org.tudo.sse.semver;
+
+import java.util.List;
+
+/**
+ * Implementation of composite semantic version ranges, which consist of multiple "smaller" range specifications. Those
+ * ranges are concatenated in a way that this composite range is the union of all partial ranges - if a given version
+ * number is contained in at least one of the partial ranges, it is also contained in this range.
+ *
+ * @author Johannes Düsing
+ */
+public class MultiPartSemanticVersionRange implements SemanticVersionRange {
+
+ private final List partialRanges;
+
+ /**
+ * Creates a new range with the given list of partial ranges. Containment of any given version number is checked
+ * in the same order in which partial ranges a provided here.
+ *
+ * @param ranges List of partial ranges
+ */
+ public MultiPartSemanticVersionRange(List ranges){
+ this.partialRanges = ranges;
+ }
+
+ @Override
+ public boolean contains(SemanticVersionNumber number) {
+ for(SemanticVersionRange range : partialRanges){
+ if(range.contains(number)) return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString(){
+ StringBuilder sb = new StringBuilder();
+
+ for(int i = 0; i < partialRanges.size(); i++){
+ SemanticVersionRange range = partialRanges.get(i);
+ sb.append(range.toString());
+ if(i != partialRanges.size()-1) sb.append(".");
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/org/tudo/sse/semver/SemanticVersionNumber.java b/src/main/java/org/tudo/sse/semver/SemanticVersionNumber.java
new file mode 100644
index 0000000..496a5f2
--- /dev/null
+++ b/src/main/java/org/tudo/sse/semver/SemanticVersionNumber.java
@@ -0,0 +1,320 @@
+package org.tudo.sse.semver;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Class representing a version number as defined by the semantic versioning 2.0.0 standard. See this webpage for
+ * the formal definition. This class implements all comparison logic for version numbers as defined by the standard.
+ *
+ *
+ * Please Note: Build Metadata can be attached to semantic version numbers, but is irrelevant when comparing numbers.
+ * This is explicitly specified in the standard. This class adheres to those definitions and ignores build metadata when
+ * comparing numbers (compareTo) or checking for equality (equals, hashcode).
+ *
+ *
+ *
+ * @author Johannes Düsing
+ */
+public class SemanticVersionNumber implements Comparable {
+
+ private final int majorVersion;
+ private final int minorVersion;
+ private final int patchVersion;
+
+ private final String preReleaseData;
+ private final String buildMetadata;
+
+ private List _parsedPreReleases = null;
+
+ /**
+ * Creates a new semantic version number with the given major, minor and patch version.
+ * @param majorVersion Major version of this number
+ * @param minorVersion Minor version of this number
+ * @param patchVersion Patch version of this number
+ */
+ public SemanticVersionNumber(int majorVersion, int minorVersion, int patchVersion) {
+ this(majorVersion, minorVersion, patchVersion, null);
+ }
+
+ /**
+ * Creates a new semantic version number with the given major, minor and patch versions, as well as a prerelease string.
+ * @param majorVersion Major version of this number
+ * @param minorVersion Minor version of this number
+ * @param patchVersion Patch version of this number
+ * @param preReleaseData Prelease identifier of this number
+ */
+ public SemanticVersionNumber(int majorVersion, int minorVersion, int patchVersion, String preReleaseData) {
+ this(majorVersion, minorVersion, patchVersion, preReleaseData, null);
+ }
+
+ /**
+ * Creates a new semantic version number with the given major, minor and patch versions, as well as a prerelease and build identifier.
+ * @param majorVersion Major version of this number
+ * @param minorVersion Minor version of this number
+ * @param patchVersion Patch version of this number
+ * @param preReleaseData Prelease identifier of this number
+ * @param buildMetadata The build metadata identifier of this number - not relevant for comparisons
+ */
+ public SemanticVersionNumber(int majorVersion, int minorVersion, int patchVersion, String preReleaseData, String buildMetadata) {
+ this.majorVersion = majorVersion;
+ this.minorVersion = minorVersion;
+ this.patchVersion = patchVersion;
+
+ this.preReleaseData = preReleaseData;
+ this.buildMetadata = buildMetadata;
+ }
+
+ /**
+ * Get this number's major version.
+ * @return The major version
+ */
+ public int getMajorVersion(){
+ return this.majorVersion;
+ }
+
+ /**
+ * Get this number's minor version.
+ * @return The minor version
+ */
+ public int getMinorVersion(){
+ return this.minorVersion;
+ }
+
+ /**
+ * Get this number's patch version.
+ * @return The patch version
+ */
+ public int getPatchVersion(){
+ return this.patchVersion;
+ }
+
+ /**
+ * Checks whether this semantic version number includes a prerelease identifier.
+ * @return True if there is a prerelease identifier, false otherwise
+ */
+ public boolean hasPreRelease(){
+ return this.preReleaseData != null;
+ }
+
+ /**
+ * Checks whether this semantic version number includes build metadata.
+ * @return True if there is metadata, false otherwise
+ */
+ public boolean hasBuildMetadata(){
+ return this.buildMetadata != null;
+ }
+
+ /**
+ * Get this number's prerelease identifier, if available.
+ * @return The prerelease identifier, or null, if none is available.
+ */
+ public String getPreRelease() {
+ return this.preReleaseData;
+ }
+
+ /**
+ * Get this number's build metadata, if available.
+ * @return The build metadata, or null, if none is available.
+ */
+ public String getBuildMetadata() {
+ return this.buildMetadata;
+ }
+
+ @Override
+ public final int compareTo(SemanticVersionNumber other){
+ // The precedence of semantic version V2 numbers is defined here: https://semver.org/#spec-item-11
+ // Note that build metadata is irrelevant to precedence
+ // Major version takes precedence
+ if(this.majorVersion < other.majorVersion) return -1;
+ else if(this.majorVersion > other.majorVersion) return 1;
+ else {
+ // Minor version is second deciding factor
+ if(this.minorVersion < other.minorVersion) return -1;
+ else if(this.minorVersion > other.minorVersion) return 1;
+ else {
+ // Patch version is third deciding factor
+ if(this.patchVersion < other.patchVersion) return -1;
+ else if(this.patchVersion > other.patchVersion) return 1;
+ else {
+ // If major, minor and patch are equal, we proceed as follows:
+ // - if both versions have no prerelease data, they are equal
+ // - if one version has prerelease data and the other has not, prerelease data takes *lower* precedence, i.e. 1.0.0-alpha < 1.0.0
+ if(!this.hasPreRelease() && !other.hasPreRelease()) return 0;
+ else if(!this.hasPreRelease() && other.hasPreRelease()) return 1;
+ else if(this.hasPreRelease() && !other.hasPreRelease()) return -1;
+ else {
+ // If both versions have prerelease data, we must parse this data. SemVer specifies that it may
+ // consist of dot-separated identifiers that are either numeric or textual.
+ List thisParts = this.getParsedPreReleaseParts();
+ List otherParts = other.getParsedPreReleaseParts();
+
+ // The first difference in prerelease parts decides precedence
+ int pos = 0;
+ while(pos < thisParts.size() && pos < otherParts.size()) {
+ int compareResult = thisParts.get(pos).compareTo(otherParts.get(pos));
+ if(compareResult != 0) return compareResult;
+ pos += 1;
+ }
+
+ // If we do not find a difference for indices valid in both lists: Fewer parts equal less precedence
+ return thisParts.size() - otherParts.size();
+ }
+ }
+ }
+ }
+ }
+
+ private List getParsedPreReleaseParts(){
+ if(this._parsedPreReleases == null) {
+ this._parsedPreReleases = this.parsePreReleaseData();
+ }
+
+ return this._parsedPreReleases;
+ }
+
+ private List parsePreReleaseData(){
+ List preReleaseParts = new ArrayList<>();
+
+ StringBuilder current = new StringBuilder();
+ for(int i = 0; i < preReleaseData.length(); i++){
+ char c = preReleaseData.charAt(i);
+ if(c == '.'){
+ String currValue = current.toString();
+
+ try {
+ int currInt = Integer.parseInt(currValue);
+ preReleaseParts.add(new PreReleasePart(currInt));
+ } catch(NumberFormatException ignored){
+ preReleaseParts.add(new PreReleasePart(currValue));
+ }
+ current.setLength(0);
+ } else {
+ current.append(c);
+ }
+ }
+
+ String currValue = current.toString();
+
+ try {
+ int currInt = Integer.parseInt(currValue);
+ preReleaseParts.add(new PreReleasePart(currInt));
+ } catch(NumberFormatException ignored){
+ preReleaseParts.add(new PreReleasePart(currValue));
+ }
+
+ return preReleaseParts;
+ }
+
+ /**
+ * Class representing a part of a prerelase identifier. The SemVer v2 spec allows prerelease identifiers to be composed
+ * of dot-separated parts, which can either be numbers or strings. This class implements their comparison logic
+ * in compliance with semantic versioning rules.
+ */
+ private static final class PreReleasePart implements Comparable{
+
+ private final String stringPart;
+ private final Integer intPart;
+
+ PreReleasePart(String value){
+ this.stringPart = value;
+ this.intPart = null;
+ }
+
+ PreReleasePart(int value){
+ this.stringPart = null;
+ this.intPart = value;
+ }
+
+ boolean isNum() {
+ return intPart != null;
+ }
+
+ int numValue(){
+ if(isNum()) return intPart;
+ else throw new IllegalStateException("Not a number");
+ }
+
+ boolean isString(){
+ return stringPart != null;
+ }
+
+ String stringValue(){
+ if(isString()) return stringPart;
+ else throw new IllegalStateException("Not a string");
+ }
+
+
+ @Override
+ public int compareTo(PreReleasePart other) {
+ // If both parts are numbers, they are compared numerically
+ if(this.isNum() && other.isNum()) return this.numValue() - other.numValue();
+ // Numeric identifiers have lower precedence than text
+ else if(this.isNum()) return -1;
+ else if(other.isNum()) return 1;
+ else return this.stringValue().compareTo(other.stringValue());
+ }
+ }
+
+ /**
+ * Attempts to create a new semantic version number by parsing the given string value. If the string does not represent
+ * a valid number according to the semantic versioning 2.0.0 standard, an exception is thrown.
+ * @param value The string value to parse
+ * @return The semantic version number if parsing was successful
+ * @throws SemanticVersionParsingException If the value was invalid
+ */
+ public static SemanticVersionNumber parse(String value) throws SemanticVersionParsingException {
+ return SemanticVersionParser.parseNumber(value);
+ }
+
+ /**
+ * Attempts to create a new semantic version number by parsing the given string value. If the string does not represent
+ * a valid number according to the semantic versioning 2.0.0 standard, an empty Optional is returned.
+ * @param value The string value to parse
+ * @return Optional value containing either the parsed number (if parsing was successful), or nothing
+ */
+ public static Optional tryParse(String value) {
+ try {
+ SemanticVersionNumber number = parse(value);
+ return Optional.of(number);
+ } catch (SemanticVersionParsingException svpx){
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public String toString(){
+ StringBuilder sb = new StringBuilder();
+ sb.append(majorVersion);
+ sb.append(".");
+ sb.append(minorVersion);
+ sb.append(".");
+ sb.append(patchVersion);
+
+ if(this.hasPreRelease()){
+ sb.append("-");
+ sb.append(preReleaseData);
+ }
+
+ if(this.hasBuildMetadata()){
+ sb.append("+");
+ sb.append(buildMetadata);
+ }
+
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(Object other){
+ if(!(other instanceof SemanticVersionNumber)) return false;
+
+ return this.compareTo((SemanticVersionNumber)other) == 0;
+ }
+
+ @Override
+ public int hashCode(){
+ return Objects.hash(majorVersion, minorVersion, patchVersion, preReleaseData);
+ }
+}
diff --git a/src/main/java/org/tudo/sse/semver/SemanticVersionParser.java b/src/main/java/org/tudo/sse/semver/SemanticVersionParser.java
new file mode 100644
index 0000000..524a29a
--- /dev/null
+++ b/src/main/java/org/tudo/sse/semver/SemanticVersionParser.java
@@ -0,0 +1,193 @@
+package org.tudo.sse.semver;
+
+class SemanticVersionParser {
+
+ static SemanticVersionNumber parseNumber(String value) throws SemanticVersionParsingException {
+ if(value.isBlank()) throw new SemanticVersionParsingException(value, "Semantic version cannot be empty", 0);
+
+ ParsingState state = ParsingState.NUMBER;
+ int parsingPosition = 0;
+ StringBuilder currentValue = new StringBuilder();
+
+ int currNumberPartIdx = 0;
+ int[] numberParts = new int[] {-1, 0, 0};
+
+ String preRelease = null;
+ String buildMetadata = null;
+
+ final char[] characters = value.toCharArray();
+
+ for(char currentChar: characters) {
+
+ switch(state){
+ case NUMBER:
+ if(currentChar != '.' && currentChar != '-' && currentChar != '+') {
+ currentValue.append(currentChar);
+ } else {
+ if(currentValue.length() == 0)
+ throw new SemanticVersionParsingException(value, "Numeric identifier must have at least one digit", parsingPosition);
+
+ final String numValue = currentValue.toString();
+ currentValue.setLength(0);
+
+ if(!isNumericIdentifier(numValue))
+ throw new SemanticVersionParsingException(value, "Not a valid numeric identifier", parsingPosition);
+
+ numberParts[currNumberPartIdx] = asInt(numValue, value, parsingPosition);
+
+ if(currentChar == '.'){
+ if(currNumberPartIdx == 2)
+ throw new SemanticVersionParsingException(value, "Semantic version cannot have more than three parts", parsingPosition);
+
+ currNumberPartIdx += 1;
+ } else if(currentChar == '-'){
+ state = ParsingState.PRERELEASE;
+ } else {
+ state = ParsingState.BUILDMETADATA;
+ }
+ }
+ break;
+
+ case PRERELEASE:
+ if(currentChar != '+'){
+ currentValue.append(currentChar);
+ } else {
+ final String preReleaseValue = currentValue.toString();
+ currentValue.setLength(0);
+
+ if(!isValidPreRelease(preReleaseValue))
+ throw new SemanticVersionParsingException(value, "Not a valid prerelease identifier", parsingPosition);
+
+ preRelease = preReleaseValue;
+
+ state = ParsingState.BUILDMETADATA;
+ }
+ break;
+ case BUILDMETADATA:
+ currentValue.append(currentChar);
+
+ break;
+ }
+
+ parsingPosition += 1;
+ }
+
+ String remaining = currentValue.toString();
+
+ switch(state){
+ case NUMBER:
+ numberParts[currNumberPartIdx] = asInt(remaining, value, parsingPosition);
+ break;
+ case PRERELEASE:
+ if(!isValidPreRelease(remaining))
+ throw new SemanticVersionParsingException(value, "Not a valid prerelease identifier", parsingPosition);
+
+ preRelease = remaining;
+ break;
+ case BUILDMETADATA:
+ if(!isValidBuild(remaining))
+ throw new SemanticVersionParsingException(value, "Not a valid build identifier", parsingPosition);
+
+ buildMetadata = remaining;
+ }
+
+ return new SemanticVersionNumber(numberParts[0], numberParts[1], numberParts[2], preRelease, buildMetadata);
+ }
+
+ private static boolean isValidBuild(String value){
+ if(value.isBlank()) return false;
+
+ final String[] parts = value.split("\\.");
+
+ for(String part: parts){
+ if(!isAlphanumericIdentifier(part) && !isDigits(part))
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean isValidPreRelease(String value){
+ if(value.isBlank()) return false;
+
+ final String[] parts = value.split("\\.");
+
+ for(String part: parts){
+ if(!isAlphanumericIdentifier(part) && !isNumericIdentifier(part))
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean isAlphanumericIdentifier(String value) {
+ if(value.isEmpty()) return false;
+
+ boolean hasNonDigit = false;
+
+ for(char c : value.toCharArray()) {
+ if(isNonDigit(c)) hasNonDigit = true;
+
+ if(!isIdentifierCharacter(c)) return false;
+ }
+
+ return hasNonDigit;
+ }
+
+ private static boolean isNumericIdentifier(String value){
+ if(value.isEmpty()) return false;
+ if(value.equals("0")) return true;
+
+ char[] chars = value.toCharArray();
+
+ if(!isPositiveDigit(chars[0])) return false;
+
+ for(int i = 1; i < chars.length; i++){
+ if(!isDigit(chars[i])) return false;
+ }
+
+ return true;
+ }
+
+ private static boolean isDigits(String value){
+ if(value.isEmpty()) return false;
+ for(char c : value.toCharArray()){
+ if(!isDigit(c)) return false;
+ }
+ return true;
+ }
+
+ private static boolean isIdentifierCharacter(char c){
+ return isDigit(c) || isNonDigit(c);
+ }
+
+ private static boolean isNonDigit(char c){
+ return c == '-' || isLetter(c);
+ }
+
+ private static boolean isLetter(char c) {
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+ }
+
+ private static boolean isPositiveDigit(char c){
+ return (c >= '1' && c <= '9');
+ }
+
+ private static boolean isDigit(char c) {
+ return (c >= '0' && c <= '9');
+ }
+
+ private static int asInt(String value, String number, int pos) throws SemanticVersionParsingException {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException nfx) {
+ throw new SemanticVersionParsingException(number, "Expected an integer", pos);
+ }
+ }
+
+ private enum ParsingState {
+ NUMBER,
+ PRERELEASE,
+ BUILDMETADATA
+ }
+}
diff --git a/src/main/java/org/tudo/sse/semver/SemanticVersionParsingException.java b/src/main/java/org/tudo/sse/semver/SemanticVersionParsingException.java
new file mode 100644
index 0000000..299123c
--- /dev/null
+++ b/src/main/java/org/tudo/sse/semver/SemanticVersionParsingException.java
@@ -0,0 +1,77 @@
+package org.tudo.sse.semver;
+
+/**
+ * Class representing an exception while parsing a semantic version number.
+ *
+ * @author Johannes Düsing
+ */
+public class SemanticVersionParsingException extends Exception {
+
+ /**
+ * Value that was being parsed
+ */
+ private final String parsedValue;
+
+ /**
+ * Message describing the error
+ */
+ private final String msg;
+
+ /**
+ * Position at which the error occurred
+ */
+ private final int parsingPosition;
+
+ /**
+ * Create a new parsing exception with the given description while parsing the given value.
+ * @param value The value that was being parsed
+ * @param description A description of the error that was encountered
+ */
+ public SemanticVersionParsingException(String value, String description) {
+ this(value, description, -1);
+ }
+
+ /**
+ * Create a new parsing exception with the given description, parsing position and parsed value.
+ * @param value The value that was being parsed
+ * @param description A description of the error that was encountered
+ * @param parsingPosition The parsing position at which the error occurred
+ */
+ public SemanticVersionParsingException(String value, String description, int parsingPosition) {
+ this.parsedValue = value;
+ this.msg = description;
+ this.parsingPosition = parsingPosition;
+ }
+
+ @Override
+ public String getMessage() {
+ StringBuilder sb = new StringBuilder("Error parsing value '");
+ sb.append(parsedValue);
+ sb.append("'");
+
+ if(parsingPosition != -1){
+ sb.append(" at position ").append(parsingPosition);
+ }
+
+ sb.append(" : ");
+ sb.append(msg);
+ return sb.toString();
+ }
+
+ /**
+ * Returns the position at which the error occurred in the original value.
+ * @return Parsing position
+ */
+ public int getParsingPosition() {
+ return this.parsingPosition;
+ }
+
+ /**
+ * Returns the value that was being parsed when the error occurred.
+ * @return The parsed value
+ */
+ public String getParsedValue(){
+ return this.parsedValue;
+ }
+
+}
diff --git a/src/main/java/org/tudo/sse/semver/SemanticVersionRange.java b/src/main/java/org/tudo/sse/semver/SemanticVersionRange.java
new file mode 100644
index 0000000..d11f764
--- /dev/null
+++ b/src/main/java/org/tudo/sse/semver/SemanticVersionRange.java
@@ -0,0 +1,17 @@
+package org.tudo.sse.semver;
+
+/**
+ * Interface that represents a range of semantic version numbers. The only requirement for implementations is that they
+ * must be able to decide whether any given semantic version number is contained within a range or not.
+ *
+ * @author Johannes Düsing
+ */
+public interface SemanticVersionRange {
+
+ /**
+ * Checks whether the given semantic version number falls within this range
+ * @param number The number to check
+ * @return True if the number is contained within this range, false otherwise
+ */
+ boolean contains(SemanticVersionNumber number);
+}
diff --git a/src/main/java/org/tudo/sse/semver/SimpleSemanticVersionRange.java b/src/main/java/org/tudo/sse/semver/SimpleSemanticVersionRange.java
new file mode 100644
index 0000000..e2202b3
--- /dev/null
+++ b/src/main/java/org/tudo/sse/semver/SimpleSemanticVersionRange.java
@@ -0,0 +1,152 @@
+package org.tudo.sse.semver;
+
+/**
+ * Simple implementation of semantic version ranges. A simple range is defined by two version numbers, a lower bound and
+ * an upper bound. Both are optional, in which case we have (semi-) open ranges. Bounds are either inclusive or exclusive.
+ *
+ * @author Johannes Düsing
+ */
+public class SimpleSemanticVersionRange implements SemanticVersionRange {
+
+ private final SemanticVersionNumber lowerBound;
+ private final SemanticVersionNumber upperBound;
+ private final boolean lowerBoundInclusive;
+ private final boolean upperBoundInclusive;
+
+ /**
+ * Creates a new simple semantic version range with the given bounds and their inclusive-specification. Use null to
+ * indicate that one (or both) bounds do not exist. If a bound is not set, its 'inclusive' value is ignored.
+ * @param lowerBound The lower bound for this range, or null if there is no lower bound.
+ * @param lowerBoundInclusive Whether the lower bound is inclusive. Ignored if there is no lower bound.
+ * @param upperBound The upper bound for this range, or null if there is no upper bound.
+ * @param upperBoundInclusive Whether the upper bound is inclusive. Ignored if there is no upper bound.
+ */
+ public SimpleSemanticVersionRange(SemanticVersionNumber lowerBound,
+ boolean lowerBoundInclusive,
+ SemanticVersionNumber upperBound,
+ boolean upperBoundInclusive) {
+ this.lowerBound = lowerBound;
+ this.lowerBoundInclusive = lowerBoundInclusive;
+ this.upperBound = upperBound;
+ this.upperBoundInclusive = upperBoundInclusive;
+ }
+
+ /**
+ * Creates a new simple semantic version range that is semi-open, i.e. a range that only has a lower bound.
+ * @param lowerBound Lower bound for this range.
+ * @param lowerBoundInclusive Whether the lower bound is inclusive.
+ * @return Semi-Open range with only a lower bound
+ */
+ public static SimpleSemanticVersionRange fromLowerBound(SemanticVersionNumber lowerBound, boolean lowerBoundInclusive) {
+ return new SimpleSemanticVersionRange(lowerBound, lowerBoundInclusive, null, false);
+ }
+
+ /**
+ * Creates a new simple semantic version range that is semi-open, i.e. a range that only has an upper bound.
+ * @param upperBound Upper bound for this range.
+ * @param upperBoundInclusive Whether the upper bound is inclusive.
+ * @return Semi-Open range with only an upper bound
+ */
+ public static SimpleSemanticVersionRange fromUpperBound(SemanticVersionNumber upperBound, boolean upperBoundInclusive) {
+ return new SimpleSemanticVersionRange(null, false, upperBound, upperBoundInclusive);
+ }
+
+ /**
+ * Checks whether this range has a lower bound.
+ * @return True if lower bound exists.
+ */
+ public boolean hasLowerBound() {
+ return lowerBound != null;
+ }
+
+ /**
+ * Checs whether this range has an upper bound.
+ * @return True if upper bound exists.
+ */
+ public boolean hasUpperBound() {
+ return this.upperBound != null;
+ }
+
+ /**
+ * Checks whether the lower bound of this range is inclusive.
+ * @return True if lower bound is inclusive, false if exclusive.
+ */
+ public boolean isLowerBoundInclusive() {
+ return lowerBoundInclusive;
+ }
+
+ /**
+ * Checks whether the upper bound of this range is inclusive.
+ * @return True if upper bound is inclusive, false if exclusive.
+ */
+ public boolean isUpperBoundInclusive() {
+ return upperBoundInclusive;
+ }
+
+ /**
+ * Gets the lower bound for this range.
+ * @return Lower bound, or null if non exists
+ */
+ public SemanticVersionNumber getLowerBound() {
+ return lowerBound;
+ }
+
+ /**
+ * Gets the upper bound for this range.
+ * @return Upper bound, or null if non exists.
+ */
+ public SemanticVersionNumber getUpperBound() {
+ return upperBound;
+ }
+
+ /**
+ * Checks whether this range represents a (Maven Central) hard requirement. A hard requirement is a special range
+ * where lower and upper bound are equal, and both are inclusive. This range contains exactly one version number.
+ * @return True if this range is a hard requirement
+ */
+ public boolean isHardRequirement(){
+ return this.lowerBound.equals(this.upperBound);
+ }
+
+ @Override
+ public boolean contains(SemanticVersionNumber number) {
+ if(hasLowerBound()){
+ int compareResult = number.compareTo(lowerBound);
+ if(compareResult < 0 || (!isLowerBoundInclusive() && compareResult == 0)) return false;
+ }
+
+ if(hasUpperBound()){
+ int compareResult = number.compareTo(upperBound);
+ if(compareResult > 0 || (!isUpperBoundInclusive() && compareResult == 0)) return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public String toString(){
+ StringBuilder sb = new StringBuilder();
+
+ if(isLowerBoundInclusive())
+ sb.append('[');
+ else
+ sb.append('(');
+
+ if(hasLowerBound())
+ sb.append(lowerBound.toString());
+
+ if(!isHardRequirement()){
+ sb.append(',');
+ sb.append(' ');
+ if(hasUpperBound())
+ sb.append(upperBound.toString());
+ }
+
+ if(isUpperBoundInclusive())
+ sb.append(']');
+ else
+ sb.append(')');
+
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/org/tudo/sse/utils/IndexIterator.java b/src/main/java/org/tudo/sse/utils/IndexIterator.java
index edb8425..fdb9a2d 100644
--- a/src/main/java/org/tudo/sse/utils/IndexIterator.java
+++ b/src/main/java/org/tudo/sse/utils/IndexIterator.java
@@ -1,9 +1,8 @@
package org.tudo.sse.utils;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
import org.apache.maven.index.reader.IndexReader;
-import org.tudo.sse.IndexWalker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.tudo.sse.model.ArtifactIdent;
import org.tudo.sse.model.index.Package;
import org.tudo.sse.model.index.IndexInformation;
@@ -13,6 +12,7 @@
import java.net.URI;
import java.util.Iterator;
import java.util.Map;
+import java.util.regex.Pattern;
/**
* This class creates an iterator for iterating over indexes and returning IndexArtifact objects.
@@ -22,6 +22,12 @@
*/
public class IndexIterator implements Iterator {
+
+ /**
+ * Pattern to split Lucene Index entries at
+ */
+ private static final String splitPattern = Pattern.quote("|");
+
private long index;
private final URI baseUri;
@@ -31,7 +37,7 @@ public class IndexIterator implements Iterator {
private IndexInformation nextArtifact;
private boolean prevHasNext;
- private static final Logger log = LogManager.getLogger(IndexIterator.class);
+ private static final Logger log = LoggerFactory.getLogger(IndexIterator.class);
/**
@@ -96,7 +102,7 @@ private void recoverConnectionReset() throws IOException{
* @see ArtifactIdent
*/
public ArtifactIdent processArtifactIdent(String gav) {
- String[] parts = gav.split(IndexWalker.splitPattern);
+ String[] parts = gav.split(splitPattern);
return new ArtifactIdent(parts[0], parts[1], parts[2]);
}
@@ -110,7 +116,7 @@ public ArtifactIdent processArtifactIdent(String gav) {
*/
public Package processPackage(String information, String checksum) {
if(information != null) {
- String[] parts = information.split(IndexWalker.splitPattern);
+ String[] parts = information.split(splitPattern);
return new Package(parts[0], Long.parseLong(parts[1]), Long.parseLong(parts[2]), Integer.parseInt(parts[3]), Integer.parseInt(parts[4]), Integer.parseInt(parts[5]), checksum);
}
return null;
@@ -141,7 +147,7 @@ private IndexInformation processIndex(Map item, ArtifactIdent id
//Create an artifact using the values found in the 'i' and '1' tags
if(iVal != null) {
- String[] parts = iVal.split(IndexWalker.splitPattern);
+ String[] parts = iVal.split(splitPattern);
Package tmpPackage = new Package(parts[0], Long.parseLong(parts[1]), Long.parseLong(parts[2]), Integer.parseInt(parts[3]), Integer.parseInt(parts[4]), Integer.parseInt(parts[5]), item.get("1"));
diff --git a/src/main/java/org/tudo/sse/utils/LibraryIndexIterator.java b/src/main/java/org/tudo/sse/utils/LibraryIndexIterator.java
new file mode 100644
index 0000000..58a17d6
--- /dev/null
+++ b/src/main/java/org/tudo/sse/utils/LibraryIndexIterator.java
@@ -0,0 +1,172 @@
+package org.tudo.sse.utils;
+
+import org.apache.maven.index.reader.ChunkReader;
+import org.apache.maven.index.reader.IndexReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Iterator over all unique library names in the Maven Central index. A library name is a tuple of GroupId:ArtifactId,
+ * separated by a colon.
+ */
+public class LibraryIndexIterator implements Iterator, AutoCloseable {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private final Set libraryHashesSeen;
+ private final Iterator