org.nuiton.jredmine.plugin.GenerateChangesMojo Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jredmine-maven-plugin Show documentation
Show all versions of jredmine-maven-plugin Show documentation
JRedmine maven plugin to interacts with Redmine's server
/*
* #%L
* JRedmine :: Maven plugin
*
* $Id: GenerateChangesMojo.java 411 2013-08-08 12:31:31Z tchemit $
* $HeadURL: http://svn.nuiton.org/svn/jredmine/tags/jredmine-1.6/jredmine-maven-plugin/src/main/java/org/nuiton/jredmine/plugin/GenerateChangesMojo.java $
* %%
* Copyright (C) 2009 - 2012 Tony Chemit, CodeLutin
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Lesser Public License for more details.
*
* You should have received a copy of the GNU General Lesser Public
* License along with this program. If not, see
* .
* #L%
*/
package org.nuiton.jredmine.plugin;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.changes.model.Action;
import org.apache.maven.plugins.changes.model.Author;
import org.apache.maven.plugins.changes.model.Body;
import org.apache.maven.plugins.changes.model.ChangesDocument;
import org.apache.maven.plugins.changes.model.Properties;
import org.apache.maven.plugins.changes.model.Release;
import org.apache.maven.plugins.changes.model.io.xpp3.ChangesXpp3Writer;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.XmlStreamWriter;
import org.nuiton.jredmine.model.IdAbles;
import org.nuiton.jredmine.model.Issue;
import org.nuiton.jredmine.model.IssueCategory;
import org.nuiton.jredmine.model.IssueStatus;
import org.nuiton.jredmine.model.Tracker;
import org.nuiton.jredmine.model.User;
import org.nuiton.jredmine.model.Version;
import org.nuiton.jredmine.service.RedmineServiceException;
import org.nuiton.plugin.PluginHelper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* Generates the changes.xml file from the Redmine's server to be used
* by the maven-changes-plugin to generates the release report and send
* the annoncement mail at a release time.
*
* @author tchemit
* @since 1.0.0
*/
@Mojo(name = "generate-changes", requiresOnline = true, requiresProject = true)
public class GenerateChangesMojo extends AbstractRedmineMojoWithProjectAndVersion implements IssueCollectorConfiguration, SkipOrRunOnlyOnceAware {
/** The actions understood by the changes.xml format. */
enum Actions {
add,
fix,
update,
remove
}
///////////////////////////////////////////////////////////////////////////
/// Mojo parameters
///////////////////////////////////////////////////////////////////////////
/**
* Flag to know if anonymous connexion to redmine server is required.
*
* For this goal, the default value is {@code true}
*
* Note: If set to {@code false}, you should fill {@link #username}
* and {@link #password} properties.
*
* @since 1.1.3
*/
@Parameter(property = "redmine.anonymous", defaultValue = "true")
protected boolean anonymous;
/**
* The path of the changes.xml
file that will be converted into an HTML report.
*
* @since 1.0.0
*/
@Parameter(property = "redmine.xmlPath", defaultValue = "${basedir}/src/changes/changes.xml", required = true)
protected File xmlPath;
/**
* The description of the release.
*
* Note : if not sets - will use the redmine version description (if exists).
*
* @since 1.0.0
*/
@Parameter(property = "releaseDescription")
protected String releaseDescription;
/**
* The changes file title.
*
* @since 1.0.0
*/
@Parameter(property = "changesTitle", defaultValue = "${project.description}", required = true)
protected String changesTitle;
/**
* If you only want to show issues for the current version in the report.
* The current version being used is ${project.version}
minus
* any "-SNAPSHOT" suffix.
*
* @since 1.0.0
*/
@Parameter(property = "redmine.onlyCurrentVersion", defaultValue = "false")
protected boolean onlyCurrentVersion;
/**
* The action mapping to the redmine tracker ids.
*
* Possible actions are {@code add}, {@code fix}, {@code update}, {@code remove}
*
* The syntax of mapping is {@code action:id [,action:id]*}
*
* Example :
*
*
* fix:1
* fix:1, add:1
*
*
* @since 1.0.0
*/
@Parameter(property = "redmine.actionMapping", required = true)
protected String actionMapping;
/**
* The comma separated list of statuses ids to include in the changes.xml
*
* Note : If a value is set to empty - that means to include all status.
*
*
* @since 1.0.0
*/
@Parameter(property = "redmine.statusIds")
protected String statusIds;
/**
* The comma separated list of category ids to include in the changes.xml
*
* Note : If a value is set to empty - that means to include all categories.
*
*
* @since 1.0.0
*/
@Parameter(property = "redmine.categoryIds")
protected String categoryIds;
/**
* A flag to skip the goal.
*
* @since 1.0.0
*/
@Parameter(property = "redmine.skipGenerateChanges", defaultValue = "false")
protected boolean skipGenerateChanges;
/**
* A flag to generate only once in a multi-module project. The changes.xml
* file will be generated only once in the pom module and then copy in the
* modules.
*
* The default behaviour is to generate once to reduce calls to redmine
*
* @since 1.0.0
*/
@Parameter(property = "redmine.generateOnce", defaultValue = "true")
protected boolean generateOnce;
/**
* A flag to restrict only to run on root module.
*
* @since 1.6
*/
@Parameter(property = "redmine.runOnlyOnRoot", defaultValue = "true")
protected boolean runOnlyOnRoot;
///////////////////////////////////////////////////////////////////////////
/// Mojo internal attributes
///////////////////////////////////////////////////////////////////////////
/** le fichier deja genere */
private static File cacheChangesFile;
/** le mapping entre l'id d'un tracker et le type d'action */
protected Map trackerToAction;
protected Map filters;
private Version[] versions;
/** flag to mark if a runOnce goal was done */
protected boolean runOnceDone;
public GenerateChangesMojo() {
super(true, true);
}
///////////////////////////////////////////////////////////////////////////
/// RedmineClientConfiguration
///////////////////////////////////////////////////////////////////////////
@Override
public boolean isAnonymous() {
return anonymous;
}
@Override
public void setAnonymous(boolean anonymous) {
this.anonymous = anonymous;
}
///////////////////////////////////////////////////////////////////////////
/// IssueCollectionConfiguration
///////////////////////////////////////////////////////////////////////////
@Override
public boolean isOnlyCurrentVersion() {
return onlyCurrentVersion;
}
@Override
public int getMaxEntries() {
// no filter on priority for building the changes.xml file
return 0;
}
@Override
public String getPriorityIds() {
// no filter on priority for building the changes.xml file
return null;
}
@Override
public String getCategoryIds() {
return filters.get("category");
}
@Override
public String getVersionNames() {
return filters.get("version");
}
@Override
public String getStatusIds() {
return filters.get("status");
}
@Override
public String getTrackerIds() {
return filters.get("tracker");
}
///////////////////////////////////////////////////////////////////////////
/// SkipOrRunOnlyOnceAware
///////////////////////////////////////////////////////////////////////////
@Override
public String getSkipProperty() {
return "skipGenerateChanges";
}
@Override
public boolean isGoalSkip() {
return skipGenerateChanges;
}
@Override
public boolean isRunOnce() {
return generateOnce;
}
@Override
public boolean isRunOnlyOnRoot() {
return runOnlyOnRoot;
}
@Override
public boolean isRunOnceDone() {
return runOnceDone;
}
@Override
public boolean checkRunOnceDone() {
Date buildStartTime = session == null ? null : session.getStartTime();
Date newStartTime = cacheChangesFile != null && cacheChangesFile.exists() ? new Date(cacheChangesFile.lastModified()) : null;
boolean needInvoke = needInvoke(isRunOnce(),
runOnlyOnRoot,
buildStartTime,
newStartTime
);
return !needInvoke;
}
///////////////////////////////////////////////////////////////////////////
/// AbstractRedmineMojo
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
/// AbstractPlugin
///////////////////////////////////////////////////////////////////////////
@Override
protected void init() throws Exception {
if (isGoalSkip()) {
return;
}
if (xmlPath == null || xmlPath.getAbsolutePath().trim().isEmpty()) {
throw new MojoExecutionException("required a xmlPath parameter");
}
if (StringUtils.isBlank(versionId)) {
throw new MojoExecutionException("required a versionId parameter");
}
versionId = PluginHelper.removeSnapshotSuffix(versionId);
if (runOnceDone = isRunOnce() && checkRunOnceDone()) {
return;
}
super.init();
if (!safe && !initOk) {
// we are in none safe mode but init is not ok...
return;
}
// get trackers
Tracker[] releaseTrackers;
try {
releaseTrackers = service.getTrackers(projectId);
} catch (RedmineServiceException e) {
throw new MojoExecutionException("could not obtain trackers for reason " + e.getMessage(), e);
}
// get statuses
IssueStatus[] statuses;
try {
statuses = service.getIssueStatuses();
} catch (RedmineServiceException e) {
throw new MojoExecutionException("could not obtain statuses for reason " + e.getMessage(), e);
}
// get categories
IssueCategory[] categories;
try {
categories = service.getIssueCategories(projectId);
} catch (RedmineServiceException e) {
throw new MojoExecutionException("could not obtain categories for reason " + e.getMessage(), e);
}
filters = new HashMap();
// build trackerToAction and tracker filter
trackerToAction = new HashMap();
String[] entries = actionMapping.split(",");
StringBuilder buffer = new StringBuilder();
for (String entry : entries) {
String[] parts = entry.split(":");
if (parts.length != 2) {
// error in syntax
throw new MojoExecutionException("the trackerMapping entry " + Arrays.toString(parts) + " is not well formed");
}
String action = parts[0].trim().toLowerCase();
try {
Actions.valueOf(action);
} catch (Exception e) {
throw new MojoExecutionException("the action " + action + " is unknown... authorized : " + Arrays.toString(Actions.values()));
}
Integer id = Integer.valueOf(parts[1].trim());
Tracker t = IdAbles.byId(id, releaseTrackers);
if (t == null) {
throw new MojoExecutionException("could not obtain the tracker with id " + id);
}
trackerToAction.put(id, action);
buffer.append(",").append(id);
}
filters.put("tracker", buffer.substring(1));
// create status filters
if (StringUtils.isNotBlank(statusIds)) {
for (String s : statusIds.split(",")) {
Integer id = Integer.valueOf(s.trim());
IssueStatus t = IdAbles.byId(id, statuses);
if (t == null) {
throw new MojoExecutionException("could not obtain the status with id " + id);
}
}
}
filters.put("status", statusIds);
// create category filters
if (StringUtils.isNotBlank(categoryIds)) {
for (String s : categoryIds.split(",")) {
Integer id = Integer.valueOf(s.trim());
IssueCategory t = IdAbles.byId(id, categories);
if (t == null) {
throw new MojoExecutionException("could not obtain the category with id " + id);
}
}
filters.put("category", categoryIds);
}
// create version filters
List versionList = new ArrayList();
boolean versionExist = true;
releaseVersion = getProjectVersion(versionId);
if (releaseVersion == null) {
versionExist = false;
// la version n'existe pas encore sur redmine
getLog().warn("The version " + versionId + " does not exist on redmine");
releaseVersion = new Version();
releaseVersion.setName(versionId);
releaseVersion.setProjectId(releaseProject.getId());
}
if (onlyCurrentVersion) {
// just release version
versionList.add(releaseVersion);
} else {
// obtain all released versions (just a effective-date)
buffer = new StringBuilder();
for (Version v : getProjectVersions()) {
String versionName = v.getName();
boolean keep = true;
if (!versionId.equals(versionName)) {
//TODO TC-20090914 make this better, since this is a
// very soft test to test only the effective date
if (v.getEffectiveDate() == null) {
// skip this unclosed version
keep = false;
}
}
if (keep) {
buffer.append(",").append(v.getName());
versionList.add(v);
}
}
if (!versionExist) {
versionList.add(releaseVersion);
}
}
versions = versionList.toArray(new Version[versionList.size()]);
if (!onlyCurrentVersion) {
filters.put("version", buffer.substring(1));
}
}
@Override
protected boolean checkSkip() {
if (isRunOnceDone()) {
if (!xmlPath.exists()) {
getLog().info("Use already generated " + xmlPath.getName() + " (" + cacheChangesFile + ")");
try {
createDirectoryIfNecessary(xmlPath.getParentFile());
copyFile(cacheChangesFile, xmlPath);
} catch (IOException e) {
throw new IllegalStateException("could not copy already generated file " + xmlPath + " from " + cacheChangesFile);
}
}
}
boolean b = super.checkSkip();
return b;
}
@Override
protected void doAction() throws Exception {
File xmlParent = xmlPath.getParentFile();
createDirectoryIfNecessary(xmlParent);
if (releaseVersion.getEffectiveDate() == null) {
getLog().debug("The version " + versionId + " is not effective on redmine, should update effective-date property to today");
releaseVersion.setEffectiveDate(new Date());
}
getLog().info("release project " + releaseProject.getName());
getLog().info("release version " + releaseVersion.getName());
getLog().info("release date " + releaseVersion.getEffectiveDate());
getLog().info("release user " + releaseUser.getFirstname() + " " + releaseUser.getLastname());
// init issues collector
IssuesCollector collector = new IssuesCollector(getLog(), verbose);
// collects issues
try {
collector.collect(service, this);
} catch (RedmineServiceException ex) {
collector.clearFilters();
throw new MojoExecutionException("could not obtain issues for reason " + ex.getMessage(), ex);
}
//TODO make some logic checks : version must be
// build the maven changes.xml as memory model
ChangesDocument doc = buildChangesDocument(releaseVersion, releaseUser, collector);
// store the generated file
ChangesXpp3Writer xppWriter = new ChangesXpp3Writer();
XmlStreamWriter writer = new XmlStreamWriter(xmlPath);
xppWriter.write(writer, doc);
getLog().info("File saved in " + xmlPath);
// cache result
cacheChangesFile = xmlPath;
}
///////////////////////////////////////////////////////////////////////////
/// Others
///////////////////////////////////////////////////////////////////////////
protected ChangesDocument buildChangesDocument(Version version, User user, IssuesCollector collector) throws RedmineServiceException {
ChangesDocument doc = new ChangesDocument();
Properties properties = new Properties();
Author author = new Author();
author.setAuthorEmail(user.getMail());
author.setName(user.getFirstname() + " " + user.getLastname());
properties.setAuthor(author);
properties.setTitle(changesTitle);
doc.setProperties(properties);
Body body = new Body();
doc.setBody(body);
//TC-20091124 : was removed since maven-changes-plugin 2.2 ?
// body.setModelEncoding(encoding);
Issue[] issues = collector.getIssues();
// iterate on versions
for (Version v : versions) {
boolean treateReleaseVersion = v.getId() == version.getId();
Issue[] issuesForVersion = Issue.byVersionId(v.getId(), issues);
Release release = new Release();
body.addRelease(release);
release.setVersion(v.getName());
if (v.getEffectiveDate() != null) {
release.setDateRelease(dateFormat.format(v.getEffectiveDate()));
}
if (v.getDescription() != null) {
release.setDescription(v.getDescription());
}
if (treateReleaseVersion) {
if (releaseDescription != null) {
// override the release description
// this is the main version to release
release.setDescription(releaseDescription);
}
if (release.getDateRelease() == null) {
release.setDateRelease(dateFormat.format(new Date()));
}
}
// iterate on actions ? or order it
for (Entry entry : trackerToAction.entrySet()) {
String type = entry.getValue();
// get issues for the tracker
Issue[] issuesForTracker = Issue.byTrackerId(entry.getKey(), issuesForVersion);
for (Issue issue : issuesForTracker) {
// new action
Action action = new Action();
action.setSystem("redmine");
action.setAction(issue.getSubject());
action.setIssue(issue.getId() + "");
action.setType(type);
if (issue.getDueDate() != null) {
action.setDate(dateFormat.format(issue.getDueDate()));
} else {
if (verbose) {
getLog().warn("issue " + issue.getSubject() + " has no dueDate...");
}
}
User a;
int id;
// created by
id = issue.getAuthorId();
if (id == 0) {
getLog().warn("issue " + issue.getSubject() + " is not created to any user, this is not normal...");
id = issue.getAuthorId();
}
a = IdAbles.byId(id, users);
if (a != null) {
action.setDueTo(a.getFirstname() + " " + a.getLastname());
action.setDueToEmail(a.getMail());
}
// resolved by
id = issue.getAssignedToId();
if (id == 0) {
getLog().warn("issue " + issue.getSubject() + " is not assigned to any user, this is not normal...");
id = issue.getAuthorId();
}
a = IdAbles.byId(id, users);
if (a != null) {
//TODO should check this is a developper name on pom.xml
action.setDev(a.getLogin());
}
release.addAction(action);
}
}
}
return doc;
}
}