Commits

Matt Ryall committed 5b4e9cb

[maven-release-plugin] copy for tag confluence-planning-plugin-1.0

Comments (0)

Files changed (12)

 <?xml version="1.0" encoding="UTF-8"?>
 
-<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">
+<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>com.atlassian.confluence.plugin.planning</groupId>
     <artifactId>confluence-planning-plugin</artifactId>
-    <version>1.0-SNAPSHOT</version>
+    <version>1.0</version>
 
     <organization>
         <name>Atlassian</name>
     </properties>
 
     <scm>
-        <connection>scm:svn:http://labs.atlassian.com/svn/CONFPLAN/trunk</connection>
-        <developerConnection>scm:svn:http://labs.atlassian.com/svn/CONFPLAN/trunk</developerConnection>
-        <url>http://labs.atlassian.com/source/browse/CONFPLAN</url>
+        <connection>scm:svn:https://labs.atlassian.com/svn/CONFPLAN/tags/confluence-planning-plugin-1.0</connection>
+        <developerConnection>scm:svn:https://labs.atlassian.com/svn/CONFPLAN/tags/confluence-planning-plugin-1.0</developerConnection>
+        <url>https://labs.atlassian.com/source/browse/CONFPLAN/tags/confluence-planning-plugin-1.0</url>
     </scm>
 
 </project>

src/main/java/com/atlassian/confluence/plugin/planning/DataOutOfDateException.java

+package com.atlassian.confluence.plugin.planning;
+
+/**
+ * Thrown when an optimistic concurrency check fails.
+ */
+public class DataOutOfDateException extends Exception {
+    public DataOutOfDateException() {
+    }
+
+    public DataOutOfDateException(String message) {
+        super(message);
+    }
+
+    public DataOutOfDateException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public DataOutOfDateException(Throwable cause) {
+        super(cause);
+    }
+}

src/main/java/com/atlassian/confluence/plugin/planning/ProjectAction.java

-package com.atlassian.confluence.plugin.planning;
-
-import com.atlassian.bandana.BandanaContext;
-import com.atlassian.bandana.BandanaManager;
-import com.atlassian.confluence.core.ConfluenceActionSupport;
-import com.atlassian.confluence.json.JSONAction;
-import com.atlassian.confluence.setup.bandana.ConfluenceBandanaContext;
-import org.apache.commons.lang.StringUtils;
-
-import java.text.SimpleDateFormat;
-import java.util.Date;
-
-public class ProjectAction extends ConfluenceActionSupport implements JSONAction
-{
-    private static final String STORAGE_KEY_PREFIX = ProjectAction.class.getName();
-    private static final BandanaContext STORAGE_CONTEXT = ConfluenceBandanaContext.GLOBAL_CONTEXT;
-
-    private BandanaManager bandanaManager;
-    private String projectName;
-    private String data;
-
-    public String execute()
-    {
-        data = StringUtils.defaultString((String) bandanaManager.getValue(STORAGE_CONTEXT, toStorageKey(projectName)),
-            defaultProject(projectName));
-        return SUCCESS;
-    }
-
-    public String save()
-    {
-        bandanaManager.setValue(STORAGE_CONTEXT, toStorageKey(projectName), data);
-        return SUCCESS;
-    }
-
-    public String getJSONString()
-    {
-        return data;
-    }
-
-    private static String defaultProject(String projectName)
-    {
-        return "{ name: \"" + projectName.replace("\"", "\\\"") + "\", " +
-            "iterations: [\n" +
-            "   { title: \"Iteration 1: " + getDefaultIterationDate() + "\", tasks: [\n" +
-            "       { title: \"Example scheduled task\", estimate: \"1d\" }\n" +
-            "   ]},\n" +
-            "   { title: \"Backlog\", tasks: [\n" +
-            "       { title: \"Example backlog task\", estimate: \"1d 4h\" }\n" +
-            "   ]}\n" +
-            "]}\n";
-    }
-
-    private static String getDefaultIterationDate()
-    {
-        return new SimpleDateFormat("d MMM").format(new Date());
-    }
-
-    private static String toStorageKey(String projectName)
-    {
-        return STORAGE_KEY_PREFIX + "_" + projectName.replaceAll("[^\\p{Alnum}]", "");
-    }
-
-    public void setProjectName(String projectName)
-    {
-        this.projectName = projectName;
-    }
-
-    public String getProjectName()
-    {
-        return projectName;
-    }
-
-    public void setData(String projectJson)
-    {
-        this.data = projectJson;
-    }
-
-    public void setBandanaManager(BandanaManager bandanaManager)
-    {
-        this.bandanaManager = bandanaManager;
-    }
-}

src/main/java/com/atlassian/confluence/plugin/planning/ProjectManager.java

+package com.atlassian.confluence.plugin.planning;
+
+import com.atlassian.bandana.BandanaContext;
+import com.atlassian.bandana.BandanaManager;
+import com.atlassian.confluence.setup.bandana.ConfluenceBandanaContext;
+import com.thoughtworks.xstream.XStream;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class ProjectManager
+{
+    private static final String STORAGE_KEY_PREFIX = "com.atlassian.confluence.plugin.confluence-planning-plugin";
+    private static final BandanaContext STORAGE_CONTEXT = ConfluenceBandanaContext.GLOBAL_CONTEXT;
+
+    private final BandanaManager bandanaManager;
+    private final XStream xStream;
+
+    public ProjectManager(BandanaManager bandanaManager)
+    {
+        this.bandanaManager = bandanaManager;
+        xStream = new XStream();
+        xStream.setClassLoader(getClass().getClassLoader()); // use the plugin classloader
+    }
+
+    public StoredProject loadProject(String projectName)
+    {
+        String xml = (String) bandanaManager.getValue(STORAGE_CONTEXT, toStorageKey(projectName));
+        if (xml == null)
+            return new StoredProject(projectName, defaultData());
+        return (StoredProject) xStream.fromXML(xml);
+    }
+
+    public StoredProject updateProject(String projectName, int version, String data) throws DataOutOfDateException
+    {
+        StoredProject existingProject = loadProject(projectName);
+        StoredProject updatedProject = existingProject.updateData(version, data);
+        String xml = xStream.toXML(updatedProject);
+        bandanaManager.setValue(STORAGE_CONTEXT, toStorageKey(projectName), xml);
+        return updatedProject;
+    }
+
+    private static String toStorageKey(String projectName)
+    {
+        return STORAGE_KEY_PREFIX + "_" + projectName.replaceAll("[^\\p{Alnum}]", "");
+    }
+
+    private static String defaultData()
+    {
+        return "{ iterations: [\n" +
+            "   { title: \"Iteration 1: " + getDefaultIterationDate() + "\", tasks: [\n" +
+            "       { title: \"Example scheduled task\", estimate: \"1d\" }\n" +
+            "   ]},\n" +
+            "   { title: \"Backlog\", tasks: [\n" +
+            "       { title: \"Example backlog task\", estimate: \"1d 4h\" }\n" +
+            "   ]}\n" +
+            "]}\n";
+    }
+
+    private static String getDefaultIterationDate()
+    {
+        return new SimpleDateFormat("d MMM").format(new Date());
+    }
+
+
+
+}

src/main/java/com/atlassian/confluence/plugin/planning/SaveProjectAction.java

+package com.atlassian.confluence.plugin.planning;
+
+import com.atlassian.bandana.BandanaManager;
+import com.atlassian.confluence.core.ConfluenceActionSupport;
+import com.atlassian.confluence.json.JSONAction;
+import com.atlassian.confluence.json.parser.JSONException;
+import com.opensymphony.webwork.ServletActionContext;
+
+import javax.servlet.http.HttpServletResponse;
+
+public class SaveProjectAction extends ConfluenceActionSupport implements JSONAction
+{
+    private static final String OUT_OF_DATE = "outofdate";
+
+    private String projectName;
+    private String data;
+    private int version;
+
+    private ProjectManager projectManager;
+    private StoredProject project;
+
+    public String execute()
+    {
+        try
+        {
+            projectManager.updateProject(projectName, version, data);
+            return SUCCESS;
+        }
+        catch (DataOutOfDateException e)
+        {
+            // Any attempt to brew coffee with a teapot should result in the
+            // error code "418 I'm a teapot". The resulting entity body MAY be
+            // short and stout. -- RFC 2324
+            ServletActionContext.getResponse().setStatus(HttpServletResponse.SC_CONFLICT);
+            project = projectManager.loadProject(projectName);
+            return OUT_OF_DATE;
+        }
+    }
+
+    public String getJSONString() throws JSONException
+    {
+        return project.getJSONString();
+    }
+
+    public String getProjectName()
+    {
+        return projectName;
+    }
+
+    public void setProjectName(String projectName)
+    {
+        this.projectName = projectName;
+    }
+
+    public void setData(String data)
+    {
+        this.data = data;
+    }
+
+    public void setVersion(int version)
+    {
+        this.version = version;
+    }
+
+    public void setBandanaManager(BandanaManager bandanaManager)
+    {
+        this.projectManager = new ProjectManager(bandanaManager);
+    }
+}

src/main/java/com/atlassian/confluence/plugin/planning/StoredProject.java

+package com.atlassian.confluence.plugin.planning;
+
+import java.io.Serializable;
+
+public class StoredProject implements Serializable
+{
+    // XStream-injected - can't be final 
+    private String name;
+    private int version;
+    private String data;
+
+    public StoredProject(String name, int version, String data) {
+        this.name = name;
+        this.version = version;
+        this.data = data;
+    }
+
+    public StoredProject(String name, String data) {
+        this(name, 0, data);
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public int getVersion() {
+        return version;
+    }
+
+    public String getData() {
+        return data;
+    }
+
+    public String getJSONString() {
+        return "{ name: \"" + name.replace("\"", "\\\"") + "\",\n" +
+            "version: " + version + ",\n" +
+            "data: " + data + "}\n";
+    }
+
+    /**
+     * Returns a copy of this project with updated data and an incremented version number.
+     *
+     * @throws DataOutOfDateException if the provided version does not match the current data version
+     */
+    public StoredProject updateData(int version, String data) throws DataOutOfDateException {
+        if (version != this.version)
+            throw new DataOutOfDateException();
+        return new StoredProject(name, this.version + 1, data);
+    }
+}

src/main/java/com/atlassian/confluence/plugin/planning/ViewProjectAction.java

+package com.atlassian.confluence.plugin.planning;
+
+import com.atlassian.bandana.BandanaManager;
+import com.atlassian.confluence.core.ConfluenceActionSupport;
+import com.atlassian.confluence.json.JSONAction;
+
+public class ViewProjectAction extends ConfluenceActionSupport implements JSONAction
+{
+    private String projectName;
+    private StoredProject project;
+    private ProjectManager projectManager;
+
+    public String execute()
+    {
+        project = loadStoredProject(projectName);
+        return SUCCESS;
+    }
+
+    private StoredProject loadStoredProject(String projectName)
+    {
+        return projectManager.loadProject(projectName);
+    }
+
+    public String getJSONString()
+    {
+        return project.getJSONString();
+    }
+
+    public void setProjectName(String projectName)
+    {
+        this.projectName = projectName;
+    }
+
+    public void setBandanaManager(BandanaManager bandanaManager)
+    {
+        this.projectManager = new ProjectManager(bandanaManager);
+    }
+}

src/main/resources/atlassian-plugin.xml

     <xwork key="planning-actions" name="Planning Plugin XWork Actions">
         <package name="planning" extends="default" namespace="/plugins/planning">
             <default-interceptor-ref name="defaultStack"/>
-            <action name="project" class="com.atlassian.confluence.plugin.planning.ProjectAction">
+            <action name="project" class="com.atlassian.confluence.plugin.planning.ViewProjectAction">
                 <result name="success" type="json"/>
             </action>
-            <action name="saveproject" class="com.atlassian.confluence.plugin.planning.ProjectAction" method="save">
+            <action name="saveproject" class="com.atlassian.confluence.plugin.planning.SaveProjectAction">
+                <result name="outofdate" type="json"/>
                 <result name="success" type="redirect">/plugins/planning/project.action?projectName=${projectName}</result>
             </action>
         </package>

src/main/resources/planning/images/handle.png

Added
New image

src/main/resources/planning/project-plan-macro.vm

 
 <script type="text/x-template" title="iteration-template">
 <li class="iteration">
+    <span class="handle" title="Drag to move iteration">&bull;</span>
+    <span class="status">Status</span>
     <h4 class="title editable"></h4>
     <span class="estimate">0d</span>
     <span class="notes">&nbsp;</span>
-    <span class="handle" title="Drag to move iteration">&bull;</span>
     <ul class="operations">
         <li><a href="#" class="add-task">Add task</a></li>
     </ul>
 </script>
 <script type="text/x-template" title="task-template">
 <li class="task">
-    <span class="status">Status</span>
+    <span class="handle" title="Drag to reorder task">&bull;</span>
+    <span class="status" title="Click to toggle task status">Status</span>
     <span class="title editable"></span>
     <span class="estimate editable">4h</span>
     <span class="notes editable">&nbsp;</span>
-    <span class="handle" title="Drag to reorder task">&bull;</span>
 </li>
 </script>

src/main/resources/planning/project.css

 .project,
 .project input {
     font-family: Helvetica, sans-serif;
-    font-size: 12px;
 }
 .project a {
     color: #36a;
     padding: 0;
     list-style: none;
 }
+/* Fonts - very specific to override Confluence styles */
+.project,
+.project li,
+.project input {
+    font-size: 12px;
+}
 
 /* Editable content styles */
 .editable-enabled .editable:hover {
 .project h4 {
     font-weight: normal;
     margin: 0;
-    margin-left: 20px;
 }
 .project h3 {
+    color: #fff !important; /* override Confluence colors */
     font-size: 1.2em;
     line-height: 24px;
-    color: #fff !important; /* override Confluence colors */
+    margin-left: 5px;
 }
 .project h4 {
     font-size: 1em;
 }
 .project .iteration,
 .project .headings {
-    padding: 2px 5px;
+    padding: 2px 0 2px 5px;
 }
 .project .iteration {
     border-top: 1px solid #ddd;
     margin-top: 10px;
 }
 .project .iteration .operations {
-    border-right: 1px solid #bbb;
     height: 20px;
-    margin: -2px 10px;
+    margin: -2px 0;
 }
 .project .iteration .operations a {
     border-left: 1px solid #bbb;
     text-decoration: none;
 }
 .project .iteration,
+.project .iteration h4, /* override Confluence specific styles */
 .project .task {
     line-height: 16px;
 }
 .project h3,
 .project h4,
+.project .handle,
 .project .status,
 .project .title,
 .project .estimate,
 .project .notes {
     float: left;
 }
-.project .handle {
-    float: right;
-}
 .project .title {
     width: 400px;
 }
     background-image: url(images/confluence/check.png);
     opacity: 1;
 }
+.project .iteration.complete .status {
+    background-image: url(images/confluence/check.png);
+    opacity: 0.3;
+}
+.project .headings .status {
+    visibility: hidden;
+}
 
 /* Drag handle */
 .project .handle {
-    background: url(images/grip-horizontal.png) no-repeat;
+    background: url(images/handle.png) repeat-y;
     cursor: move;
-    height: 16px;
+    height: 15px;
     overflow: hidden;
-    margin: 0 5px 0 0;
+    margin: 1px 5px 0 0;
     text-indent: -9999px;
-    width: 14px;
+    width: 11px;
 }
 
 /* Column headings */

src/main/resources/planning/project.js

         var $el = $(el);
         var $items = $el.find(".items");
         var backlog = new Iteration(this, "Backlog");
-        backlog.$el.attr("id", "backlog").find(".title").removeClass("editable");
+        backlog.$el.addClass("backlog").find(".title").removeClass("editable");
         $items.append(backlog.$el);
         this.$el = $el;
+        this.version = 0; // overridden when data is loaded
         var loadingData = false;
         this.getBacklog = function () {
             return backlog;
             }
         });
 
-        this.setData = function (data) {
+        this.setData = function (newProject) {
             loadingData = true; // prevent recalculation while loading data
             var project = this;
+            project.version = newProject.version;
+            $el.find(".items > :not(.backlog)").remove(); // remove existing elements
+            project.iterations = []; // remove existing data
             var iteration, currentIteration, task, currentTask;
-            for (var i=0; i<data.iterations.length; i++) {
-                iteration = data.iterations[i];
+            for (var i=0; i<newProject.data.iterations.length; i++) {
+                iteration = newProject.data.iterations[i];
                 currentIteration = (iteration.title == "Backlog") ?
                     project.getBacklog() :
                     project.addIteration(iteration.title);
 
         this.getData = function () {
             var data = {
-                name: this.getTitle(),
                 iterations: []
             };
             $el.find(".iteration").each(function () {
                 dataType: 'json',
                 data: {
                     projectName: obj.getTitle(),
+                    version: obj.version,
                     data: JSON.stringify(obj.getData())
                 },
-                error: function (message) {
-                    alert(message);
+                error: function (xhr, message) {
+                    if (xhr.status == 409)
+                    {
+                        alert("This project has already been updated by another user.\n\n" +
+                              "Your last change will be reverted and the project updated to the latest version.");
+                        var data = xhr.responseText;
+                        data = jQuery.parseJSON ? jQuery.parseJSON(data) : window["eval"]("(" + data + ")");
+                        obj.setData(data);
+                    }
+                    else
+                    {
+                        alert("Unknown error updating project data: " + message);
+                        console.log(xhr);
+                    }
                 },
                 success: function (data) {
-                    console.log("Successfully updated data");
-                    console.log(data);
+                    console.log("Successfully updated data to version: " + data.version);
+                    obj.version = data.version;
                 }
             });
             this.recalculate();