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