blob: 91ee9003466837b3a8196668417f08f9e04338ae [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 freemarker.template;
import java.io.Serializable;
import java.util.Date;
import freemarker.template.utility.StringUtil;
/**
* Represents a version number plus the further qualifiers and build info. This is
* mostly used for representing a FreeMarker version number, but should also be able
* to parse the version strings of 3rd party libraries.
*
* @see Configuration#getVersion()
*
* @since 2.3.20
*/
public final class Version implements Serializable {
private final int major;
private final int minor;
private final int micro;
private final String extraInfo;
private final String originalStringValue;
private final Boolean gaeCompliant;
private final Date buildDate;
private final int intValue;
private volatile String calculatedStringValue; // not final because it's calculated on demand
private int hashCode; // not final because it's calculated on demand
/**
* @throws IllegalArgumentException if the version string is malformed
*/
public Version(String stringValue) {
this(stringValue, null, null);
}
/**
* @throws IllegalArgumentException if the version string is malformed
*/
public Version(String stringValue, Boolean gaeCompliant, Date buildDate) {
stringValue = stringValue.trim();
originalStringValue = stringValue;
int[] parts = new int[3];
String extraInfoTmp = null;
{
int partIdx = 0;
for (int i = 0; i < stringValue.length(); i++) {
char c = stringValue.charAt(i);
if (isNumber(c)) {
parts[partIdx] = parts[partIdx] * 10 + (c - '0');
} else {
if (i == 0) {
throw new IllegalArgumentException(
"The version number string " + StringUtil.jQuote(stringValue)
+ " doesn't start with a number.");
}
if (c == '.') {
char nextC = i + 1 >= stringValue.length() ? 0 : stringValue.charAt(i + 1);
if (nextC == '.') {
throw new IllegalArgumentException(
"The version number string " + StringUtil.jQuote(stringValue)
+ " contains multiple dots after a number.");
}
if (partIdx == 2 || !isNumber(nextC)) {
extraInfoTmp = stringValue.substring(i);
break;
} else {
partIdx++;
}
} else {
extraInfoTmp = stringValue.substring(i);
break;
}
}
}
if (extraInfoTmp != null) {
char firstChar = extraInfoTmp.charAt(0);
if (firstChar == '.' || firstChar == '-' || firstChar == '_') {
extraInfoTmp = extraInfoTmp.substring(1);
if (extraInfoTmp.length() == 0) {
throw new IllegalArgumentException(
"The version number string " + StringUtil.jQuote(stringValue)
+ " has an extra info section opened with \"" + firstChar + "\", but it's empty.");
}
}
}
}
extraInfo = extraInfoTmp;
major = parts[0];
minor = parts[1];
micro = parts[2];
intValue = calculateIntValue();
this.gaeCompliant = gaeCompliant;
this.buildDate = buildDate;
}
private boolean isNumber(char c) {
return c >= '0' && c <= '9';
}
public Version(int major, int minor, int micro) {
this(major, minor, micro, null, null, null);
}
/**
* Creates an object based on the {@code int} value that uses the same kind of encoding as {@link #intValue()}.
*
* @since 2.3.24
*/
public Version(int intValue) {
this.intValue = intValue;
this.micro = intValue % 1000;
this.minor = (intValue / 1000) % 1000;
this.major = intValue / 1000000;
this.extraInfo = null;
this.gaeCompliant = null;
this.buildDate = null;
originalStringValue = null;
}
public Version(int major, int minor, int micro, String extraInfo, Boolean gaeCompatible, Date buildDate) {
this.major = major;
this.minor = minor;
this.micro = micro;
this.extraInfo = extraInfo;
this.gaeCompliant = gaeCompatible;
this.buildDate = buildDate;
intValue = calculateIntValue();
originalStringValue = null;
}
private int calculateIntValue() {
return intValueFor(major, minor, micro);
}
static public int intValueFor(int major, int minor, int micro) {
return major * 1000000 + minor * 1000 + micro;
}
private String getStringValue() {
if (originalStringValue != null) return originalStringValue;
String calculatedStringValue = this.calculatedStringValue;
if (calculatedStringValue == null) {
synchronized (this) {
calculatedStringValue = this.calculatedStringValue;
if (calculatedStringValue == null) {
calculatedStringValue = major + "." + minor + "." + micro;
if (extraInfo != null) calculatedStringValue += "-" + extraInfo;
this.calculatedStringValue = calculatedStringValue;
}
}
}
return calculatedStringValue;
}
/**
* Contains the major.minor.micor numbers and the extraInfo part, not the other information.
*/
@Override
public String toString() {
return getStringValue();
}
/**
* The 1st version number, like 1 in "1.2.3".
*/
public int getMajor() {
return major;
}
/**
* The 2nd version number, like 2 in "1.2.3".
*/
public int getMinor() {
return minor;
}
/**
* The 3rd version number, like 3 in "1.2.3".
*/
public int getMicro() {
return micro;
}
/**
* The arbitrary string after the micro version number without leading dot, dash or underscore,
* like "RC03" in "2.4.0-RC03".
* This is usually a qualifier (RC, SNAPHOST, nightly, beta, etc) and sometimes build info (like
* date).
*/
public String getExtraInfo() {
return extraInfo;
}
/**
* @return The Google App Engine compliance, or {@code null}.
*/
public Boolean isGAECompliant() {
return gaeCompliant;
}
/**
* @return The build date if known, or {@code null}.
*/
public Date getBuildDate() {
return buildDate;
}
/**
* @return major * 1000000 + minor * 1000 + micro.
*/
public int intValue() {
return intValue;
}
@Override
public int hashCode() {
int r = hashCode;
if (r != 0) return r;
synchronized (this) {
if (hashCode == 0) {
final int prime = 31;
int result = 1;
result = prime * result + (buildDate == null ? 0 : buildDate.hashCode());
result = prime * result + (extraInfo == null ? 0 : extraInfo.hashCode());
result = prime * result + (gaeCompliant == null ? 0 : gaeCompliant.hashCode());
result = prime * result + intValue;
if (result == 0) result = -1; // 0 is reserved for "not set"
hashCode = result;
}
return hashCode;
}
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Version other = (Version) obj;
if (intValue != other.intValue) return false;
if (other.hashCode() != hashCode()) return false;
if (buildDate == null) {
if (other.buildDate != null) return false;
} else if (!buildDate.equals(other.buildDate)) {
return false;
}
if (extraInfo == null) {
if (other.extraInfo != null) return false;
} else if (!extraInfo.equals(other.extraInfo)) {
return false;
}
if (gaeCompliant == null) {
if (other.gaeCompliant != null) return false;
} else if (!gaeCompliant.equals(other.gaeCompliant)) {
return false;
}
return true;
}
}