/*
* 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;
}
}