You are viewing a plain text version of this content. The canonical link for it is here.
Posted to derby-commits@db.apache.org by kr...@apache.org on 2010/10/25 13:11:49 UTC

svn commit: r1027056 [1/3] - in /db/derby/code/trunk/tools/release/jirasoap: ./ src/ src/main/ src/main/java/ src/main/java/org/ src/main/java/org/apache/ src/main/java/org/apache/derbyBuild/ src/main/java/org/apache/derbyBuild/jirasoap/ src/main/wsdl/

Author: kristwaa
Date: Mon Oct 25 11:11:49 2010
New Revision: 1027056

URL: http://svn.apache.org/viewvc?rev=1027056&view=rev
Log:
DERBY-4857: Utilize the SOAP API to fetch JIRA issue list for release notes generation

Added the client part of the new functionality.
The client is an independent Maven project, which consists of the code
required to build the RPC infrastructure (based on WSDL and SOAP) and allowing
us to query the JIRA instance used by Apache Derby to generate a list of
issues fixed in a release.
The output of the client is a text file containing information about the
relevant JIRA issues. This file will be fed into the ReleaseNotesGenerator.
The client can perform the following tasks:
 o (primary) generate list of fixed issues
 o (secondary) print release ancestry chain
 o (secondary) print all Derby releases

To build the client manually: cd into 'tools/release/jirasoap', then run
'mvn -Pbuildclient' ('mvn clean' to clean up generated files)
To run the client manually:
    java -jar tools/release/jirasoap/target/JiraSOAP-jar-with-dependencies.jar
(this will print the usage text - read it!)

Patch file: derby-4857-2a-jirasoap_maven_client.diff

Added:
    db/derby/code/trunk/tools/release/jirasoap/
    db/derby/code/trunk/tools/release/jirasoap/pom.xml   (with props)
    db/derby/code/trunk/tools/release/jirasoap/src/
    db/derby/code/trunk/tools/release/jirasoap/src/main/
    db/derby/code/trunk/tools/release/jirasoap/src/main/java/
    db/derby/code/trunk/tools/release/jirasoap/src/main/java/org/
    db/derby/code/trunk/tools/release/jirasoap/src/main/java/org/apache/
    db/derby/code/trunk/tools/release/jirasoap/src/main/java/org/apache/derbyBuild/
    db/derby/code/trunk/tools/release/jirasoap/src/main/java/org/apache/derbyBuild/jirasoap/
    db/derby/code/trunk/tools/release/jirasoap/src/main/java/org/apache/derbyBuild/jirasoap/DerbyVersion.java   (with props)
    db/derby/code/trunk/tools/release/jirasoap/src/main/java/org/apache/derbyBuild/jirasoap/FilteredIssueLister.java   (with props)
    db/derby/code/trunk/tools/release/jirasoap/src/main/wsdl/
    db/derby/code/trunk/tools/release/jirasoap/src/main/wsdl/jirasoapservice-v2.wsdl   (with props)

Added: db/derby/code/trunk/tools/release/jirasoap/pom.xml
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/tools/release/jirasoap/pom.xml?rev=1027056&view=auto
==============================================================================
--- db/derby/code/trunk/tools/release/jirasoap/pom.xml (added)
+++ db/derby/code/trunk/tools/release/jirasoap/pom.xml Mon Oct 25 11:11:49 2010
@@ -0,0 +1,179 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.apache.derbyBuild</groupId>
+  <artifactId>JiraSOAP</artifactId>
+  <packaging>jar</packaging>
+  <version>LATEST</version>
+  <name>Derby JiraSOAP</name>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>axistools-maven-plugin</artifactId>
+                <version>1.4</version>
+                <configuration>
+                    <wsdlFiles>
+                        <wsdlFile>jirasoapservice-v2.wsdl</wsdlFile>
+                    </wsdlFiles>
+                    <packageSpace>org.apache.derbyBuild.jirasoap</packageSpace>
+                </configuration>
+                <dependencies>
+            <!-- Required for attachment support; you can remove these
+                 dependencies if attachment support is not needed. Note that
+                 if you do want it, you have to specify the dependencies both
+                 here in the plugin and also in the POM dependencies. This
+                 means that in the current state, with the dependencies
+                 specified only here, the compile time warning will go away,
+                 but attachment can't be used during run-time.
+             -->
+                    <dependency>
+                        <groupId>javax.mail</groupId>
+                        <artifactId>mail</artifactId>
+                        <version>1.4.1</version>
+                    </dependency>
+                    <dependency>
+                        <groupId>javax.activation</groupId>
+                        <artifactId>activation</artifactId>
+                        <version>1.1</version>
+                    </dependency>
+                </dependencies>
+                <executions>
+                    <execution>
+                        <id>wsdl2java-generation</id>
+                        <phase>generate-sources</phase>
+                        <goals>
+                            <goal>wsdl2java</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <artifactId>maven-assembly-plugin</artifactId>
+                    <configuration>
+                        <descriptorRefs>
+                            <descriptorRef>jar-with-dependencies</descriptorRef>
+                        </descriptorRefs>
+                        <archive>
+                            <manifest>
+                                <addClasspath>true</addClasspath>
+                                <mainClass>org.apache.derbyBuild.jirasoap.FilteredIssueLister</mainClass>
+                            </manifest>
+                        </archive>
+                    </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>2.0.2</version>
+                <configuration>
+                    <source>1.4</source>
+                    <target>1.4</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <profiles>
+        <profile>
+            <id>buildclient</id>
+            <build>
+                <defaultGoal>assembly:assembly</defaultGoal>
+            </build>
+        </profile>
+        <profile>
+            <id>fetch-wsdl</id>
+            <build>
+                <defaultGoal>generate-sources</defaultGoal>
+                <plugins>
+                    <plugin>
+                        <artifactId>maven-antrun-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <phase>generate-sources</phase>
+                                <goals>
+                                    <goal>run</goal>
+                                </goals>
+                                <configuration>
+                                    <tasks>
+                                        <get src="${derby.soap.jiraurl}/rpc/soap/jirasoapservice-v2?wsdl"
+                                            dest="${basedir}/src/main/wsdl/jirasoapservice-v2.wsdl"/>
+                                    </tasks>
+                                </configuration>
+                            </execution>
+                        </executions>
+                        <dependencies>
+                            <dependency>
+                                <groupId>org.apache.axis</groupId>
+                                <artifactId>axis-ant</artifactId>
+                                <version>1.4</version>
+                            </dependency>
+                        </dependencies>
+                    </plugin>
+
+
+                </plugins>
+            </build>
+            <properties>
+                <derby.soap.jiraurl>https://issues.apache.org/jira</derby.soap.jiraurl>
+            </properties>
+        </profile>
+    </profiles>
+                
+    <dependencies>
+
+        <dependency> 
+            <groupId>org.apache.axis</groupId> 
+            <artifactId>axis</artifactId> 
+            <version>1.4</version> 
+        </dependency> 
+        <dependency> 
+            <groupId>org.apache.axis</groupId> 
+            <artifactId>axis-jaxrpc</artifactId> 
+            <version>1.4</version> 
+        </dependency> 
+        <dependency> 
+            <groupId>org.apache.axis</groupId> 
+            <artifactId>axis-saaj</artifactId> 
+            <version>1.4</version> 
+        </dependency> 
+        <dependency> 
+            <groupId>axis</groupId> 
+            <artifactId>axis-wsdl4j</artifactId> 
+            <version>1.5.1</version> 
+            <scope>compile</scope> 
+        </dependency>
+        <dependency>
+            <groupId>commons-logging</groupId>
+            <artifactId>commons-logging</artifactId>
+            <version>1.1.1</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-discovery</groupId>
+            <artifactId>commons-discovery</artifactId>
+            <version>0.4</version>
+        </dependency>
+        <dependency>
+            <groupId>ant</groupId>
+            <artifactId>ant</artifactId>
+            <version>1.6.5</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.mail</groupId>
+            <artifactId>mail</artifactId>
+            <version>1.4.1</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.activation</groupId>
+            <artifactId>activation</artifactId>
+            <version>1.1</version>
+        </dependency>
+    </dependencies>
+    <description>Client fetching information from the ASF JIRA instance needed for generation of Derby release notes.</description>
+    <properties>
+    <!-- All plugins should use this value.
+         See http://docs.codehaus.org/display/MAVENUSER/POM+Element+for+Source+File+Encoding
+      -->
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+</project>

Propchange: db/derby/code/trunk/tools/release/jirasoap/pom.xml
------------------------------------------------------------------------------
    svn:eol-style = native

Added: db/derby/code/trunk/tools/release/jirasoap/src/main/java/org/apache/derbyBuild/jirasoap/DerbyVersion.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/tools/release/jirasoap/src/main/java/org/apache/derbyBuild/jirasoap/DerbyVersion.java?rev=1027056&view=auto
==============================================================================
--- db/derby/code/trunk/tools/release/jirasoap/src/main/java/org/apache/derbyBuild/jirasoap/DerbyVersion.java (added)
+++ db/derby/code/trunk/tools/release/jirasoap/src/main/java/org/apache/derbyBuild/jirasoap/DerbyVersion.java Mon Oct 25 11:11:49 2010
@@ -0,0 +1,193 @@
+/*
+
+   Derby - Class org.apache.derbyBuild.jirasoap.DerbyVersion
+
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to you 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.apache.derbyBuild.jirasoap;
+
+/**
+ * Class representing a Derby version.
+ * <p>
+ * The format is major.minor.fixpack.point, for instance 10.6.2.1.
+ */
+class DerbyVersion
+        implements Comparable {
+
+    /** Constant for version which haven't been released. */
+    private static final long NOT_RELEASED = -1;
+
+    /** Derby version string, for instance "10.6.2.1". */
+    private final String version;
+    private final int major;
+    private final int minor;
+    private final int fixpack;
+    private final int point;
+    private final long releaseDate;
+
+    /**
+     * Creates a new Derby version object.
+     *
+     * @param rv remote version object fetched from JIRA
+     */
+    public DerbyVersion(RemoteVersion rv) {
+        this(rv.getName(), rv.isReleased()
+                                ? rv.getReleaseDate().getTimeInMillis()
+                                : NOT_RELEASED);
+    }
+
+    DerbyVersion(String version, long relDate) {
+        this.version = version;
+        String[] comp = version.split("\\.");
+        if (comp.length != 4) {
+            throw new IllegalArgumentException("invalid version: " + version);
+        }
+        major = Integer.parseInt(comp[0]);
+        minor = Integer.parseInt(comp[1]);
+        fixpack = Integer.parseInt(comp[2]);
+        point = Integer.parseInt(comp[3]);
+        this.releaseDate = relDate;
+    }
+
+    /**
+     * Returns the Derby version string.
+     *
+     * @return Version string, for instance "10.6.2.1".
+     */
+    public String getVersion() {
+        return version;
+    }
+
+    /**
+     * Returns the release date in milliseconds since the Epoch.
+     *
+     * @return Milliseconds since the Epoch.
+     * @throws IllegalStateException if the version hasn't been released
+     */
+    public long getReleaseDateMillis() {
+        if (!isReleased()) {
+            throw new IllegalStateException("not released");
+        }
+        return releaseDate;
+    }
+
+    /**
+     * Tells if this version has been released.
+     *
+     * @return {@code true} if released, {@code false} if not.
+     */
+    public boolean isReleased() {
+        return releaseDate != NOT_RELEASED;
+    }
+
+    /**
+     * Tells if this version has the same fixpack as the other version.
+     * <p>
+     * This generally means that the two versions are release candidates for an
+     * upcoming release.
+     *
+     * @param other other version
+     * @return {@code true} if the fixpack component of the two versions are
+     *      equal (in addition to the major and minor version), for instance
+     *      the case for 10.6.2.1 and 10.6.2.2, {@code false} otherwise.
+     *
+     */
+    public boolean isSameFixPack(DerbyVersion other) {
+        return (major == other.major && minor == other.minor &&
+                fixpack == other.fixpack);
+    }
+
+    /**
+     * Compares this version to another version based on the Derby version
+     * strings.
+     * <p>
+     * Note that this comparision doesn't take the release date into
+     * consideration, but only the release string. This means that even though
+     * 10.3.3.0 was released after 10.4.1.3, 10.4.1.3 will be considered
+     * greater than 10.3.3.0.
+     *
+     * @param o other version
+     * @return {@code 1} if this version is greater than the other version,
+     *      {@code -1} if this version is smaller than the other version, and
+     *      {@code 0} if the two versions are identical.
+     */
+    public int compareTo(Object o) {
+        DerbyVersion other = (DerbyVersion) o;
+        if (major > other.major) {
+            return 1;
+        }
+        if (major < other.major) {
+            return -1;
+        }
+        if (minor > other.minor) {
+            return 1;
+        }
+        if (minor < other.minor) {
+            return -1;
+        }
+        if (fixpack > other.fixpack) {
+            return 1;
+        }
+        if (fixpack < other.fixpack) {
+            return -1;
+        }
+        if (point > other.point) {
+            return 1;
+        }
+        if (point < other.point) {
+            return -1;
+        }
+        return 0;
+    }
+
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final DerbyVersion other = (DerbyVersion) obj;
+        if (this.major != other.major) {
+            return false;
+        }
+        if (this.minor != other.minor) {
+            return false;
+        }
+        if (this.fixpack != other.fixpack) {
+            return false;
+        }
+        if (this.point != other.point) {
+            return false;
+        }
+        return true;
+    }
+
+    public int hashCode() {
+        int hash = 7;
+        hash = 83 * hash + this.major;
+        hash = 83 * hash + this.minor;
+        hash = 83 * hash + this.fixpack;
+        hash = 83 * hash + this.point;
+        return hash;
+    }
+
+    public String toString() {
+        return version + " (" + (releaseDate == NOT_RELEASED ? "n/a" : Long.toString(releaseDate)) + ")";
+    }
+}

Propchange: db/derby/code/trunk/tools/release/jirasoap/src/main/java/org/apache/derbyBuild/jirasoap/DerbyVersion.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: db/derby/code/trunk/tools/release/jirasoap/src/main/java/org/apache/derbyBuild/jirasoap/FilteredIssueLister.java
URL: http://svn.apache.org/viewvc/db/derby/code/trunk/tools/release/jirasoap/src/main/java/org/apache/derbyBuild/jirasoap/FilteredIssueLister.java?rev=1027056&view=auto
==============================================================================
--- db/derby/code/trunk/tools/release/jirasoap/src/main/java/org/apache/derbyBuild/jirasoap/FilteredIssueLister.java (added)
+++ db/derby/code/trunk/tools/release/jirasoap/src/main/java/org/apache/derbyBuild/jirasoap/FilteredIssueLister.java Mon Oct 25 11:11:49 2010
@@ -0,0 +1,730 @@
+/*
+
+   Derby - Class org.apache.derbyBuild.jirasoap.FilteredIssueLister
+
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to you 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.apache.derbyBuild.jirasoap;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintStream;
+
+import java.rmi.RemoteException;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.GregorianCalendar;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.xml.rpc.ServiceException;
+
+/**
+ * Client talking to the Apache JIRA instance to retrieve and derive information
+ * required to generate releases notes for a Derby release.
+ * <p>
+ * The purpose of this client is to carry out some of the tasks a release
+ * manager has to do when generating the release notes.
+ */
+public class FilteredIssueLister {
+
+    /** System property for specifying the ancestor cutoff threshold. */
+    private static final String ANCESTOR_CUTOFF_PROP = "ancestorCutoff";
+    private static final String DEFAULT_ANCESTOR_CUTOFF = "10.3.3.0";
+    /** System property for turning on reporting of disqualified issues. */
+    private static final String REPORT_DISQUALIFICATIONS_PROP =
+            "reportDisqualifications";
+    /** Help text for command line invocation. */
+    private static final String USAGE =
+"-- Apache Derby JIRA SOAP client --\n\n" +
+"The main purpose of this client is to fetch the required information from\n" +
+"JIRA, such that the release manager can generate the release notes.\n" +
+"This tool does not generate the release notes, but provides some of the\n" +
+"information for the tool doing that.\n\n" +
+"Primary usage:\n" +
+"  o <USER> <PASSWORD> <VERSION> <FILTERID> <DESTINATION_FILE> [ANCESTRY]\n" +
+"    generates a list of fixed issues for the specified release version,\n" +
+"    which can be processed by the ReleaseNotesGenerator tool.\n" +
+"    Note that the release ancestry should be verified. The ancestry is\n" +
+"    printed to standard out and into the generated file. You can also\n" +
+"    check up-front by running the 'ancestors' mode (see 'Secondary usage').\n"+
+"    If incorrect, re-run and specify the release ancestry manually.\n" +
+"\n" +
+"Secondary usage:\n" +
+"  o <USER> <PASSWORD> ancestors <VERSION>\n" +
+"    prints the ancestors of the specified version\n"+
+"    (only released versions can be ancestors)\n" +
+"  o <USER> <PASSWORD> releases\n" +
+"    prints all Derby releases, sorted by release date\n"+
+"\n" +
+"Argument values:\n" +
+"  o VERSION\n" +
+"      Derby version string, i.e. 10.6.2.1\n" +
+"  o FILTERID\n" +
+"      JIRA id, only digits allowed\n" +
+"  o ANCESTRY\n" +
+"      if necessary, the release ancestry can be overridden by specifying\n " +
+"      the ancestors by version manually. Valid values:\n" +
+"        - derive (the default)\n" +
+"        - ignore (don't filter issues)\n" +
+"        - VERSION[,VERSION]* (manually specified)\n" +
+"\n" +
+"System properties:\n" +
+"  o " + ANCESTOR_CUTOFF_PROP + "\n" +
+"    modify the value of the cutoff version\n" +
+"    (default is 10.3.3.0)\n" +
+"  o " + REPORT_DISQUALIFICATIONS_PROP + "\n" +
+"    if set to true, disqualified issues will be printed to standard out\n" +
+"    (default is false)\n" +
+"\n";
+
+    /** Apache Derby project identifier in JIRA. */
+    private static final String DERBY_PROJECT = "DERBY";
+    /** Custom Derby flag used in JIRA. */
+    private static final String FIELD_RELEASE_NOTE = "Release Note Needed";
+    /**
+     * Name of the file containing release notes in JIRA. This is by
+     * Apache Derby community convention.
+     */
+    private static final String RELEASE_NOTE_NAME = "releaseNote.html";
+
+    private PrintStream logOut = new PrintStream(System.out);
+    private JiraSoapService jiraSoapService;
+    /** JIRA user to log in as. */
+    private String user;
+    /** JIRA authentication token. */
+    private String auth;
+    /** Cached version objects. */
+    private DerbyVersion[] allVersions;
+    /** The point at which we stop listing ancestors for a release. */
+    private final DerbyVersion ancestorCutOff;
+    /** Tells if disqualified issues should be reported. */
+    private final boolean reportDisqualifiedIssues;
+    /** Tells if the release ancestry has been overriden by the user. */
+    private boolean ancestryOverridden;
+
+    /**
+     * Creates a new JIRA client.
+     *
+     * @param username JIRA user to log in as
+     * @param cred JIRA password
+     * @throws RemoteException if the login fails for some unexpected reason, or
+     *      if fetching the version list fails
+     * @throws ServiceException if obtaining the JIRA service fails
+     * @throws RuntimeException if the JIRA credentials are invalid
+     */
+    public FilteredIssueLister(String username, String cred)
+            throws RemoteException, ServiceException {
+        JiraSoapServiceService jiraSoapServiceLocator =
+                new JiraSoapServiceServiceLocator();
+        log("getting JIRA service");
+        jiraSoapService = jiraSoapServiceLocator.getJirasoapserviceV2();
+        log("logging in as '" + username + "'");
+        try {
+            auth = jiraSoapService.login(username, cred);
+        } catch (RemoteAuthenticationException rae) {
+            // Give a friendlier error message for this case.
+            throw new RuntimeException(
+                    "JIRA login failed. Cause:\n" + rae.toString());
+        }
+        user = username;
+        log("fetching versions");
+        RemoteVersion[] jiraVer =
+                jiraSoapService.getVersions(auth, DERBY_PROJECT);
+        allVersions = new DerbyVersion[jiraVer.length];
+        for (int i=0; i < jiraVer.length; i++) {
+            allVersions[i] = new DerbyVersion(jiraVer[i]);
+        }
+        // Give a better error message if user-specified cutoff value is bad.
+        try {
+            ancestorCutOff = getVersion(System.getProperty(
+                ANCESTOR_CUTOFF_PROP, DEFAULT_ANCESTOR_CUTOFF));
+        } catch (IllegalArgumentException iae) {
+            throw new IllegalArgumentException(
+                    "invalid ancestor cutoff version", iae);
+        }
+        reportDisqualifiedIssues =
+                Boolean.getBoolean(REPORT_DISQUALIFICATIONS_PROP);
+    }
+
+    /** Constructor for testing, where the Derby versions can be specified
+     * manually.
+     *
+     * @param versions format is {{"major.minor.fixpack.point", "YYYY-MM-DD"}}
+     */
+    FilteredIssueLister(String username, String cred, String[][] versions)
+            throws RemoteException, ServiceException {
+        JiraSoapServiceService jiraSoapServiceLocator =
+                new JiraSoapServiceServiceLocator();
+        log("getting JIRA service");
+        jiraSoapService = jiraSoapServiceLocator.getJirasoapserviceV2();
+        log("logging in as '" + username + "'");
+        try {
+            auth = jiraSoapService.login(username, cred);
+        } catch (RemoteAuthenticationException rae) {
+            // Give a friendlier error message for this case.
+            throw new RuntimeException(
+                    "JIRA login failed. Cause:\n" + rae.toString());
+        }
+        user = username;
+        allVersions = new DerbyVersion[versions.length];
+        // Expected format: release version, release date (YYYY-MM-DD)
+        for (int i=0; i < versions.length; i++) {
+            allVersions[i] = new DerbyVersion(
+                    versions[i][0], parseDate(versions[i][1]));
+        }
+        // Give a better error message if user-specified cutoff value is bad.
+        try {
+            ancestorCutOff = getVersion(System.getProperty(
+                ANCESTOR_CUTOFF_PROP, DEFAULT_ANCESTOR_CUTOFF));
+        } catch (IllegalArgumentException iae) {
+            throw new IllegalArgumentException(
+                    "invaild ancestor cutoff version", iae);
+        }
+        reportDisqualifiedIssues =
+                Boolean.getBoolean(REPORT_DISQUALIFICATIONS_PROP);
+    }
+
+    /**
+     * Generates a list of Derby JIRA issues addressed by the target release
+     * version and writes these to a file for further processing.
+     * <p>
+     * <b>Important:</b> Although some sanity checks are performed, it is
+     * crucial that the manually created filter is set up correctly. If the
+     * filter misses issues addressed by the release, they will not make it
+     * into the generated release notes. Short description:
+     * <ul>
+     *  <li>include bugs and improvements</li>
+     *  <li>include issues resolved as Fixed</li>
+     *  <li>include issues marked as Resolved or Closed</li>
+     *  <li>include all release candidates in the fix version field
+     *      (if not already released)</li>
+     * </ul>
+     *
+     * @param version the target release version
+     * @param filterId the JIRA filter id
+     * @param destFile output file for the issue report
+    * @throws IOException if writing to the output file fails
+     */
+    public void prepareReleaseNotes(String version, long filterId,
+                                    String destFile,
+                                    String[] ancestorVersions)
+            throws IOException {
+        DerbyVersion releaseVersion = getVersion(version);
+        DerbyVersion[] ancestors = null;
+        if (ancestorVersions == null) {
+            // Obtain a list of ancestors, used to disqualify JIRA issues
+            // matched by the JIRA filter.
+            ancestryOverridden = false;
+            ancestors = getAncestors(releaseVersion);
+        } else {
+            ancestryOverridden = true;
+            ancestors = new DerbyVersion[ancestorVersions.length];
+            for (int i=0; i < ancestorVersions.length; i++) {
+                ancestors[i] = getVersion(ancestorVersions[i]);
+            }
+        }
+        persistFilterResult(releaseVersion, filterId, destFile, ancestors);
+    }
+
+    /**
+     * Prints the list of ancestors, i.e. earlier releases down the release
+     * chain, for specified Derby version.
+     * <p>
+     * Note that only released versions are considered to be ancestors.
+     *
+     * @param parentVersion the version to start at (released or not)
+     */
+    public void printAncestors(String parentVersion) {
+        DerbyVersion parent = getVersion(parentVersion);
+        if (parent.compareTo(ancestorCutOff) < 0) {
+            throw new IllegalArgumentException(
+                    "specified version " + parentVersion +
+                    " is less than the ancestor cut-off version: " +
+                    ancestorCutOff.getVersion());
+        }
+        DerbyVersion[] ancestors = getAncestors(parent);
+        System.out.println("--- Ancestors for version " + parentVersion + " (" +
+                (parent.isReleased() ? "released)" : "unreleased)"));
+        for (int i=0; i < ancestors.length; i++) {
+            DerbyVersion a = ancestors[i];
+            Calendar cal = GregorianCalendar.getInstance();
+            cal.setTimeInMillis(a.getReleaseDateMillis());
+            System.out.println(a.getVersion() + ", " +
+                    cal.get(Calendar.YEAR) + "-" +
+                    padZero(cal.get(Calendar.MONTH) +1) + "-" +
+                    padZero(cal.get(Calendar.DAY_OF_MONTH)));
+        }
+        // Special case when there is no ancestor.
+        if (ancestors.length == 0) {
+            System.out.println("<no ancestors found in JIRA>");
+        }
+        System.out.println("(cutoff=" + ancestorCutOff.getVersion() + ")");
+    }
+
+    /**
+     * Prints all Derby releases.
+     */
+    public void printReleases() {
+        ArrayList releases = new ArrayList();
+        for (int i=0; i < allVersions.length; i++) {
+            DerbyVersion dv = allVersions[i];
+            if (dv.isReleased()) {
+                releases.add(dv);
+            }
+        }
+        Collections.sort(releases, new Comparator() {
+
+            public int compare(Object o1, Object o2) {
+                Long release1 = new Long(
+                        ((DerbyVersion)o1).getReleaseDateMillis());
+                Long release2 = new Long(
+                        ((DerbyVersion)o2).getReleaseDateMillis());
+                return release1.compareTo(release2);
+            }
+        });
+        Collections.reverse(releases);
+        System.out.println("--- Derby releases");
+        Iterator relIter = releases.iterator();
+        while (relIter.hasNext()) {
+            DerbyVersion dv = (DerbyVersion)relIter.next();
+            Calendar cal = GregorianCalendar.getInstance();
+            cal.setTimeInMillis(dv.getReleaseDateMillis());
+            System.out.println(dv.getVersion() + ", " +
+                    cal.get(Calendar.YEAR) + "-" +
+                    padZero(cal.get(Calendar.MONTH) +1) + "-" +
+                    padZero(cal.get(Calendar.DAY_OF_MONTH)));
+
+        }
+    }
+
+    /**
+     * Releases resources associated with the client.
+     *
+     * @throws RemoteException if logging out fails
+     */
+    public void destroy()
+            throws RemoteException {
+        jiraSoapService.logout(auth);
+        auth = null;
+        jiraSoapService = null;
+        allVersions = null;
+    }
+
+    /**
+     * Executes a JIRA filter and writes the matching JIRA issues to file.
+     *
+     * @param targetVersion targetted release version
+     * @param filterId JIRA filter id used to obtain the relevant issues
+     * @param destFile destination file
+     * @param excludeFixVersions exclude issues which have been fixed on one
+     *      of the exclude versions (also called ancestry chain)
+     * @return The number of filters written to the destination file.
+     * @throws IOException if writing to the output file fails
+     */
+    private int persistFilterResult(DerbyVersion targetVersion, long filterId,
+                                   String destFile,
+                                   DerbyVersion[] excludeFixVersions)
+            throws IOException {
+        // Extract the version string from the versions to exclude.
+        int size = excludeFixVersions == null ? 0 : excludeFixVersions.length;
+        final ArrayList excludeList = new ArrayList(size);
+        for (int i=0; i < size; i++) {
+            excludeList.add(excludeFixVersions[i].getVersion());
+        }
+
+        BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
+                new FileOutputStream(destFile), "UTF-8"));
+        out.write("// Produced on " + new java.util.Date().toString());
+        out.newLine();
+        out.write("// Release version: " + targetVersion.getVersion());
+        out.newLine();
+        out.write("// Previous release: " + excludeFixVersions[0].getVersion());
+        out.newLine();
+        out.write("// " + (ancestryOverridden ? "Overridden" : "Derived"));
+        out.write(" ancestry chain");
+        out.newLine();
+        for (int i=0; i < excludeFixVersions.length; i++) {
+            out.write("//   " + excludeFixVersions[i].getVersion());
+            out.newLine();
+        }
+        out.write("// Filter id: " + filterId + ", user id " + user);
+        out.newLine();
+        log("fetching issues from filter (id = " + filterId + ")");
+        RemoteIssue[] issues = null;
+        try {
+            issues= jiraSoapService.getIssuesFromFilterWithLimit(
+                auth, Long.toString(filterId), 0, 1000);
+        } catch (org.apache.derbyBuild.jirasoap.RemoteException re) {
+            throw new IllegalArgumentException(
+                    "invalid filter id: " + filterId +
+                    " (" + re.getFaultString() + ")");
+        }
+        log("persisting issues (filter matched " + issues.length + " issues)");
+        out.write("// Filter issue count: " + issues.length);
+        out.newLine();
+        int count = 0;
+        int issuesWithReleaseNote = 0;
+        // Adhere to this very simple format.
+        // --- (separator)
+        // DERBY-XXXX
+        // SUMMARY
+        // FIX_VERSION[,FIX_VERSION]*
+        // RELEASENOTE_ATTACHMENT_ID|null|missing
+        // ("null" if not existing, "missing" if missing)
+        for (int i=0; i < issues.length; i++) {
+            RemoteIssue ri = issues[i];
+            // This will throw exception if the target version isn't in the list
+            // of fix versions, and return null if the issue has been dis-
+            // qualified because it has already been fixed in an ancestor.
+            String fixVersions = stringifyAndCheckFixVersions(ri.getKey(),
+                    ri.getFixVersions(), excludeList, targetVersion);
+            if (fixVersions == null) {
+                continue;
+            }
+            // Persist the issue (human readable/editable).
+            out.write("---");
+            out.newLine();
+            // key
+            out.write(ri.getKey());
+            out.newLine();
+            // summary
+            out.write(ri.getSummary());
+            out.newLine();
+            // fix versions
+            out.write(fixVersions);
+            out.newLine();
+            // release note flag and affects existing applications flag
+            RemoteCustomFieldValue[] fieldValues = ri.getCustomFieldValues();
+            boolean releaseNoteNeeded = hasCustomField(
+                    FIELD_RELEASE_NOTE, fieldValues);
+            // release note attachemnt id
+            if (hasReleaseNote(ri)) {
+                issuesWithReleaseNote++;
+                long latest = 0;
+                RemoteAttachment[] attachments =
+                        jiraSoapService.getAttachmentsFromIssue(
+                                                            auth, ri.getKey());
+                // Find the latest attachment, just use the one with the
+                // highest id.
+                for (int a=0; a < attachments.length; a++) {
+                    String name = attachments[a].getFilename();
+                    long id = Long.parseLong(attachments[a].getId());
+                    if (name.equals(RELEASE_NOTE_NAME)) {
+                        latest = Math.max(latest, id);
+                    }
+                }
+                out.write(Long.toString(latest));
+            } else {
+                if (releaseNoteNeeded) {
+                    out.write("missing");
+                } else {
+                    out.write("null");
+                }
+            }
+            out.newLine();
+            count++;
+        }
+
+        // Write some more status
+        out.write("// Issues written: " + count);
+        out.newLine();
+        out.write("// Issues disqualified: ");
+        if (excludeFixVersions == null) {
+            out.write("disqualification disabled");
+        } else {
+            out.write(Integer.toString(issues.length - count));
+        }
+        out.newLine();
+        out.write("// Issues with release note: " + issuesWithReleaseNote);
+        out.newLine();
+        out.close();
+
+        // Log some basic information
+        log("wrote " + count + " issues, " + issuesWithReleaseNote +
+                " with release notes, " + (issues.length - count) +
+                " issues disqualified");
+        log("dump file: " + new File(destFile).getAbsolutePath());
+        return count;
+    }
+
+    /**
+     * Returns the version object for the specified Derby version.
+     *
+     * @param version target version
+     * @return A version object.
+     * @throws IllegalArgumentException if the specified version doesn't exist
+     */
+    private DerbyVersion getVersion(String version) {
+        DerbyVersion match = null;
+        for (int i=0; i < allVersions.length; i++) {
+            if (version.equals(allVersions[i].getVersion())) {
+                match = allVersions[i];
+            }
+        }
+        if (match == null) {
+            throw new IllegalArgumentException(
+                    "version '" + version + "' doesn't exist");
+        }
+        return  match;
+    }
+
+    /**
+     * Computes the ancestors for the specified version.
+     *
+     * @param parent the initial parent version
+     * @return A list of ancestors for the specified version.
+     */
+    private DerbyVersion[] getAncestors(DerbyVersion parent) {
+        ArrayList ancestors = new ArrayList();
+        DerbyVersion[] dv = getSortedAndFilteredReleases(parent);
+        if (!parent.isReleased() && dv.length > 0) {
+            ancestors.add(dv[0]);
+        }
+        while (dv.length > 1 && dv[0].compareTo(ancestorCutOff) >= 0) {
+            dv = getSortedAndFilteredReleases(dv[1]);
+            ancestors.add(dv[0]);
+        }
+        dv = new DerbyVersion[ancestors.size()];
+        ancestors.toArray(dv);
+        return dv;
+    }
+
+    /**
+     * Returns a list of sorted and filtered Derby releases.
+     * <p>
+     * If a target release is specified, all later releases will be filtered
+     * out. The filtering happens at two levels:
+     * <ul> <li>version number (i.e. 10.6.2.1 > 10.5.1.0)</li>
+     *      <li>release date</li>
+     * </ul>
+     * If the target version has been released, it will be placed at index zero.
+     * If the target version hasn't been released, it will not be included in
+     * the list.
+     * <p>
+     * Not specifying a target version will return all Derby releases sorted by
+     * version number.
+     *
+     * @param target target version to start sorting/filtering at (may be null)
+     * @return A list of previous releases, sorted by version number
+     *      (highest first).
+     */
+    private DerbyVersion[] getSortedAndFilteredReleases(DerbyVersion target) {
+        // Add versions to the list, filtering as specified.
+        ArrayList tmp = new ArrayList();
+        for (int i=0; i < allVersions.length; i++) {
+            DerbyVersion dv = allVersions[i];
+            // Skip versions that haven't been released.
+            if (!dv.isReleased()) {
+                continue;
+            }
+            if (target != null) {
+                if (dv.compareTo(target) > 0) {
+                    continue;
+                }
+                if (target.isReleased() && dv.getReleaseDateMillis() >
+                        target.getReleaseDateMillis()) {
+                    continue;
+                }
+            }
+            tmp.add(dv);
+        }
+        // Sort, then reverse to get newest version at index zero.
+        Collections.sort(tmp);
+        Collections.reverse(tmp);
+        DerbyVersion[] result = new DerbyVersion[tmp.size()];
+        tmp.toArray(result);
+        return result;
+    }
+
+    /** Adds a leading zero if the value is less than ten. */
+    private static String padZero(int val) {
+        if (val < 10) {
+            return "0" + Integer.toString(val);
+        } else {
+            return Integer.toString(val);
+        }
+    }
+
+    /**
+     * Interface for running from the command line.
+     *
+     * @param args see USAGE constant, or invoke with zero arguments
+     * @throws Exception if something goes wrong
+     */
+    public static void main(String[] args)
+            throws Exception {
+        // Always require JIRA user name and password.
+        if (args.length > 2) {
+            FilteredIssueLister client =
+                    new FilteredIssueLister(args[0], args[1]);
+            try {
+                // PRINT ANCESTORS
+                if (args[2].equalsIgnoreCase("ancestors")) {
+                    if (args.length == 2) {
+                        System.err.println("Missing version argument.");
+                        System.exit(1);
+                    }
+                    client.printAncestors(args[3]);
+                // PRINT VERSIONS
+                } else if(args[2].equalsIgnoreCase("releases")) {
+                    client.printReleases();
+                // RELEASE NOTES PREPARATION / GENERATE ISSUE LIST
+                } else {
+                    if (args.length < 4) {
+                        System.err.println("Missing argument(s).");
+                        System.exit(1);
+                    }
+                    String[] overriddenAncestry = null;
+                    // This is the default release target
+                    // Args: user password version filterId dest [remove]
+                    if (args.length > 5) {
+                        overriddenAncestry = args[5].split(",");
+                        if (args[0].equalsIgnoreCase("ignore")) {
+                            overriddenAncestry = new String[0];
+                        } else if(args[0].equalsIgnoreCase("derive")) {
+                            overriddenAncestry = null;
+                        }
+                    }
+                    client.prepareReleaseNotes(args[2], Long.parseLong(args[3]),
+                            args[4], overriddenAncestry);
+                }
+            } finally {
+                client.destroy();
+            }
+        } else {
+            System.err.println(USAGE);
+        }
+    }
+
+    /** Logs status/convenience messages. */
+    private void log(String msg) {
+        if (logOut != null) {
+            logOut.println(msg);
+        }
+    }
+
+    /**
+     * Converts an array of fix versions into a string representation.
+     *
+     * @param fixVersions fix versions for a JIRA issue
+     * @return A string describing all the fix versions.
+     */
+    private String stringifyAndCheckFixVersions(String issueKey,
+            RemoteVersion[] fixVersions, List excludeVersions,
+            DerbyVersion releaseVersion) {
+        if (fixVersions.length == 0) {
+            throw new IllegalStateException(issueKey + " has no fix version");
+        }
+        boolean disqualified = false;
+        boolean sanityCheckPassed = false;
+        StringBuffer sb = new StringBuffer();
+        StringBuffer fixedIn = new StringBuffer(); // only used for reporting
+        for (int i=0; i < fixVersions.length; i++) {
+            String fv = fixVersions[i].getName();
+            if (!sanityCheckPassed) {
+                DerbyVersion dv = new DerbyVersion(fixVersions[i]);
+                if (dv.equals(releaseVersion) ||
+                        dv.isSameFixPack(releaseVersion)) {
+                    sanityCheckPassed = true;
+                }
+            }
+            if (excludeVersions.contains(fv)) {
+                disqualified = true;
+                fixedIn.append(fv).append(',');
+                // Could return null here, but then the sanity-check may be
+                // bypassed.
+            }
+            sb.append(fv).append(',');
+        }
+        sb.deleteCharAt(sb.length() -1);
+
+        // Sanity check to catch if an invalid JIRA filter is being used.
+        if (!sanityCheckPassed) {
+            throw new IllegalStateException(issueKey + " not marked as fixed " +
+                    "in the target release version" +
+                    releaseVersion.getVersion() + ", nor in any of the " +
+                    "versions with the same fixpack. Invalid JIRA filter?");
+        }
+        if (disqualified) {
+            if (reportDisqualifiedIssues) {
+                fixedIn.deleteCharAt(fixedIn.length() -1);
+                System.out.println(issueKey + " disqualified, " +
+                        "already fixed in " + fixedIn.toString());
+            }
+            return null;
+        } else {
+            return sb.toString();
+        }
+    }
+
+    /**
+     * Tells if the issue has a release note.
+     *
+     * @param issue JIRA issue
+     * @return {@code true} if the issue has a release note attached.
+     */
+    private static boolean hasReleaseNote(RemoteIssue issue) {
+        String[] aNames = issue.getAttachmentNames();
+        for (int i=0; i < aNames.length; i++) {
+            if (aNames[i].equals(RELEASE_NOTE_NAME)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Tells if the issue has the specified custom field value set.
+     *
+     * @param fieldValue the value to look for
+     * @param values the field values
+     * @return {@code true} if the custom field value was found,
+     *      {@code false} otherwise.
+     */
+    private static boolean hasCustomField(String fieldName,
+                                          RemoteCustomFieldValue[] values) {
+        // The API is a but awkward when it comes to fields, but we can do our
+        // thing by looking at the custom field values only.
+        for (int i=0; i < values.length; i++) {
+            String[] v = values[i].getValues();
+            for (int j=0; j < v.length; j++) {
+                if (fieldName.equals(v[j])) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private static final Calendar PARSECAL = GregorianCalendar.getInstance();
+    private static synchronized long parseDate(String date) {
+        String[] comp = date.split("-");
+        int year = Integer.parseInt(comp[0]);
+        int month = Integer.parseInt(comp[1]) -1;
+        int day = Integer.parseInt(comp[2]);
+        PARSECAL.set(year, month, day, 0, 0, 0);
+        return PARSECAL.getTimeInMillis();
+    }
+}

Propchange: db/derby/code/trunk/tools/release/jirasoap/src/main/java/org/apache/derbyBuild/jirasoap/FilteredIssueLister.java
------------------------------------------------------------------------------
    svn:eol-style = native