/* * Copyright 2013 James Moger * * Licensed 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 org.moxie.ant; import java.io.File; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.tools.ant.DefaultLogger; import org.moxie.ArtifactVersion; import org.moxie.ArtifactVersion.NumberField; import org.moxie.Constants; import org.moxie.MoxieException; import org.moxie.Substitute; import org.moxie.Toolkit; import org.moxie.maxml.Maxml; import org.moxie.maxml.MaxmlException; import org.moxie.maxml.MaxmlMap; import org.moxie.utils.FileUtils; import org.moxie.utils.StringUtils; /** * MxVersion is used to build a release target where you want to publish a release * or to reset for the next development cycle. It is something like the Maven * versions plugin. * * @author James Moger * */ public class MxVersion extends MxTask { enum Stage { release, snapshot; } Stage stage; NumberField incrementNumber; File moxieFile; File releaseLog; boolean dryrun; public MxVersion() { super(); setTaskName("mx:version"); } public void setStage(String value) { for (Stage stage : Stage.values()) { if (value.equalsIgnoreCase(stage.name())) { this.stage = stage; break; } } if (stage == null) { throw new MoxieException(MessageFormat.format("Illegal stage {0}", value)); } } public void setIncrementNumber(String value) { for (NumberField number : NumberField.values()) { if (value.equalsIgnoreCase(number.name())) { this.incrementNumber = number; break; } } if (stage == null) { throw new MoxieException(MessageFormat.format("Illegal stage {0}", value)); } } public void setReleaselog(File file) { releaseLog = file; } public void setDryrun(boolean value) { this.dryrun = value; } @Override public void execute() { if (stage == null) { stage = Stage.release; } if (moxieFile == null) { moxieFile = new File(getProject().getBaseDir(), "build.moxie"); } if (releaseLog == null) { releaseLog = new File(getProject().getBaseDir(), "releases.moxie"); } MaxmlMap map = null; try { map = Maxml.parse(moxieFile); } catch (MaxmlException e) { throw new MoxieException(e); } String groupId = map.getString(Toolkit.Key.groupId.name(), ""); String artifactId = map.getString(Toolkit.Key.artifactId.name(), ""); String projectName = map.getString(Toolkit.Key.name.name(), null); if (StringUtils.isEmpty(projectName)) { projectName = groupId + ":" + artifactId; } String version; String releaseVersion = map.getString(Toolkit.Key.releaseVersion.name(), ""); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); Date releaseDate = map.getDate(Toolkit.Key.releaseDate.name(), null); String releaseDateStr = releaseDate == null ? null : df.format(releaseDate); Date buildDate = new Date(); String buildDateStr = buildDate == null ? null : df.format(buildDate); ArtifactVersion artifactVersion = new ArtifactVersion( map.getString(Constants.Key.version.name(), "0.0.0-SNAPSHOT")); Map<String, String> properties = new HashMap<String, String>(); List<Substitute> replacements = new ArrayList<Substitute>(); switch (stage) { case release: if (artifactVersion.isSnapshot()) { // update development snapshot info to release info releaseVersion = artifactVersion.setSnapshot(false).toString(); } else { // preserve major.minor.incremental, increment build number releaseVersion = artifactVersion.incrementBuildNumber().toString(); } title("Preparing RELEASE", releaseVersion); version = releaseVersion; releaseDate = buildDate; releaseDateStr = buildDateStr; replacements.add(new Substitute(Toolkit.Key.version.name(), releaseVersion)); replacements.add(new Substitute(Toolkit.Key.releaseVersion.name(), releaseVersion)); replacements.add(new Substitute(Toolkit.Key.releaseDate.name(), releaseDateStr)); updateDescriptor(replacements); // update release log properties.put(Toolkit.Key.name.projectId(), projectName); properties.put(Toolkit.Key.version.projectId(), releaseVersion); properties.put(Toolkit.Key.buildDate.projectId(), releaseDateStr); properties.put(Toolkit.Key.releaseVersion.projectId(), releaseVersion); properties.put(Toolkit.Key.releaseDate.projectId(), releaseDateStr); updateReleaseLog(stage, releaseLog, properties); break; case snapshot: // start a new minor version SNAPSHOT development cycle if (artifactVersion.isSnapshot()) { throw new MoxieException("The current version {0} is already a SNAPSHOT!", artifactVersion.toString()); } artifactVersion.setSnapshot(true); if (incrementNumber == null) { // increment minor version, if unspecified incrementNumber = NumberField.minor; } switch (incrementNumber) { case major: artifactVersion.incrementMajorVersion(); break; case minor: artifactVersion.incrementMinorVersion(); break; case incremental: artifactVersion.incrementIncrementalVersion(); break; default: artifactVersion.incrementMinorVersion(); break; } version = artifactVersion.toString(); title("Preparing SNAPSHOT", version); replacements.add(new Substitute(Toolkit.Key.version.name(), version)); updateDescriptor(replacements); // update release log updateReleaseLog(stage, releaseLog, properties); break; default: throw new MoxieException("Unknown stage \"{0}\"", stage); } // update Ant project properties getProject().setProperty(Toolkit.Key.name.projectId(), projectName); getProject().setProperty(Toolkit.Key.groupId.projectId(), groupId); getProject().setProperty(Toolkit.Key.artifactId.projectId(), artifactId); getProject().setProperty(Toolkit.Key.version.projectId(), version); getProject().setProperty(Toolkit.Key.releaseVersion.projectId(), releaseVersion); getProject().setProperty(Toolkit.Key.releaseDate.projectId(), releaseDateStr); getProject().setProperty(Toolkit.Key.buildDate.projectId(), buildDateStr); if (isShowTitle()) { // 3 is for "[] " setConsoleOffset(getTaskName().length() + 3 - DefaultLogger.LEFT_COLUMN_SIZE); } // share these paths for consumption by another task (e.g. mx:Commit) if (releaseLog.exists()) { sharePaths(moxieFile.getAbsolutePath(), releaseLog.getAbsolutePath()); } else { sharePaths(moxieFile.getAbsolutePath()); } } /** * Updates the Moxie descriptor with the specified key:value pairs. * * @param replacements */ protected void updateDescriptor(List<Substitute> replacements) { String content = FileUtils.readContent(moxieFile, "\n"); StringBuilder sb = new StringBuilder(); String [] lines = content.split("\n"); for (String line : lines) { String trimmed = line.trim(); for (Substitute replacement : replacements) { if (trimmed.startsWith(replacement.token)) { int start = line.indexOf(replacement.token) + replacement.token.length(); int colon = line.indexOf(':', start) + 1; line = line.substring(0, colon) + " " + replacement.value; } } sb.append(line).append('\n'); } if (dryrun) { System.out.println(sb.toString()); } else { FileUtils.writeContent(moxieFile, sb.toString()); } } /** * Updates the release log. * * @param replacements */ protected void updateReleaseLog(Stage stage, File file, Map<String, String> properties) { if (!file.exists()) { return; } int currentRelease = 0; String releaseBase = "r"; // identify the releaseBase, current release, and conditionally resolve properties StringBuilder sb = new StringBuilder(); String content = FileUtils.readContent(file, "\n"); for (String line : content.split("\n")) { if (Stage.release.equals(stage)) { // resolve properties for a release line = resolveProperties(line, properties); } // identify the current release and the releaseBase if (currentRelease == 0 && line.matches(getFieldRegex("release"))) { Pattern p = Pattern.compile(getObjectRegex("release")); Matcher m = p.matcher(line); if (m.find()) { releaseBase = m.group(1); currentRelease = Integer.parseInt(m.group(2)); } } sb.append(line).append('\n'); } content = sb.toString(); sb.setLength(0); // update snapshot and releases // we do this in a separate loop in case the order of the keys is different if (Stage.release.equals(stage)) { // RELEASE int newRelease = currentRelease + 1; boolean updatedRelease = false; boolean updatedSnapshot = false; boolean updatedReleases = false; for (String line : content.split("\n")) { // update if (line.matches(getFieldRegex("release"))) { // advance release number int idx = line.indexOf(':') + 1; String newline = line.substring(0, idx); line = newline + " &" + releaseBase + newRelease; updatedRelease = true; } else if (line.matches(getFieldRegex("snapshot"))) { // set snapshot to null int idx = line.indexOf(':') + 1; String newline = line.substring(0, idx); line = newline + " ~"; updatedSnapshot = true; } else if (line.matches(getFieldRegex("releases"))) { // update releases array int idx = line.indexOf(':') + 1; String newline = line.substring(0, idx); line = newline + " &" + releaseBase + "[1.." + newRelease + "]"; updatedReleases = true; } sb.append(line).append('\n'); } if (!updatedRelease) { // insert release field sb.append("release: &" + releaseBase + newRelease).append('\n'); } if (!updatedSnapshot) { // insert snapshot field sb.append("snapshot: ~\n"); } if (!updatedReleases) { // insert releases field sb.append("releases: &" + releaseBase + "[1.." + newRelease + "]").append('\n'); } } else if (Stage.snapshot.equals(stage)) { // RESET for development int snapshotRelease = currentRelease + 1; // insert log template for next release String indent = " "; sb.append("#\n# ${project.version} release\n#\n"); sb.append(releaseBase + snapshotRelease + ": {\n"); sb.append(indent).append("title").append(": ${project.name} ${project.version} released\n"); sb.append(indent).append("id").append(": ${project.version}\n"); sb.append(indent).append("date").append(": ${project.buildDate}\n"); sb.append(indent).append("note").append(": ~\n"); sb.append(indent).append("html").append(": ~\n"); sb.append(indent).append("text").append(": ~\n"); sb.append(indent).append("security").append(": ~\n"); sb.append(indent).append("fixes").append(": ~\n"); sb.append(indent).append("changes").append(": ~\n"); sb.append(indent).append("additions").append(": ~\n"); sb.append(indent).append("dependencyChanges").append(": ~\n"); sb.append(indent).append("contributors").append(": ~\n"); sb.append("}\n\n"); // update the snapshot field to point to the inserted template boolean updatedSnapshot = false; for (String line : content.split("\n")) { if (line.matches(getFieldRegex("snapshot"))) { int idx = line.indexOf(':') + 1; String newline = line.substring(0, idx); line = newline + " &" + releaseBase + snapshotRelease; updatedSnapshot= true; } sb.append(line).append('\n'); } if (!updatedSnapshot) { // insert snapshot field sb.append("snapshot: &" + releaseBase + snapshotRelease).append('\n'); } } if (dryrun) { System.out.println(sb.toString()); } else { FileUtils.writeContent(file, sb.toString()); } } protected String getFieldRegex(String name) { return "^\\s*(?:'|\")?" + name + "(?:'|\")?\\s*:.*"; } protected String getObjectRegex(String name) { return "^\\s*(?:'|\")?" + name + "(?:'|\")?\\s*:\\s*&([a-zA-Z]+)(\\d+)"; } protected String resolveProperties(String string, Map<String, String> properties) { if (string == null) { return null; } Pattern p = Pattern.compile("\\$\\{[a-zA-Z0-9-_\\.]+\\}"); StringBuilder sb = new StringBuilder(string); int start = 0; while (true) { Matcher m = p.matcher(sb.toString()); if (m.find(start)) { String prop = m.group(); prop = prop.substring(2, prop.length() - 1); String value = getProperty(prop, properties); if (value.equals(prop)) { // leave property intact, it will stand out start = m.end(); continue; } sb.replace(m.start(), m.end(), value); start = m.start() + value.length(); } else { return sb.toString(); } } } protected String getProperty(String key, Map<String, String> properties) { String value = key; if (properties.containsKey(key)) { value = properties.get(key); } return value; } }