diff --git a/utilities/buildutils/revisioninfo/edu/cornell/mannlib/vitro/utilities/revisioninfo/InfoResponseParser.java b/utilities/buildutils/revisioninfo/edu/cornell/mannlib/vitro/utilities/revisioninfo/InfoResponseParser.java new file mode 100644 index 000000000..dfaeb8a33 --- /dev/null +++ b/utilities/buildutils/revisioninfo/edu/cornell/mannlib/vitro/utilities/revisioninfo/InfoResponseParser.java @@ -0,0 +1,109 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.utilities.revisioninfo; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parse the response that we got from SVN info. + * + * Not thread-safe. + */ +public class InfoResponseParser { + private static final Pattern URL_PATTERN = Pattern.compile("URL: (\\S+)"); + private static final Pattern ROOT_PATTERN = Pattern + .compile("Repository Root: (\\S+)"); + + private static final String TRUNK_PREFIX = "/trunk"; + private static final String TAGS_PREFIX = "/tags/"; + private static final String BRANCHES_PREFIX = "/branches/"; + + private final String infoResponse; + private String path; + + public InfoResponseParser(String infoResponse) { + this.infoResponse = infoResponse; + } + + public String parse() { + try { + path = figurePath(); + System.err.println("path=" + path); + + if (isTrunkPath()) { + return "trunk"; + } else if (isTagPath()) { + return "tag " + getTagName(); + } else if (isBranchPath()) { + return "branch " + getBranchName(); + } else { + return null; + } + } catch (Exception e) { + System.err.println(e); // TODO + return null; + } + } + + private String figurePath() throws Exception { + if (infoResponse == null) { + throw new Exception("infoResponse is null."); + } + + String url = getUrlFromResponse(); + String root = getRootFromResponse(); + System.err.println("url=" + url); // TODO + System.err.println("root=" + root); // TODO + + if (!url.startsWith(root)) { + throw new Exception("url doesn't start with root."); + } + + return url.substring(root.length()); + } + + private String getUrlFromResponse() throws Exception { + return findNonEmptyMatch(URL_PATTERN, 1); + } + + private String getRootFromResponse() throws Exception { + return findNonEmptyMatch(ROOT_PATTERN, 1); + } + + private String findNonEmptyMatch(Pattern pattern, int groupIndex) + throws Exception { + Matcher matcher = pattern.matcher(infoResponse); + if (!matcher.find()) { + throw new Exception("no match with '" + pattern + "'."); + } + + String value = matcher.group(groupIndex); + if ((value == null) || (value.isEmpty())) { + throw new Exception("match with '" + pattern + "' is empty."); + } + + return value; + } + + private boolean isTrunkPath() { + return path.equals(TRUNK_PREFIX); + } + + private boolean isTagPath() { + return path.startsWith(TAGS_PREFIX); + } + + private String getTagName() { + return path.substring(TAGS_PREFIX.length()); + } + + private boolean isBranchPath() { + return path.startsWith(BRANCHES_PREFIX); + } + + private String getBranchName() { + return path.substring(BRANCHES_PREFIX.length()); + } + +} diff --git a/utilities/buildutils/revisioninfo/edu/cornell/mannlib/vitro/utilities/revisioninfo/ProcessRunner.java b/utilities/buildutils/revisioninfo/edu/cornell/mannlib/vitro/utilities/revisioninfo/ProcessRunner.java new file mode 100644 index 000000000..1e1ffb88b --- /dev/null +++ b/utilities/buildutils/revisioninfo/edu/cornell/mannlib/vitro/utilities/revisioninfo/ProcessRunner.java @@ -0,0 +1,161 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.utilities.revisioninfo; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A harness that runs a system-level command. + * + * No provision is made for standard input. + * + * The standard output and standard error streams are asynchronously read, so + * the sub-process will not block on full buffers. Warning: if either of these + * streams contain more data than can fit into a String, then we will have a + * problem. + */ +public class ProcessRunner { + private int returnCode; + private String stdOut = ""; + private String stdErr = ""; + private File workingDirectory; + + private final Map environmentAdditions = new HashMap(); + + /** Set the directory that the command will run in. */ + public void setWorkingDirectory(File workingDirectory) { + this.workingDirectory = workingDirectory; + } + + /** Add (or replace) any environment variable. */ + public void setEnvironmentVariable(String key, String value) { + this.environmentAdditions.put(key, value); + } + + /** + * Run the command. + * + * @param command + * a list containing the operating system program and its + * arguments. See + * {@link java.lang.ProcessBuilder#ProcessBuilder(List)}. + */ + public void run(List command) throws ProcessException { + try { + ProcessBuilder builder = new ProcessBuilder(command); + + if (workingDirectory != null) { + builder.directory(workingDirectory); + } + + if (!environmentAdditions.isEmpty()) { + builder.environment().putAll(this.environmentAdditions); + } + + Process process = builder.start(); + StreamEater outputEater = new StreamEater(process.getInputStream()); + StreamEater errorEater = new StreamEater(process.getErrorStream()); + + this.returnCode = process.waitFor(); + + outputEater.join(); + this.stdOut = outputEater.getContents(); + + errorEater.join(); + this.stdErr = errorEater.getContents(); + } catch (IOException e) { + throw new ProcessException("Exception when handling sub-process:", + e); + } catch (InterruptedException e) { + throw new ProcessException("Exception when handling sub-process:", + e); + } + } + + public int getReturnCode() { + return returnCode; + } + + public String getStdErr() { + return stdErr; + } + + public String getStdOut() { + return stdOut; + } + + /** + * A thread that reads an InputStream until it reaches end of file, then + * closes the stream. + */ + private static class StreamEater extends Thread { + private final InputStream stream; + + private final StringWriter contents = new StringWriter(); + + private final byte[] buffer = new byte[4096]; + + public StreamEater(InputStream stream) { + this.stream = stream; + this.start(); + } + + @Override + public void run() { + try { + int howMany = 0; + while (true) { + howMany = stream.read(buffer); + if (howMany > 0) { + contents.write(new String(buffer, 0, howMany)); + } else if (howMany == 0) { + Thread.yield(); + } else { + break; + } + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + stream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public String getContents() { + return contents.toString(); + } + } + + /** + * Indicates a problem when dealing with a spawned sub-process. + */ + public static class ProcessException extends Exception { + + public ProcessException() { + } + + public ProcessException(String message) { + super(message); + } + + public ProcessException(Throwable cause) { + super(cause); + } + + public ProcessException(String message, Throwable cause) { + super(message, cause); + } + + } + +} diff --git a/utilities/buildutils/revisioninfo/edu/cornell/mannlib/vitro/utilities/revisioninfo/RevisionInfoBuilder.java b/utilities/buildutils/revisioninfo/edu/cornell/mannlib/vitro/utilities/revisioninfo/RevisionInfoBuilder.java new file mode 100644 index 000000000..5940a44c0 --- /dev/null +++ b/utilities/buildutils/revisioninfo/edu/cornell/mannlib/vitro/utilities/revisioninfo/RevisionInfoBuilder.java @@ -0,0 +1,159 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.utilities.revisioninfo; + +import java.io.BufferedReader; +import java.io.EOFException; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import edu.cornell.mannlib.vitro.utilities.revisioninfo.ProcessRunner.ProcessException; + +/** + * Get release and revision information to display on screen. + * + * Ask Subversion for the information. If Subversion is available, and if this + * is a working directory, then we can build the info from the responses we get + * from "svn info" and "svnversion". + * + * If that doesn't work, read the information from the ".revisionInfo" file in + * the product directory. Presumably, that file was created when the source was + * exported from Subversion. + * + * If that doesn't work either, return something like this: + * "productName ~ unknown ~ unknown" + */ +public class RevisionInfoBuilder { + private static final String SVN_DIRECTORY_NAME = ".svn"; + private static final String[] SVNVERSION_COMMAND = { "svnversion" }; + private static final String[] SVN_INFO_COMMAND = { "svn", "info" }; + private static final String INFO_LINE_DELIMITER = " ~ "; + private static final String REVISION_INFO_FILENAME = ".revisionInfo"; + + private final String productName; + private final File productDirectory; + + public RevisionInfoBuilder(String[] args) { + if (args.length != 2) { + throw new IllegalArgumentException( + "RevisionInfoBuilder requires 2 arguments, not " + + args.length); + } + + productName = args[0]; + productDirectory = new File(args[1]); + + if (!productDirectory.isDirectory()) { + throw new IllegalArgumentException("Directory '" + + productDirectory.getAbsolutePath() + "' does not exist."); + } + } + + private String buildInfo() { + String infoLine; + infoLine = buildInfoFromSubversion(); + if (infoLine == null) { + infoLine = buildInfoFromFile(); + } + if (infoLine == null) { + infoLine = buildDummyInfo(); + } + return infoLine; + } + + private String buildInfoFromSubversion() { + if (!isThisASubversionWorkspace()) { + return null; + } + + String release = assembleReleaseNameFromSubversion(); + String revision = obtainRevisionLevelFromSubversion(); + System.err.println("release=" + release); // TODO + System.err.println("revision=" + revision); // TODO + return buildLine(release, revision); + } + + private boolean isThisASubversionWorkspace() { + File svnDirectory = new File(productDirectory, SVN_DIRECTORY_NAME); + return svnDirectory.isDirectory(); + } + + private String assembleReleaseNameFromSubversion() { + String infoResponse = runSubProcess(SVN_INFO_COMMAND); + return new InfoResponseParser(infoResponse).parse(); + } + + private String obtainRevisionLevelFromSubversion() { + String response = runSubProcess(SVNVERSION_COMMAND); + return (response == null) ? null : response.trim(); + } + + private String runSubProcess(String[] cmdArray) { + List command = Arrays.asList(cmdArray); + try { + ProcessRunner runner = new ProcessRunner(); + runner.setWorkingDirectory(productDirectory); + + runner.run(command); + + int rc = runner.getReturnCode(); + if (rc != 0) { + throw new ProcessRunner.ProcessException("Return code from " + + command + " was " + rc); + } + + String output = runner.getStdOut(); + // System.err.println(command + " response was '" + output + "'"); + return output; + } catch (ProcessException e) { + return null; + } + } + + private String buildInfoFromFile() { + try { + File revisionInfoFile = new File(productDirectory, + REVISION_INFO_FILENAME); + BufferedReader reader = new BufferedReader(new FileReader( + revisionInfoFile)); + + String release = reader.readLine(); + if (release == null) { + throw new EOFException("No release line in file."); + } + + String revision = reader.readLine(); + if (revision == null) { + throw new EOFException("No revision line in file."); + } + + return buildLine(release, revision); + } catch (IOException e) { + return null; + } + } + + private String buildDummyInfo() { + return buildLine("unknown", "unknown"); + } + + private String buildLine(String release, String revision) { + return productName + INFO_LINE_DELIMITER + release.trim() + + INFO_LINE_DELIMITER + revision.trim(); + } + + public static void main(String[] args) { + try { + System.out.println(new RevisionInfoBuilder(args).buildInfo()); + } catch (Exception e) { + e.printStackTrace(System.err); + System.err.println("usage: RevisionInfoBuilder [product_name] " + + "[product_directory] [supplied_values]"); + System.exit(1); + } + } + +}