/*
* sulky-modules - several general-purpose modules.
* Copyright (C) 2007-2015 Joern Huxhorn
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright 2007-2015 Joern Huxhorn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.huxhorn.sulky.version;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Version implementation according to <a href="http://semver.org/">Semantic Versioning 2.0.0</a>.
*
*
*/
public class SemanticVersion
implements Serializable, Comparable<SemanticVersion>
{
private static final long serialVersionUID = -2336778957623151857L;
private static final String VERSION_NUMBER_PATTERN_STRING = "((0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*))";
private static final String IDENTIFIER_ELEMENT_PATTERN_STRING = "([-a-zA-Z0-9]+)";
private static final String IDENTIFIER_PATTERN_STRING = "(" + IDENTIFIER_ELEMENT_PATTERN_STRING + "(\\." + IDENTIFIER_ELEMENT_PATTERN_STRING + ")*)";
private static final String SEMANTIC_VERSION_PATTERN_STRING =
VERSION_NUMBER_PATTERN_STRING
+ "(-" + IDENTIFIER_PATTERN_STRING + ")?"
+ "(\\+" + IDENTIFIER_PATTERN_STRING + ")?";
private static final Pattern SEMANTIC_VERSION_PATTERN = Pattern.compile(SEMANTIC_VERSION_PATTERN_STRING);
private static final Pattern IDENTIFIER_ELEMENT_PATTERN = Pattern.compile(IDENTIFIER_ELEMENT_PATTERN_STRING);
private static final int MAJOR_GROUP_INDEX = 2;
private static final int MINOR_GROUP_INDEX = 3;
private static final int PATCH_GROUP_INDEX = 4;
private static final int PRE_RELEASE_GROUP_INDEX = 6;
private static final int BUILD_METADATA_GROUP_INDEX = 11;
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private final long major;
private final long minor;
private final long patch;
private final String[] preRelease;
private final String[] buildMetadata;
private transient String versionString;
public static SemanticVersion parse(String versionString)
{
if(versionString == null)
{
throw new NullPointerException("versionString must not be null!");
}
Matcher matcher = SEMANTIC_VERSION_PATTERN.matcher(versionString);
if(!matcher.matches())
{
throw new IllegalArgumentException("'"+versionString+"' is not a valid semantic version!");
}
long major = Long.parseLong(matcher.group(MAJOR_GROUP_INDEX));
long minor = Long.parseLong(matcher.group(MINOR_GROUP_INDEX));
long patch = Long.parseLong(matcher.group(PATCH_GROUP_INDEX));
String preReleaseGroup = matcher.group(PRE_RELEASE_GROUP_INDEX);
String[] preRelease = EMPTY_STRING_ARRAY;
if(preReleaseGroup != null)
{
preRelease = preReleaseGroup.split("\\.");
}
String buildMetadataGroup = matcher.group(BUILD_METADATA_GROUP_INDEX);
String[] buildMetadata = EMPTY_STRING_ARRAY;
if(buildMetadataGroup != null)
{
buildMetadata = buildMetadataGroup.split("\\.");
}
return new SemanticVersion(major, minor, patch, preRelease, buildMetadata);
}
public SemanticVersion(long major, long minor, long patch)
{
this(major, minor, patch, EMPTY_STRING_ARRAY, EMPTY_STRING_ARRAY);
}
public SemanticVersion(long major, long minor, long patch, String[] preRelease)
{
this(major, minor, patch, preRelease, EMPTY_STRING_ARRAY);
}
public SemanticVersion(long major, long minor, long patch, String[] preRelease, String[] buildMetadata)
{
if(major < 0)
{
throw new IllegalArgumentException("major must not be negative!");
}
if(minor < 0)
{
throw new IllegalArgumentException("minor must not be negative!");
}
if(patch < 0)
{
throw new IllegalArgumentException("patch must not be negative!");
}
if(preRelease == null)
{
preRelease = EMPTY_STRING_ARRAY;
}
if(buildMetadata == null)
{
buildMetadata = EMPTY_STRING_ARRAY;
}
for (String current : preRelease)
{
if(current == null)
{
throw new IllegalArgumentException("preRelease must not contain null!");
}
if(!IDENTIFIER_ELEMENT_PATTERN.matcher(current).matches())
{
throw new IllegalArgumentException("preRelease identifier '"+current+"' is invalid!");
}
}
for (String current : buildMetadata)
{
if(current == null)
{
throw new IllegalArgumentException("buildMetadata must not contain null!");
}
if(!IDENTIFIER_ELEMENT_PATTERN.matcher(current).matches())
{
throw new IllegalArgumentException("buildMetadata identifier '"+current+"' is invalid!");
}
}
// ensure immutability
if(preRelease.length != 0)
{
String[] newArray = new String[preRelease.length];
System.arraycopy(preRelease, 0, newArray, 0, preRelease.length);
preRelease = newArray;
}
if(buildMetadata.length != 0)
{
String[] newArray = new String[buildMetadata.length];
System.arraycopy(buildMetadata, 0, newArray, 0, buildMetadata.length);
buildMetadata = newArray;
}
this.major = major;
this.minor = minor;
this.patch = patch;
this.preRelease = preRelease;
this.buildMetadata = buildMetadata;
}
public long getMajor()
{
return major;
}
public long getMinor()
{
return minor;
}
public long getPatch()
{
return patch;
}
public List<String> getPreRelease()
{
return Arrays.asList(preRelease);
}
public List<String> getBuildMetadata()
{
return Arrays.asList(buildMetadata);
}
private String generateString()
{
StringBuilder result = new StringBuilder();
result.append(major).append('.').append(minor).append('.').append(patch);
if(preRelease.length > 0)
{
result.append('-');
boolean first = true;
for (String current : preRelease)
{
if(first)
{
first = false;
}
else
{
result.append('.');
}
result.append(current);
}
}
if(buildMetadata.length > 0)
{
result.append('+');
boolean first = true;
for (String current : buildMetadata)
{
if(first)
{
first = false;
}
else
{
result.append('.');
}
result.append(current);
}
}
return result.toString();
}
@Override
public String toString()
{
if(versionString == null)
{
versionString = generateString();
}
return versionString;
}
public int compareTo(SemanticVersion other)
{
if(other == null)
{
throw new NullPointerException("other must not be null!");
}
if(major < other.major)
{
return -1;
}
if(major > other.major)
{
return 1;
}
if(minor < other.minor)
{
return -1;
}
if(minor > other.minor)
{
return 1;
}
if(patch < other.patch)
{
return -1;
}
if(patch > other.patch)
{
return 1;
}
if(preRelease.length == 0)
{
if(other.preRelease.length == 0)
{
return 0;
}
return 1;
}
if(other.preRelease.length == 0)
{
return -1;
}
int maxLength = Math.max(preRelease.length, other.preRelease.length);
for(int i=0; i<maxLength; i++)
{
String a;
String b;
if(i < preRelease.length)
{
a = preRelease[i];
}
else
{
return -1;
}
if(i < other.preRelease.length)
{
b = other.preRelease[i];
}
else
{
return 1;
}
boolean aIsNumber = true;
boolean bIsNumber = true;
long aAsNumber = 0;
long bAsNumber = 0;
try
{
aAsNumber = Long.parseLong(a);
}
catch(NumberFormatException ex)
{
aIsNumber = false;
}
try
{
bAsNumber = Long.parseLong(b);
}
catch(NumberFormatException ex)
{
bIsNumber = false;
}
if(aIsNumber)
{
if(bIsNumber)
{
if(aAsNumber == bAsNumber)
{
continue;
}
if(aAsNumber < bAsNumber)
{
return -1;
}
return 1;
}
return -1;
}
if(bIsNumber)
{
return 1;
}
// neither a nor b are numbers
int compared = a.compareTo(b);
if(compared == 0)
{
continue;
}
if(compared < 0)
{
return -1;
}
return 1;
}
return 0;
}
@SuppressWarnings("SimplifiableIfStatement")
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SemanticVersion that = (SemanticVersion) o;
if (major != that.major) return false;
if (minor != that.minor) return false;
if (patch != that.patch) return false;
return Arrays.equals(preRelease, that.preRelease) && Arrays.equals(buildMetadata, that.buildMetadata);
}
@Override
public int hashCode()
{
int result = (int) (major ^ (major >>> 32));
result = 31 * result + (int) (minor ^ (minor >>> 32));
result = 31 * result + (int) (patch ^ (patch >>> 32));
result = 31 * result + Arrays.hashCode(preRelease);
result = 31 * result + Arrays.hashCode(buildMetadata);
return result;
}
}