JavaBuilder/src/dorkbox/BuildVersion.java

523 lines
17 KiB
Java

package dorkbox;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import dorkbox.build.util.BuildLog;
import dorkbox.build.util.wildcard.Paths;
import dorkbox.util.FileUtil;
import dorkbox.util.IO;
import dorkbox.util.OS;
import dorkbox.util.Version;
/**
* Utility class to deal with getting version strings, and incrementing version numbers.
*
* CRITICAL: If the "version" modified the build file, the variable used in the build file CANNOT be `static final`! This is because the
* compiler will inline the value during compile-time, and we cannot modify it during runtime
*/
@SuppressWarnings("Convert2Diamond")
public
class BuildVersion {
private static
class IncrementingVersion extends Version {
private IncrementingVersion() {
// no-arg for serialization
super();
}
IncrementingVersion(final Version version) {
super(version);
}
IncrementingVersion(final String version) {
super(version);
}
/**
* Increments the MAJOR version and resets the MINOR/PATCH version to 0.
*
* @return the Version object, for chaining instructions
*/
Version incrementMajor() {
return increment(0);
}
/**
* Increments the MINOR version and resets the PATCH version to 0.
*
* @return the Version object, for chaining instructions
*/
Version incrementMinor() {
return increment(1);
}
/**
* Increments the specified version (as an array) and resets everything > index to 0.
*
* @return the Version object, for chaining instructions
*/
Version increment(int index) {
if (internalVersion.length < index) {
internalVersion = Arrays.copyOf(internalVersion, index);
}
internalVersion[index] = internalVersion[index]+1;
// if there are any indices "smaller" than the specified, we zero them out.
final int length = internalVersion.length;
if (length > index) {
for (int i = index+1; i < length; i++) {
internalVersion[i] = 0; // reset minor/patch/etc to 0
}
}
// have to re-calculate the String
StringBuilder s = new StringBuilder(this.version.length());
for (int i : internalVersion) {
s.append(Integer.toString(i)).append('.');
}
// remove the last .
s.deleteCharAt(s.length()-1);
version = s.toString();
return this;
}
}
private File file;
private IncrementingVersion original;
private IncrementingVersion current;
private IncrementingVersion anchored;
private File readme;
private List<File> sourceFiles = new ArrayList<File>();
private boolean ignoreSaves = false;
/**
* Gets the [MAJOR][MINOR] version from a string. The passed in string can start with letters/words, as the first digit is used. If an
* empty string is passed in, [0, 0] will be returned. If a string with a major, but no minor is passed in, [major, 0] will be
* returned.
*
* @return an new Version object, which has major/minor version info
*/
public static
BuildVersion get(final Class<?> clazz, final String version) {
return new BuildVersion(clazz, version);
}
private BuildVersion() {
// no-arg for serialization
}
@SuppressWarnings("IncompleteCopyConstructor")
public
BuildVersion(final BuildVersion other) {
this.current = new IncrementingVersion(other.original);
file = other.file;
readme = other.readme;
sourceFiles.addAll(other.sourceFiles);
ignoreSaves = other.ignoreSaves;
original = new IncrementingVersion(other.original); // make a copy
}
public
BuildVersion(final String version) {
this.current = new IncrementingVersion(version);
original = new IncrementingVersion(version);
file = null;
}
public
BuildVersion(final Class<?> clazz, final String version) {
this.current = new IncrementingVersion(version);
original = new IncrementingVersion(version);
if (clazz != null) {
final Paths javaFile = Builder.getJavaFile(clazz);
file = javaFile.getFiles()
.get(0);
}
else {
file = null;
}
}
/**
* Specifies that a readme file is ALSO a part of the versioning information
*
* @param readme the README.md file that also has version info in it (xml/maven format)
*/
public
BuildVersion readme(final File readme) {
this.readme = readme;
return this;
}
/**
* Specifies that a source-code file is ALSO a part of the versioning information
*
* @param name project name for getting module/project location information via the source file
* @param sourceRootPath the location on disk of the /src (root location of all source code) where the source file is located
* @param sourceFile the .java file (specified as a class file) that also has version info in it
*/
public
BuildVersion sourceFile(final String name, final String sourceRootPath, final Class<?> sourceFile) {
// register this module. This could be refactored out -- however it is best to do it this way so we don't duplicate code
Builder.registerModule(name, sourceRootPath);
try {
// now we get the location (based on the module info above) for this class file
final Paths source = Builder.getJavaFile(sourceFile);
this.sourceFiles.add(source.getFiles()
.get(0));
} catch (Exception e) {
BuildLog.println("Error getting sourceFile for class " + sourceFile.getClass());
e.printStackTrace();
}
return this;
}
/**
* Verifies that all of the version information on save() will be valid. Throws IOException if it was not OK
*/
public
void verify() throws IOException {
if (ignoreSaves) {
return;
}
// only saves the readme if it was included.
if (readme != null) {
final String readmeOrigText = "<version>" + original.toString() + "</version>";
validate(readme, null, readmeOrigText, original.toString());
}
// only saves the sourcefile if it was included.
if (!sourceFiles.isEmpty()) {
for (File sourceFile : sourceFiles) {
final String precedingText = "String getVersion() {";
final String readmeOrigText = "return \"" + original.toString() + "\";";
validate(sourceFile, precedingText, readmeOrigText, original.toString());
}
}
// now check the build file
final String origText = "BuildVersion version = BuildVersion.get(\"" + original.toString() + "\")";
validate(file, null, origText, original.toString());
}
private static
void validate(final File file, final String precedingText, final String origText, final String expectedVersion) throws IOException {
if (file == null) {
throw new IOException("Unable to save the version information if the calling class is not detected.");
}
List<String> strings = FileUtil.read(file, true);
boolean hasPrecedingText = precedingText != null && !precedingText.isEmpty();
boolean foundPrecedingText = false;
boolean found = false;
if (hasPrecedingText) {
for (String string : strings) {
if (string.contains(precedingText)) {
foundPrecedingText = true;
}
if (foundPrecedingText && string.contains(origText)) {
found = true;
break;
}
}
}
else {
for (String string : strings) {
// the source string (in the file) cannot be "final", because "final" (if static) is inlined by the compiler.
if (string.contains(origText)) {
found = true;
break;
}
}
}
if (!found) {
throw new IOException("Expected version string/info '" + expectedVersion + "' NOT FOUND in '" + file +
"'. Check spacing/formatting and try again.");
}
}
/**
* Saves this file (if specified) and the README.md file (if specified)
*/
public
BuildVersion save() {
if (original.toString()
.equals(toString())) {
// don't save anything if nothing has changed.
return this;
}
if (!ignoreSaves) {
// only saves the readme if it was included.
if (readme != null) {
final String readmeOrigText = "<version>" + original.toString() + "</version>";
final String readmeNewText = "<version>" + toString() + "</version>";
save(readme, null, readmeOrigText, readmeNewText);
}
// only saves the sourcefile if it was included.
if (!sourceFiles.isEmpty()) {
for (File sourceFile : sourceFiles) {
final String precedingText = "String getVersion() {";
final String readmeOrigText = "return \"" + original.toString() + "\";";
final String readmeNewText = "return \"" + toString() + "\";";
save(sourceFile, precedingText, readmeOrigText, readmeNewText);
}
}
final String origText = "BuildVersion version = BuildVersion.get(\"" + original.toString() + "\")";
final String newText = "BuildVersion version = BuildVersion.get(\"" + toString() + "\")";
save(file, null, origText, newText);
}
return this;
}
/**
* Saves this file, if there is a file specified.
*
* @param file this is the file we are rewriting
* @param precedingText can be NULL, but is the PRECEDING text to the orig text (in the event we want a more exact match)
* @param origText this is what the ORIGINAL text must be
* @param newText this is what the text will become
*/
private static
void save(final File file, String precedingText, String origText, String newText) {
if (file == null) {
throw new RuntimeException("Unable to save the version information if the calling class is not detected.");
}
try {
List<String> strings = FileUtil.read(file, true);
boolean hasPrecedingText = precedingText != null && !precedingText.isEmpty();
boolean foundPrecedingText = false;
boolean found = false;
if (hasPrecedingText) {
for (int i = 0; i < strings.size(); i++) {
String string = strings.get(i);
if (string.contains(precedingText)) {
foundPrecedingText = true;
}
if (foundPrecedingText && string.contains(origText)) {
string = string.replace(origText, newText);
strings.set(i, string);
found = true;
break;
}
}
}
else {
for (int i = 0; i < strings.size(); i++) {
String string = strings.get(i);
// it cannot be "final", because "final" (if static) is inlined by the compiler.
if (string.contains(origText)) {
string = string.replace(origText, newText);
strings.set(i, string);
found = true;
break;
}
}
}
if (!found) {
throw new RuntimeException("Expected version string/info NOT FOUND in '" + file +
"'. Check spacing/formatting and try again.");
}
// now write the strings back to the file
Writer output = null;
try {
output = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)));
// FileWriter always assumes default encoding is OK
int lastIndex = strings.size();
// remove all ending strings (except the one JUST BEFORE there is text)
for (int i = lastIndex - 1; i >= 0; i--) {
final String string = strings.get(i);
if (string == null || string.isEmpty()) {
lastIndex = i;
} else {
break;
}
}
// write all of the original args
for (int i = 0; i < lastIndex; i++) {
final String arg = strings.get(i);
output.write(arg);
output.write(OS.LINE_SEPARATOR);
}
// make sure there is a new line at the end of the file (so it's easier to read)
output.write(OS.LINE_SEPARATOR);
} finally {
IO.close(output);
}
} catch (IOException e) {
throw new RuntimeException("Unable to write file.", e);
}
}
/**
* Gets the [MAJOR][MINOR] version from a string. The passed in string can start with letters/words, as the first digit is used.
*
* @return an new Version object, which has major/minor/etc version info
*/
public static
BuildVersion get(String version) {
return get(getCallingClass(), version);
}
/**
* @return the original version. This is useful if version numbers were incremented during the build process
*/
public
Version getOriginal() {
return original;
}
private static Class getCallingClass() {
// java < 8, it is SIGNIFICANTLY faster to call sun.reflect.Reflection.getCallerClass
// java >= 8, Thread.stackTrace was fixed, so it is the now preferred method
if (OS.javaVersion < 8) {
Class<?> callerClass = sun.reflect.Reflection.getCallerClass(3);
if (callerClass == null) {
return null;
}
return callerClass;
} else {
StackTraceElement[] cause = Thread.currentThread().getStackTrace();
if (cause == null || cause.length < 3) {
return null;
}
StackTraceElement stackTraceElement = cause[3];
if (stackTraceElement == null) {
return null;
}
try {
return Class.forName(stackTraceElement.getClassName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
}
/**
* @return a copy of the current version. Any updates to the copy do not apply to the original.
*/
public
BuildVersion copy() {
return new BuildVersion(this);
}
/**
* @return true if this version number has been changed.
*/
public
boolean hasChanged() {
return !this.current.equals(original);
}
public
BuildVersion ignoreSaves() {
this.ignoreSaves = true;
return this;
}
/**
* Increments the MAJOR version and resets the MINOR/PATCH version to 0.
*
* @return the Version object, for chaining instructions
*/
public
Version incrementMajor() {
return this.current.incrementMajor();
}
/**
* Increments the MINOR version and resets the PATCH version to 0.
*
* @return the Version object, for chaining instructions
*/
public
Version incrementMinor() {
return this.current.incrementMinor();
}
@Override
public
String toString() {
return this.current.toString();
}
/**
* Anchored means that
* - anchored = original
* - original = copy of current, so reading files, etc, work AFTER the version was changed, but are not modified any further
*/
public
void anchor() {
this.anchored = this.original;
this.original = new IncrementingVersion(this.current);
}
public
Version getAnchored() {
return this.anchored;
}
}