Commits

Joseph Walton committed ad47d6e Merge

Deployable as a Confluence plugin.

Comments (0)

Files changed (10)

     <properties>
         <maven.compiler.source>1.6</maven.compiler.source>
         <maven.compiler.target>1.6</maven.compiler.target>
+        <confluence.version>4.1.6</confluence.version>
+        <amps.version>3.7.3</amps.version>
     </properties>
 
     <dependencies>
         <dependency>
+            <groupId>com.atlassian.confluence</groupId>
+            <artifactId>confluence</artifactId>
+            <version>${confluence.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.atlassian.plugins.rest</groupId>
+            <artifactId>atlassian-rest-common</artifactId>
+            <version>2.5.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>org.incava</groupId>
             <artifactId>java-diff</artifactId>
             <version>1.1</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
+    
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>com.atlassian.maven.plugins</groupId>
+                <artifactId>maven-amps-plugin</artifactId>
+                <version>${amps.version}</version>
+                <extensions>true</extensions>
+                <configuration>
+                    <product>confluence</product>
+                    <productVersion>${confluence.version}</productVersion>
+                    <productDataVersion>3.5</productDataVersion>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
 </project>

src/main/java/ShowBlamed.java

 
                 return String.format("#%06X", 0xFFFFFF & Color.HSBtoRGB(hue, saturation, brightness));
             }
+
+            @Override
+            public String getRevLabel(Revision<SFThing> r)
+            {
+                return ch.revDetails.get(r).revLabel;
+            }
         };
 
         op.startHtmlBody();

src/main/java/com/atlassian/labs/fedex19/blame/BlameServlet.java

+package com.atlassian.labs.fedex19.blame;
+
+import java.awt.Color;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.List;
+import java.util.Random;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.parsers.ParserConfigurationException;
+
+import com.atlassian.confluence.content.render.xhtml.DefaultConversionContext;
+import com.atlassian.confluence.content.render.xhtml.Renderer;
+import com.atlassian.confluence.core.VersionHistorySummary;
+import com.atlassian.confluence.pages.AbstractPage;
+import com.atlassian.confluence.pages.PageManager;
+import com.atlassian.confluence.renderer.PageContext;
+import com.atlassian.confluence.security.PermissionManager;
+
+import com.megginson.sax.XMLWriter;
+
+import org.xml.sax.SAXException;
+
+public class BlameServlet extends HttpServlet
+{
+    protected PermissionManager permissionManager;
+    protected PageManager pageManager;
+    private Renderer renderer;
+
+    public void setPermissionManager(PermissionManager permissionManager)
+    {
+        this.permissionManager = permissionManager;
+    }
+
+    public void setPageManager(PageManager pageManager)
+    {
+        this.pageManager = pageManager;
+    }
+
+    public void setRenderer(Renderer renderer)
+    {
+        this.renderer = renderer;
+    }
+
+    @Override
+    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
+    {
+        String p = req.getParameter("pageId");
+        if (p == null)
+        {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Provide a page ID");
+            return;
+        }
+
+        long pageId;
+
+        try
+        {
+            pageId = Long.parseLong(p);
+        }
+        catch (NumberFormatException nfe)
+        {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Page IDs are numeric");
+            return;
+        }
+
+        AbstractPage ap = pageManager.getAbstractPage(pageId);
+
+        if (ap == null)
+        {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Page not found");
+        }
+
+        resp.setContentType("text/html");
+
+//        PrintWriter w = resp.getWriter();
+
+//        w.write("Current version: " + ap.getVersion() + "\n");
+
+        List<VersionHistorySummary> summaries = pageManager.getVersionHistorySummaries(ap);
+
+        ConfluenceHistory history = new ConfluenceHistory();
+
+        for (VersionHistorySummary vhs : summaries)
+        {
+//            w.write(vhs.getVersion() + "," + vhs.getLastModifierName() + "," + vhs.getId() + "\n");
+//
+//            w.write(pageManager.getPage(vhs.getId()).getBodyAsString());
+
+            try
+            {
+                history.addRevision(
+                        pageManager.getPage(vhs.getId()).getBodyAsString(),
+                        Integer.toString(vhs.getVersion()),
+                        vhs.getLastModifierName(), vhs.getLastModificationDate());
+            }
+            catch (SAXException e)
+            {
+                throw new ServletException(e);
+            }
+            catch (ParserConfigurationException e)
+            {
+                throw new ServletException(e);
+            }
+        }
+
+        // Not strictly XHTML now....
+        resp.getWriter().println("<!DOCTYPE html><html><head><meta name='decorator' content='atl.general'/><title>Blame</title><meta charset='UTF-8'>");
+        resp.getWriter().println("<body>");
+
+        StringWriter sw = new StringWriter();
+
+        try
+        {
+            write(sw, history);
+        }
+        catch (SAXException e)
+        {
+            throw new ServletException(e);
+        }
+
+        String s = sw.toString();
+
+        /* Drop the XML declaration */
+        int i = s.indexOf("?>");
+        if (i >= 0)
+        {
+            s = s.substring(i + 2);
+        }
+
+        resp.getWriter().println(renderer.render(s, new DefaultConversionContext(ap.toPageContext())));
+
+        resp.getWriter().println("</body></html>");
+        resp.getWriter().close();
+    }
+
+//    static String marker = "HACKHACKHACK";
+
+    void write(Writer w, final ConfluenceHistory history) throws SAXException, IOException
+    {
+        XMLWriter xw = new XMLWriter(w);
+
+        OutputPage op = new OutputPage(xw){
+            @Override
+            public String titleFor(Revision<SFThing> r)
+            {
+                return history.revDetails.get(r).toString();
+            }
+
+            @Override
+            public String randomColour(Revision<SFThing> r)
+            {
+                float hue = (float) Math.random();
+                hue = new Random(history.revDetails.get(r).who.hashCode()).nextFloat() + (float) (Math.random() / 5);
+                float saturation = 0.2f + (float) (Math.random() / 5);
+                float brightness = 0.85f;
+
+                return String.format("#%06X", 0xFFFFFF & Color.HSBtoRGB(hue, saturation, brightness));
+            }
+
+            @Override
+            public String getRevLabel(Revision<SFThing> r)
+            {
+                return history.revDetails.get(r).revLabel;
+            }
+        };
+
+        op.startHtmlBody();
+//        xw.characters(marker);
+        op.writeBlamed(Blamer.blame(history.revisions));
+        op.writeRevisionKey(xw);
+        op.endHtmlBody();
+
+//        w.close();
+    }
+}

src/main/java/com/atlassian/labs/fedex19/blame/ConfluenceHistory.java

 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 
 public class ConfluenceHistory
 {
-    public List<Revision<SFThing>> revisions;
-    public Map<Revision<SFThing>, RevDetails> revDetails;
+    public List<Revision<SFThing>> revisions = new ArrayList<Revision<SFThing>>();
+    public Map<Revision<SFThing>, RevDetails> revDetails = new HashMap<Revision<SFThing>, RevDetails>();
 
     public static ConfluenceHistory load(File f) throws IOException, ParserConfigurationException, SAXException
     {
 
         ConfluenceHistory ch = new ConfluenceHistory();
 
-        ch.revisions = new ArrayList<Revision<SFThing>>();
-        ch.revDetails = new HashMap<Revision<SFThing>, RevDetails>();
+        int id = lines.size();
 
         for (String l : lines.subList(1, lines.size()))
         {
             Date when = new Date(sa[1]);
             String who = sa[2];
 
-            Revision<SFThing> rev = new Revision<SFThing>(StorageFormatTokens.byWordFragments().parse(contents));
-            ch.revisions.add(rev);
-
-            RevDetails rd = new RevDetails();
-            rd.who = who;
-            rd.when = when;
-            ch.revDetails.put(rev, rd);
+            ch.addRevision(contents, Integer.toString(id--), who, when);
         }
 
-        Collections.reverse(ch.revisions);
-
         return ch;
     }
+
+    private void addRevision(Revision<SFThing> rev, RevDetails rd)
+    {
+        revisions.add(0, rev);
+        revDetails.put(rev, rd);
+    }
+
+    public void addRevision(Revision<SFThing> rev, String revLabel, String who, Date when)
+    {
+        RevDetails rd = new RevDetails();
+        rd.revLabel = revLabel;
+        rd.who = who;
+        rd.when = when;
+        addRevision(rev, rd);
+    }
+
+    public void addRevision(String storageFormatContents, String revLabel, String who, Date when) throws IOException, ParserConfigurationException, SAXException
+    {
+        Revision<SFThing> rev = new Revision<SFThing>(StorageFormatTokens.byWordFragments().parse(storageFormatContents));
+
+        addRevision(rev, revLabel, who, when);
+    }
 }

src/main/java/com/atlassian/labs/fedex19/blame/OutputPage.java

 {
     private final XMLWriter xw;
 
+    private final boolean asHtml;
+
     public OutputPage(XMLWriter xw)
     {
+        this(xw, false);
+    }
+
+    public OutputPage(XMLWriter xw, boolean asHtml)
+    {
         this.xw = xw;
+        this.asHtml = asHtml;
     }
 
     Map<Revision<SFThing>, String> colours = new TreeMap<Revision<SFThing>, String>();
         return String.format("#%06x", c);
     }
 
+    public String getRevLabel(Revision<SFThing> r)
+    {
+        return r.toString();
+    }
+
     public void writeRevisionKey(XMLWriter xw) throws SAXException
     {
         xw.startElement(StorageFormatTokens.XHTML_NS, "p");
         for (Map.Entry<Revision<SFThing>, String> e : colours.entrySet())
         {
             xw.startElement(StorageFormatTokens.XHTML_NS, "span", "span", attsForRevision(e.getKey()));
-            xw.characters(e.getKey().toString());
+            xw.characters(getRevLabel(e.getKey()));
             xw.endElement(StorageFormatTokens.XHTML_NS, "span");
         }
         xw.endElement(StorageFormatTokens.XHTML_NS, "p");
                 }
                 t.write(xw);
 
-//                xw.startElement(StorageFormatTokens.XHTML_NS, "span", "span", attsForRevision(b.getRevision()));
-//                xw.characters("(");
-//                xw.endElement(StorageFormatTokens.XHTML_NS, "span");
+                if (asHtml)
+                {
+                    xw.startElement(StorageFormatTokens.XHTML_NS, "span", "span", attsForRevision(b.getRevision()));
+                    xw.characters("(");
+                    xw.endElement(StorageFormatTokens.XHTML_NS, "span");
+                }
             }
             else if (t instanceof SFEndElement)
             {
                     xw.endElement(StorageFormatTokens.XHTML_NS, "span");
                     inRev = null;
                 }
-//                xw.startElement(StorageFormatTokens.XHTML_NS, "span", "span", attsForRevision(b.getRevision()));
-//                xw.characters(")");
-//                xw.endElement(StorageFormatTokens.XHTML_NS, "span");
+
+                if (asHtml)
+                {
+                    xw.startElement(StorageFormatTokens.XHTML_NS, "span", "span", attsForRevision(b.getRevision()));
+                    xw.characters(")");
+                    xw.endElement(StorageFormatTokens.XHTML_NS, "span");
+                }
 
                 t.write(xw);
             }
         }
     }
 
-    public void startHtmlBody() throws SAXException
+    public void start() throws SAXException
     {
         xw.startDocument();
         xw.setPrefix(StorageFormatTokens.XHTML_NS, "");
+    }
+
+    public void startHtmlBody() throws SAXException
+    {
+        start();
 
         xw.startElement(StorageFormatTokens.XHTML_NS, "html");
         xw.startElement(StorageFormatTokens.XHTML_NS, "body");

src/main/java/com/atlassian/labs/fedex19/blame/RevDetails.java

 {
     public Date when;
     public String who;
+    public String revLabel;
 
     public String toString()
     {
         return who + ", " + when;
     }
-}
+}

src/main/java/com/atlassian/labs/fedex19/blame/SFThing.java

 package com.atlassian.labs.fedex19.blame;
+import java.util.Collections;
+import java.util.Map;
+
 import com.megginson.sax.XMLWriter;
 
+import org.xml.sax.Attributes;
 import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
 
 
 
 
     public static SFThing ofStartElement(String name)
     {
-        return new SFStartElement(name);
+        return new SFStartElement(name, Collections.<String, String>emptyMap());
+    }
+
+    public static SFThing ofStartElement(String name, Map<String, String> attributes)
+    {
+        return new SFStartElement(name, attributes);
     }
 
     public static SFThing ofEndElement(String name)
 
 class SFStartElement extends SFThingWithString
 {
-    public SFStartElement(String s)
+    private final Map<String, String> attributes;
+
+    public SFStartElement(String s, Map<String, String> attributes)
     {
         super(s);
+        this.attributes = attributes;
     }
 
     @Override
     public String toString()
     {
-        return "start:" + s;
+        if (attributes.isEmpty())
+        {
+            return "start:" + s;
+        }
+        else
+        {
+            return "start:" + s + " " + attributes;
+        }
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return super.hashCode() ^ attributes.hashCode();
+    };
+
+    @Override
+    public boolean equals(Object obj)
+    {
+        return super.equals(obj) && attributes.equals(((SFStartElement)obj).attributes);
     }
 
     @Override
     void write(XMLWriter ch) throws SAXException
     {
+        AttributesImpl attrs = new AttributesImpl();
+
+        for (Map.Entry<String, String> e : attributes.entrySet())
+        {
+            attrs.addAttribute("", e.getKey(), "", "CDATA", e.getValue());
+        }
+
         if (s.contains(":"))
         {
-            ch.startElement(StorageFormatTokens.XHTML_NS, "tt");
-            ch.characters("<" + s + ">");
-            ch.endElement(StorageFormatTokens.XHTML_NS, "tt");
+            ch.startElement("", s, "", attrs);
+//            ch.startElement(StorageFormatTokens.XHTML_NS, "tt");
+//            ch.characters("<" + s + ">");
+//            ch.endElement(StorageFormatTokens.XHTML_NS, "tt");
         }
         else
         {
-            ch.startElement(StorageFormatTokens.XHTML_NS, s);
+            ch.startElement(StorageFormatTokens.XHTML_NS, s, "", attrs);
         }
     }
 }
     {
         if (s.contains(":"))
         {
-            ch.startElement(StorageFormatTokens.XHTML_NS, "tt");
-            ch.characters("</" + s + ">");
-            ch.endElement(StorageFormatTokens.XHTML_NS, "tt");
+            ch.endElement(s);
+//            ch.startElement(StorageFormatTokens.XHTML_NS, "tt");
+//            ch.characters("</" + s + ">");
+//            ch.endElement(StorageFormatTokens.XHTML_NS, "tt");
         }
         else
         {

src/main/java/com/atlassian/labs/fedex19/blame/StorageFormatTokens.java

 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 
 import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.parsers.SAXParser;
     @Override
     public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
     {
-        things.add(SFThing.ofStartElement(qName));
+        Map<String, String> attrs = new HashMap<String, String>();
+
+        for (int i = 0; i < attributes.getLength(); i++)
+        {
+            attrs.put(attributes.getQName(i), attributes.getValue(i));
+        }
+
+        things.add(SFThing.ofStartElement(qName, attrs));
     }
 
     @Override

src/main/resources/atlassian-plugin.xml

+<atlassian-plugin name='Confluence Blame' key='com.atlassian.labs.fedex19.blame' pluginsVersion="2">
+    <plugin-info>
+        <description>Blame annotations for Confluence</description>
+        <vendor name="Atlassian Software" url="http://www.atlassian.com/"/>
+        <version>0.1</version>
+    </plugin-info>
+
+    <servlet name="Confluence Blame" key="blamesrv" class="com.atlassian.labs.fedex19.blame.BlameServlet">
+        <description>Provides blame annotations for Confluence.</description>
+        <url-pattern>/blame</url-pattern>
+    </servlet>
+
+    <web-item key='blamelink' name='Blame link' section='system.content.action/primary' weight='20'>
+        <label key='blame'/>
+        <link linkId='blame-link'>/plugins/servlet/blame?pageId=$page.id</link>
+    </web-item>
+</atlassian-plugin>

src/test/java/com/atlassian/labs/fedex19/blame/StorageFormatTokensTest.java

 
 import org.junit.Test;
 
+import static org.junit.Assert.assertFalse;
+
 import static org.junit.Assert.assertEquals;
 
 
                 ),
                 sft.parse("Test"));
     }
+
+    @Test
+    public void parsingContentWithNamespaces() throws Exception
+    {
+        StorageFormatTokens sft = new StorageFormatTokens();
+
+        List<SFThing> expected = Arrays.asList(
+                SFThing.ofStartElement("ac:link"),
+                SFThing.ofStartElement("ri:user"),
+                SFThing.ofEndElement("ri:user"),
+                SFThing.ofEndElement("ac:link"));
+
+        assertEquals(expected,
+                sft.parse("<ac:link><ri:user /></ac:link>"));
+    }
+
+    @Test
+    public void elementsStartsWithDifferentAttributesAreDifferent()
+    {
+        SFThing a = SFThing.ofStartElement("ri:user", Collections.singletonMap("ri:username", "user"));
+        SFThing b = SFThing.ofStartElement("ri:user");
+
+        assertFalse(a.equals(b));
+    }
+
+    @Test
+    public void parsingContentWithAttributes() throws Exception
+    {
+        StorageFormatTokens sft = new StorageFormatTokens();
+
+        List<SFThing> expected = Arrays.asList(
+                SFThing.ofStartElement("ac:link"),
+                SFThing.ofStartElement("ri:user", Collections.singletonMap("ri:username", "user")),
+                SFThing.ofEndElement("ri:user"),
+                SFThing.ofEndElement("ac:link"));
+
+        assertEquals(expected,
+                sft.parse("<ac:link><ri:user ri:username=\"user\" /></ac:link>"));
+    }
 }