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 sourceFiles = new ArrayList(); 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 = "" + original.toString() + ""; 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 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 = "" + original.toString() + ""; final String readmeNewText = "" + toString() + ""; 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 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; } }