/****************************************************************************/
/* File: Semver.java */
/* Author: F. Georges - H2O Consulting */
/* Date: 2010-11-15 */
/* Tags: */
/* Copyright (c) 2010 Florent Georges (see end of file.) */
/* ------------------------------------------------------------------------ */
package org.expath.pkg.repo.deps;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.expath.pkg.repo.PackageException;
/**
* Represents a SemVer template, or a SemVer version number.
*
* See http://semver.org/. A template is like a version number, but it can
* have less parts. For instance "2.3" is a valid template, even though it
* is not a valid version number (because it does not have any patch number).
* This template matches any version number starting with "2.3", for instance
* "2.3.0", "2.3.99" and "2.3.4beta5".
*
* SemVer has released a new version of the spec, and the pre-release part
* (like "pre1" or "beta5") must now be preceded by a slash (it was forbidden
* before...) TODO: Change this class to accept both kinds. And adapt the
* Packaging spec too.
*
* @author Florent Georges
*/
public class Semver
{
public Semver(String semver)
throws PackageException
{
myParts = parse(semver);
myString = semver;
}
/**
* Does {@code rhs} (a SemVer version) match this SemVer template?
*/
public boolean matches(Semver rhs)
throws PackageException
{
int this_len = myParts.length;
int rhs_len = rhs.myParts.length;
// TODO: We should probably relax the rule to accept only two components
// in a semver (two components version numbers are very common, and we
// should be able to treat them as semvers). Maybe emit a warning or
// even an error based on a config option.
if ( rhs_len < 3 ) {
throw new PackageException("RHS is not a SemVer version: '" + rhs.myString + "'");
}
if ( rhs_len < this_len ) {
// if the template has a special string, but the RHS has not,
// then it does not match
return false;
}
for ( int i = 0; i < this_len; ++i ) {
String p1 = myParts[i];
String p2 = rhs.myParts[i];
if ( ! p1.equals(p2) ) {
// if one part is not equal, then it does not match
return false;
}
}
// if all parts are equal, then the version matches the template
return true;
}
/**
* Does {@code rhs} (a SemVer version) match this SemVer template as a minimum?
*
* Return true if {@code rhs} is equal or above this template.
*/
public boolean matchesMin(Semver rhs)
throws PackageException
{
int this_len = myParts.length;
int rhs_len = rhs.myParts.length;
int max = this_len == 4 ? 3 : this_len;
for ( int i = 0; i < max; ++i ) {
String p1 = myParts[i];
String p2 = rhs.myParts[i];
int cmp = compareNumbers(p1, p2);
if ( cmp < 0 ) {
return true;
}
if ( cmp > 0 ) {
return false;
}
}
if ( this_len < 3 ) { // template is only 1 or 2 parts-long
return true;
}
if ( this_len == 4 ) { // template has a special part
if ( rhs_len == 4 ) {
String p1 = myParts[3];
String p2 = rhs.myParts[3];
return p1.compareTo(p2) <= 0;
}
return true; // 1.0.0alpha is before 1.0.0
}
if ( rhs_len == 4 ) {
return false;
}
return true; // perfect 3 parts equality
}
/**
* Does {@code rhs} (a SemVer version) match this SemVer template as a maximum?
*
* Return true if {@code rhs} is equal or below this template.
*/
public boolean matchesMax(Semver rhs)
throws PackageException
{
int this_len = myParts.length;
int rhs_len = rhs.myParts.length;
int max = this_len == 4 ? 3 : this_len;
for ( int i = 0; i < max; ++i ) {
String p1 = myParts[i];
String p2 = rhs.myParts[i];
int cmp = compareNumbers(p1, p2);
if ( cmp < 0 ) {
return false;
}
if ( cmp > 0 ) {
return true;
}
}
if ( this_len < 3 ) { // template is only 1 or 2 parts-long
return true;
}
if ( this_len == 4 ) { // template has a special part
if ( rhs_len == 4 ) {
String p1 = myParts[3];
String p2 = rhs.myParts[3];
return p1.compareTo(p2) >= 0;
}
return false; // 1.0.0alpha is before 1.0.0
}
if ( rhs_len == 4 ) {
return true;
}
return true; // perfect 3 parts equality
}
private static int compareNumbers(String lhs, String rhs)
{
int lhs_len = lhs.length();
int rhs_len = rhs.length();
if ( lhs_len < rhs_len ) {
return -1;
}
if ( lhs_len > rhs_len ) {
return 1;
}
return lhs.compareTo(rhs);
}
/**
* Parse the different parts of a SemVer template.
*
* Has the package visibility, in order to be unit-tested. MUST NOT be
* called from the outside!
*/
static String[] parse(String semver)
throws PackageException
{
// TODO: Check the doc of split(): regex or not regex?, etc.
String[] parts = semver.split("\\.");
int len = parts.length;
if ( len > 3 ) {
parseError(semver, "too much version parts");
}
if ( len == 0 ) {
parseError(semver, "no version parts");
}
if ( ! NUMBER_RE.matcher(parts[0]).matches() ) {
parseError(semver, "first part is not a number");
}
if ( len == 1 ) {
return parts;
}
if ( ! NUMBER_RE.matcher(parts[1]).matches() ) {
parseError(semver, "second part is not a number");
}
if ( len == 2 ) {
return parts;
}
if ( NUMBER_RE.matcher(parts[2]).matches() ) {
return parts;
}
Matcher m = LAST_PART_RE.matcher(parts[2]);
if ( m.matches() ) {
String[] result = new String[4];
result[0] = parts[0];
result[1] = parts[1];
result[2] = m.group(1); // TODO: Do groups start at 1 ??? (with 0 = whole match)
result[3] = m.group(3);
return result;
}
parseError(semver, "third part is invalid");
// to make javac happy (it does not detect parseError() always throws an exception)
return null;
}
private static void parseError(String semver, String msg)
throws PackageException
{
throw new PackageException("Invalid SemVer pattern '" + semver + "': " + msg);
}
private static final Pattern NUMBER_RE = Pattern.compile("^([1-9][0-9]*)|0$");
// TODO: Check the regex against the SemVer spec...
private static final Pattern LAST_PART_RE =
Pattern.compile("^(([1-9][0-9]*)|0)([a-zA-Z][-a-zA-Z0-9]*)$");
private String myString; // for reporting purposes
private String[] myParts;
}
/* ------------------------------------------------------------------------ */
/* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS COMMENT. */
/* */
/* The contents of this file are subject to the Mozilla Public License */
/* Version 1.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.mozilla.org/MPL/. */
/* */
/* Software distributed under the License is distributed on an "AS IS" */
/* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See */
/* the License for the specific language governing rights and limitations */
/* under the License. */
/* */
/* The Original Code is: all this file. */
/* */
/* The Initial Developer of the Original Code is Florent Georges. */
/* */
/* Contributor(s): none. */
/* ------------------------------------------------------------------------ */