You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@roller.apache.org by sn...@apache.org on 2005/09/21 19:08:35 UTC

svn commit: r290745 - in /incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04: ./ AtomCollection.java AtomHandler.java AtomService.java AtomServlet.java RollerAtomHandler.java WSSEUtilities.java package.html

Author: snoopdave
Date: Wed Sep 21 10:08:31 2005
New Revision: 290745

URL: http://svn.apache.org/viewcvs?rev=290745&view=rev
Log:
Starting new Atom Protocol impl

Added:
    incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/
    incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomCollection.java
    incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomHandler.java
    incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomService.java
    incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomServlet.java
    incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/RollerAtomHandler.java
    incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/WSSEUtilities.java
    incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/package.html

Added: incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomCollection.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomCollection.java?rev=290745&view=auto
==============================================================================
--- incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomCollection.java (added)
+++ incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomCollection.java Wed Sep 21 10:08:31 2005
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2005 David M Johnson (For RSS and Atom In Action)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.roller.presentation.atomapi04;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.Namespace;
+
+/**
+ * Models an Atom collection.
+ * 
+ * @author Dave Johnson
+ */
+/*
+ * Based on: draft-ietf-atompub-protocol-04.txt 
+ * 
+ * appCollection = element
+ *    app:collection { 
+ *       attribute next { text } ?, 
+ *       appMember* 
+ *    }
+ * 
+ * Here is an example Atom collection:
+ * 
+ * <?xml version="1.0" encoding='utf-8'?> 
+ * <collection xmlns="http://purl.org/atom/app#"> 
+ * <member href="http://example.org/1"
+ *    hrefreadonly="http://example.com/1/bar" 
+ *    title="Sample 1"
+ *    updated="2003-12-13T18:30:02Z" /> 
+ * <member href="http://example.org/2"
+ *    hrefreadonly="http://example.com/2/bar" 
+ *    title="Sample 2"
+ *    updated="2003-12-13T18:30:02Z" /> 
+ * <member href="http://example.org/3"
+ *    hrefreadonly="http://example.com/3/bar" 
+ *    title="Sample 3"
+ *    updated="2003-12-13T18:30:02Z" /> 
+ * <member href="http://example.org/4"
+ *    title="Sample 4" 
+ *    updated="2003-12-13T18:30:02Z" /> 
+ * </collection>
+ */
+public class AtomCollection
+{
+    public static final Namespace ns = 
+        Namespace.getNamespace("http://purl.org/atom/app#");
+    
+    private static SimpleDateFormat df =
+        new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ssZ" );
+    private String next    = null;
+    private List   members = new ArrayList();
+
+    public AtomCollection()
+    {
+    }
+
+    /** URI of collection containing member elements updated earlier in time */
+    public String getNext()
+    {
+        return next;
+    }
+
+    public void setNext(String next)
+    {
+        this.next = next;
+    }
+
+    public List getMembers()
+    {
+        return members;
+    }
+
+    public void setMembers(List members)
+    {
+        this.members = members;
+    }
+
+    public void addMember(Member member)
+    {
+        members.add(member);
+    }
+
+    /** Models an Atom collection member */
+    /*
+     * appMember = element app:member { attribute title { text }, attribute href {
+     * text }, attribute hrefreadonly { text } ?, attribute updated { text } }
+     */
+    public static class Member
+    {
+        private String title;
+        private String href;
+        private String hrefreadonly;
+        private Date   updated;
+
+        public Member()
+        {
+        }
+
+        /** Human readable title */
+        public String getTitle()
+        {
+            return title;
+        }
+
+        public void setTitle(String title)
+        {
+            this.title = title;
+        }
+
+        /** The URI used to edit the member source */
+        public String getHref()
+        {
+            return href;
+        }
+
+        public void setHref(String href)
+        {
+            this.href = href;
+        }
+
+        /** The URI for readonly access to member source */
+        public String getHrefreadonly()
+        {
+            return hrefreadonly;
+        }
+
+        public void setHrefreadonly(String hrefreadonly)
+        {
+            this.hrefreadonly = hrefreadonly;
+        }
+
+        /** Same as updated value of collection member */
+        public Date getUpdated()
+        {
+            return updated;
+        }
+
+        public void setUpdated(Date updated)
+        {
+            this.updated = updated;
+        }
+    }
+
+    /** Deserialize an Atom Collection XML document into an object */
+    public static AtomCollection documentToCollection(Document document)
+            throws Exception
+    {
+        AtomCollection collection = new AtomCollection();
+        Element root = document.getRootElement();
+        if (root.getAttribute("next") != null)
+        {
+            collection.setNext(root.getAttribute("next").getValue());
+        }
+        List mems = root.getChildren("member", ns);
+        Iterator iter = mems.iterator();
+        while (iter.hasNext())
+        {
+            Element e = (Element) iter.next();
+            collection.addMember(AtomCollection.elementToMember(e));
+        }
+        return collection;
+    }
+
+    /** Serialize an AtomCollection object into an XML document */
+    public static Document collectionToDocument(AtomCollection collection)
+    {
+        Document doc = new Document();
+        Element root = new Element("collection", ns);
+        doc.setRootElement(root);
+        if (collection.getNext() != null)
+        {
+            root.setAttribute("next", collection.getNext());
+        }
+        Iterator iter = collection.getMembers().iterator();
+        while (iter.hasNext())
+        {
+            Member member = (Member) iter.next();
+            root.addContent(AtomCollection.memberToElement(member));
+        }
+        return doc;
+    }
+
+    /** Deserialize an Atom collection member XML element into an object */
+    public static Member elementToMember(Element element) throws Exception
+    {
+        Member member = new Member();
+        member.setTitle(element.getAttribute("title").getValue());
+        member.setHref(element.getAttribute("href").getValue());
+        if (element.getAttribute("href") != null)
+        {
+            member.setHref(element.getAttribute("href").getValue());
+        }
+        member.setUpdated(df.parse(element.getAttribute("updated").getValue()));
+        return member;
+    }
+
+    /** Serialize a collection member into an XML element */
+    public static Element memberToElement(Member member)
+    {
+        Element element = new Element("member", ns);
+        element.setAttribute("title", member.getTitle()); // TODO: escape/strip HTML?
+        element.setAttribute("href", member.getHref());
+        if (member.getHrefreadonly() != null)
+        {
+            element.setAttribute("hrefreadonly", member.getHrefreadonly());
+        }
+        element.setAttribute("updated", df.format(member.getUpdated()));
+        return element;
+    }
+
+    /** Start and end date range */
+    public static class Range { Date start=null; Date end=null; }
+    
+    /** Parse HTTP Range header into a start and end date range */
+    public static Range parseRange(String rangeString) throws ParseException 
+    {
+        // Range: updated=<isodate>/<isodate>   
+        // Range: updated=<isodate>/ 
+        // Range: updated=/<isodate>  
+
+        Range range = new Range();
+        String[] split = rangeString.split("=");
+        if (split[1].startsWith("/")) 
+        {
+            // we have only end date
+            range.end = df.parse(split[1].split("/")[1]);
+        }
+        else if (split[1].endsWith("/"))
+        {
+            // we have only start date
+            range.start = df.parse(split[1].split("/")[0]);
+        }
+        else
+        {
+            // both dates present
+            String[] dates = split[1].split("/");
+            range.start = df.parse(dates[0]);
+            range.end = df.parse(dates[1]);
+        }
+        return range;
+    }
+}

Added: incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomHandler.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomHandler.java?rev=290745&view=auto
==============================================================================
--- incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomHandler.java (added)
+++ incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomHandler.java Wed Sep 21 10:08:31 2005
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2005 David M Johnson (For RSS and Atom In Action)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.roller.presentation.atomapi04;
+
+import java.io.InputStream;
+import java.util.Date;
+
+import com.sun.syndication.feed.atom.Entry;
+
+/**
+ * Interface to be supported by an Atom server, expected lifetime: one request.
+ * AtomServlet calls this generic interface instead of Roller specific APIs. 
+ * Does not impose any specific set of collections, just three collection types: 
+ * entries, resources and categories. Implementations determine what collections 
+ * of each type exist and what URIs are used to get and edit them.
+ * <p />
+ * Designed to be Roller independent.
+ * 
+ * @author David M Johnson
+ */
+public interface AtomHandler
+{   
+    /** Get username of authenticated user */
+    public String getAuthenticatedUsername();    
+
+    /**
+     * Return introspection document
+     */
+    public AtomService getIntrospection(String[] pathInfo) throws Exception;
+    
+    /**
+     * Return collection
+     * @param pathInfo Used to determine which collection
+     */   
+    public AtomCollection getCollection(String[] pathInfo) throws Exception;
+    
+    /**
+     * Return collection restricted by date range
+     * @param pathInfo Used to determine which collection
+     * @param start    Start date or null if none
+     * @param end      End date or null of none
+     * @param offset   Offset into query results (or -1 if none)
+     */
+    public AtomCollection getCollection(
+            String[] pathInfo, Date start, Date end, int offset) 
+        throws Exception; 
+    
+    /**
+     * Create a new entry specified by pathInfo and posted entry.
+     * @param pathInfo Path info portion of URL
+     */
+    public Entry postEntry(String[] pathInfo, Entry entry) throws Exception;
+
+    /**
+     * Get entry specified by pathInfo.
+     * @param pathInfo Path info portion of URL
+     */
+    public Entry getEntry(String[] pathInfo) throws Exception;
+    
+    /**
+     * Update entry specified by pathInfo and posted entry.
+     * @param pathInfo Path info portion of URL
+     */
+    public Entry putEntry(String[] pathInfo, Entry entry) throws Exception;
+
+    /**
+     * Delete entry specified by pathInfo.
+     * @param pathInfo Path info portion of URL
+     */
+    public void deleteEntry(String[] pathInfo) throws Exception;
+    
+    /**
+     * Create a new resource specified by pathInfo, contentType, and binary data
+     * @param pathInfo Path info portion of URL
+     * @param contentType MIME type of uploaded content
+     * @param data Binary data representing uploaded content
+     */
+    public String postResource(String[] pathInfo, String name, String contentType, 
+            InputStream is) throws Exception;
+
+    /**
+     * Update a resource.
+     * @param pathInfo Path info portion of URL
+     */
+    public void putResource(String[] pathInfo, String contentType, 
+            InputStream is) throws Exception;
+    
+    /**
+     * Delete resource specified by pathInfo.
+     * @param pathInfo Path info portion of URL
+     */
+    public void deleteResource(String[] pathInfo) throws Exception;
+    
+    /**
+     * Get resource file path (so Servlet can determine MIME type).
+     * @param pathInfo Path info portion of URL
+     */
+    public String getResourceFilePath(String[] pathInfo) throws Exception;
+    
+    public boolean isIntrospectionURI(String [] pathInfo);  
+ 
+    public boolean isCollectionURI(String [] pathInfo);   
+    public boolean isEntryCollectionURI(String [] pathInfo);   
+    public boolean isResourceCollectionURI(String [] pathInfo);   
+    public boolean isCategoryCollectionURI(String [] pathInfo);  
+    
+    public boolean isEntryURI(String[] pathInfo);
+    public boolean isResourceURI(String[] pathInfo);
+    public boolean isCategoryURI(String[] pathInfo);
+}
+

Added: incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomService.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomService.java?rev=290745&view=auto
==============================================================================
--- incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomService.java (added)
+++ incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomService.java Wed Sep 21 10:08:31 2005
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2005 David M Johnson (For RSS and Atom In Action)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.roller.presentation.atomapi04;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.Namespace;
+import org.jdom.filter.Filter;
+
+/**
+ * This class models an Atom workspace.
+ * 
+ * @author Dave Johnson
+ */
+/*
+ * Based on: draft-ietf-atompub-protocol-04.txt 
+ * 
+ * appService = 
+ *    element app:service { 
+ *       (appWorkspace* & anyElement* ) 
+ *    }
+ * 
+ * Here is an example Atom workspace:
+ * 
+ * <?xml version="1.0" encoding='utf-8'?> 
+ * <service
+ *    xmlns="http://purl.org/atom/app#"> 
+ *    <workspace title="Main Site" > 
+ *       <collection
+ *          contents="entries" title="My Blog Entries"
+ *          href="http://example.org/reilly/feed" /> 
+ *       <collection contents="generic"
+ *          title="Documents" href="http://example.org/reilly/pic" /> 
+ *    </workspace>
+ *    <workspace title="Side Bar Blog"> 
+ *       <collection contents="entries"
+ *          title="Entries" href="http://example.org/reilly/feed" /> 
+ *       <collection
+ *          contents="http://example.net/booklist" title="Books"
+ *          href="http://example.org/reilly/books" /> 
+ *    </workspace> 
+ * </service>
+ */
+public class AtomService
+{
+    public static final Namespace ns = 
+        Namespace.getNamespace("http://purl.org/atom/app#");
+    
+    private List workspaces = new ArrayList();
+
+    public AtomService()
+    {
+    }
+
+    public void addWorkspace(AtomService.Workspace workspace)
+    {
+        workspaces.add(workspace);
+    }
+
+    public List getWorkspaces()
+    {
+        return workspaces;
+    }
+
+    public void setWorkspaces(List workspaces)
+    {
+        this.workspaces = workspaces;
+    }
+
+    /**
+     * This class models an Atom workspace.
+     * 
+     * @author Dave Johnson
+     */
+    /*
+     * appWorkspace = element app:workspace { attribute title { text }, (
+     * appCollection* & anyElement* ) }
+     */
+    public static class Workspace
+    {
+        private String title       = null;
+        private List   collections = new ArrayList();
+
+        public Workspace()
+        {
+        }
+
+        public List getCollections()
+        {
+            return collections;
+        }
+
+        public void setCollections(List collections)
+        {
+            this.collections = collections;
+        }
+
+        public void addCollection(AtomService.Collection col)
+        {
+            collections.add(col);
+        }
+
+        /** Workspace must have a human readable title */
+        public String getTitle()
+        {
+            return title;
+        }
+
+        public void setTitle(String title)
+        {
+            this.title = title;
+        }
+    }
+
+    /**
+     * This class models an Atom workspace collection.
+     * 
+     * @author Dave Johnson
+     */
+    /*
+     * appCollection = element app:collection { attribute title { text },
+     * attribute contents { text }, attribute href { text }, anyElement* }
+     */
+    public static class Collection
+    {
+        private String title;
+        private String contents = "generic";
+        private String href;
+
+        public Collection()
+        {
+        }
+
+        /**
+         * Contents attribute conveys the nature of a collection's member
+         * resources. May be "entry" or "generic" and defaults to "generic"
+         */
+        public String getContents()
+        {
+            return contents;
+        }
+
+        public void setContents(String contents)
+        {
+            this.contents = contents;
+        }
+
+                /** The URI of the collection */
+        public String getHref()
+        {
+            return href;
+        }
+
+        public void setHref(String href)
+        {
+            this.href = href;
+        }
+
+                /** Must have human readable title */
+        public String getTitle()
+        {
+            return title;
+        }
+
+        public void setTitle(String title)
+        {
+            this.title = title;
+        }
+    }
+
+    /** Deserialize an Atom service XML document into an object */
+    public static AtomService documentToService(Document document)
+    {
+        AtomService service = new AtomService();
+        Element root = document.getRootElement();
+        List spaces = root.getChildren("workspace", ns);
+        Iterator iter = spaces.iterator();
+        while (iter.hasNext())
+        {
+            Element e = (Element) iter.next();
+            service.addWorkspace(AtomService.elementToWorkspace(e));
+        }
+        return service;
+    }
+
+    /** Serialize an AtomService object into an XML document */
+    public static Document serviceToDocument(AtomService service)
+    {
+        Document doc = new Document();
+        Element root = new Element("service", ns);
+        doc.setRootElement(root);
+        Iterator iter = service.getWorkspaces().iterator();
+        while (iter.hasNext())
+        {
+            AtomService.Workspace space = (AtomService.Workspace) iter.next();
+            root.addContent(AtomService.workspaceToElement(space));
+        }
+        return doc;
+    }
+
+    /** Deserialize a Atom workspace XML element into an object */
+    public static AtomService.Workspace elementToWorkspace(Element element)
+    {
+        AtomService.Workspace space = new AtomService.Workspace();
+        space.setTitle(element.getAttribute("title").getValue());
+        List collections = element.getChildren("collection", ns);
+        Iterator iter = collections.iterator();
+        while (iter.hasNext())
+        {
+            Element e = (Element) iter.next();
+            space.addCollection(AtomService.elementToCollection(e));
+        }
+        return space;
+    }
+
+    /** Serialize an AtomService.Workspace object into an XML element */
+    public static Element workspaceToElement(Workspace space)
+    {
+        Namespace ns = Namespace.getNamespace("http://purl.org/atom/app#");
+        Element element = new Element("workspace", ns);
+        element.setAttribute("title", space.getTitle());
+        Iterator iter = space.getCollections().iterator();
+        while (iter.hasNext())
+        {
+            AtomService.Collection col = (AtomService.Collection) iter.next();
+            element.addContent(collectionToElement(col));
+        }
+        return element;
+    }
+
+    /** Deserialize an Atom service collection XML element into an object */
+    public static AtomService.Collection elementToCollection(Element element)
+    {
+        AtomService.Collection collection = new AtomService.Collection();
+        collection.setTitle(element.getAttribute("title").getValue());
+        collection.setHref(element.getAttribute("href").getValue());
+        if (element.getAttribute("href") != null)
+        {
+            collection.setContents(element.getAttribute("contents").getValue());
+        }
+        return collection;
+    }
+
+    /** Serialize an AtomService.Collection object into an XML element */
+    public static Element collectionToElement(AtomService.Collection collection)
+    {
+        Namespace ns = Namespace.getNamespace("http://purl.org/atom/app#");
+        Element element = new Element("collection", ns);
+        element.setAttribute("title", collection.getTitle()); 
+        element.setAttribute("href", collection.getHref());
+        if (collection.getContents() != null)
+        {
+            element.setAttribute("contents", collection.getContents());
+        }
+        return element;
+    }
+}
\ No newline at end of file

Added: incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomServlet.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomServlet.java?rev=290745&view=auto
==============================================================================
--- incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomServlet.java (added)
+++ incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/AtomServlet.java Wed Sep 21 10:08:31 2005
@@ -0,0 +1,399 @@
+/*
+ * Copyright 2005 David M Johnson (For RSS and Atom In Action)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.roller.presentation.atomapi04;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.JDOMException;
+import org.jdom.input.SAXBuilder;
+import org.jdom.output.Format;
+import org.jdom.output.XMLOutputter;
+import org.roller.util.Utilities;
+
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.feed.atom.Feed;
+import com.sun.syndication.feed.atom.Link;
+import com.sun.syndication.io.FeedException;
+import com.sun.syndication.io.WireFeedInput;
+import com.sun.syndication.io.WireFeedOutput;
+import java.io.StringWriter;
+
+/**
+ * Atom Servlet implements Atom by calling a Roller independent handler.
+ * @web.servlet name="AtomServlet"
+ * @web.servlet-mapping url-pattern="/atom04/*"
+ * @author David M Johnson
+ */
+public class AtomServlet extends HttpServlet
+{
+    public static final String FEED_TYPE = "atom_1.0"; 
+    
+    private static Log mLogger = 
+        LogFactory.getFactory().getInstance(AtomServlet.class);
+
+    //-----------------------------------------------------------------------------
+    /**
+     * Create an Atom request handler.
+     * TODO: make AtomRequestHandler implementation configurable.
+     */
+    private AtomHandler createAtomRequestHandler(HttpServletRequest request)
+    {
+        return new RollerAtomHandler(request);   
+    }
+    
+    //-----------------------------------------------------------------------------
+    /**
+     * Handles an Atom GET by calling handler and writing results to response.
+     */
+    protected void doGet(HttpServletRequest req, HttpServletResponse res)
+        throws ServletException, IOException
+    {
+        AtomHandler handler = createAtomRequestHandler(req);
+        String userName = handler.getAuthenticatedUsername();
+        if (userName != null) 
+        {
+            String[] pathInfo = getPathInfo(req);
+            try
+            {
+                if (handler.isIntrospectionURI(pathInfo)) 
+                {
+                    // return an Atom Service document
+                    AtomService service = handler.getIntrospection(pathInfo);                   
+                    Document doc = AtomService.serviceToDocument(service);
+                    Writer writer = res.getWriter();
+                    XMLOutputter outputter = new XMLOutputter();
+                    outputter.setFormat(Format.getPrettyFormat());
+                    outputter.output(doc, writer);
+                    writer.close();
+                    res.setStatus(HttpServletResponse.SC_OK);
+                }
+                else if (handler.isCollectionURI(pathInfo))
+                {
+                    // return a collection
+                    String ranges = req.getHeader("Range");
+                    if (ranges == null) req.getParameter("Range");
+                    AtomCollection col = null;
+                    if (ranges != null) 
+                    {
+                        // return a range of collection members
+                        AtomCollection.Range range = 
+                            AtomCollection.parseRange(req.getHeader("Range"));
+                        int offset = 0;
+                        String offsetString = req.getParameter("offset");
+                        if (offsetString != null) 
+                        {
+                            offset = Integer.parseInt(offsetString);
+                        }
+                        col= handler.getCollection(
+                            pathInfo, range.start, range.end, offset);
+                    }
+                    else 
+                    {
+                        col= handler.getCollection(pathInfo);
+                    }
+                    // serialize collection to XML and write it out
+                    Document doc = AtomCollection.collectionToDocument(col);
+                    Writer writer = res.getWriter();
+                    XMLOutputter outputter = new XMLOutputter();
+                    outputter.setFormat(Format.getPrettyFormat());
+                    outputter.output(doc, writer);
+                    writer.close();
+                    res.setStatus(HttpServletResponse.SC_OK);
+                }
+                else if (handler.isEntryURI(pathInfo)) 
+                {
+                    // return an entry
+                    Entry entry = handler.getEntry(pathInfo);                    
+                    Writer writer = res.getWriter(); 
+                    serializeEntry(entry, writer);                    
+                    writer.close();
+                }
+                else if (handler.isResourceURI(pathInfo))
+                {
+                    // return a resource
+                    String absPath = handler.getResourceFilePath(pathInfo);
+                    String type = getServletContext().getMimeType(absPath);
+                    res.setContentType(type);
+                    Utilities.copyInputToOutput(
+                        new FileInputStream(absPath), res.getOutputStream());
+                }
+                else
+                {
+                    res.setStatus(HttpServletResponse.SC_NOT_FOUND);
+                }
+            }
+            catch (Exception e)
+            {
+                res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+                e.printStackTrace(res.getWriter());
+                mLogger.error(e);
+            }
+        }
+        else 
+        {
+            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        }
+    }
+    
+    //-----------------------------------------------------------------------------  
+    /**
+     * Handles an Atom POST by calling handler to identify URI, reading/parsing
+     * data, calling handler and writing results to response.
+     */
+    protected void doPost(HttpServletRequest req, HttpServletResponse res)
+        throws ServletException, IOException
+    {
+        AtomHandler handler = createAtomRequestHandler(req);
+        String userName = handler.getAuthenticatedUsername();
+        if (userName != null) 
+        {
+            String[] pathInfo = getPathInfo(req);
+            try
+            {
+                if (handler.isEntryCollectionURI(pathInfo)) 
+                {
+                    // parse incoming entry                    
+                    Entry unsavedEntry = parseEntry(
+                        new InputStreamReader(req.getInputStream()));
+                    
+                    // call handler to post it
+                    Entry savedEntry = handler.postEntry(pathInfo, unsavedEntry);
+                    Iterator links = savedEntry.getLinks().iterator();
+                    
+                    // return alternate link as Location header
+                    while (links.hasNext()) {
+                        Link link = (Link) links.next();
+                        if (link.getRel().equals("alternate") || link.getRel() == null) {
+                            res.addHeader("Location", link.getHref());
+                            break;
+                        }
+                    }                  
+                    // write entry back out to response
+                    res.setStatus(HttpServletResponse.SC_CREATED);
+                    Writer writer = res.getWriter(); 
+                    serializeEntry(savedEntry, writer);                    
+                    writer.close();
+                }
+                else if (handler.isResourceCollectionURI(pathInfo)) 
+                {
+                    // get incoming file name from HTTP header
+                    String name = req.getHeader("Name");
+                    
+                    // hand input stream of to hander to post file
+                    String location = handler.postResource(
+                       pathInfo, name, req.getContentType(), req.getInputStream());
+                    res.setStatus(HttpServletResponse.SC_CREATED);
+                    res.setHeader("Location", location);
+                }
+                else
+                {
+                    res.setStatus(HttpServletResponse.SC_NOT_FOUND);
+                }
+            }
+            catch (Exception e)
+            {
+                res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+                e.printStackTrace(res.getWriter());
+                mLogger.error(e);
+            }
+        }
+        else 
+        {
+            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        }
+    }
+
+    //-----------------------------------------------------------------------------    
+    /**
+     * Handles an Atom PUT by calling handler to identify URI, reading/parsing
+     * data, calling handler and writing results to response.
+     */
+    protected void doPut(HttpServletRequest req, HttpServletResponse res)
+        throws ServletException, IOException
+    {
+        AtomHandler handler = createAtomRequestHandler(req);
+        String userName = handler.getAuthenticatedUsername();
+        if (userName != null) 
+        {
+            String[] pathInfo = getPathInfo(req);
+            try
+            {
+                if (handler.isEntryURI(pathInfo)) 
+                {
+                    // parse incoming entry
+                    Entry unsavedEntry = parseEntry(
+                        new InputStreamReader(req.getInputStream()));
+                    
+                    // call handler to put entry
+                    Entry updatedEntry = handler.putEntry(pathInfo, unsavedEntry);
+                    
+                    // write entry back out to response
+                    Writer writer = res.getWriter(); 
+                    serializeEntry(updatedEntry, writer);                    
+                    res.setStatus(HttpServletResponse.SC_OK);
+                    writer.close();
+                }
+                else if (handler.isResourceCollectionURI(pathInfo)) 
+                {
+                    // handle input stream to handler
+                    handler.putResource(
+                        pathInfo, req.getContentType(), req.getInputStream());
+                    res.setStatus(HttpServletResponse.SC_OK);
+                }
+                else
+                {
+                    res.setStatus(HttpServletResponse.SC_NOT_FOUND);
+                }
+            }
+            catch (Exception e)
+            {
+                res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+                e.printStackTrace(res.getWriter());
+                mLogger.error(e);
+            }
+        }
+        else 
+        {
+            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        }
+    }
+
+    //-----------------------------------------------------------------------------
+    /**
+     * Handle Atom DELETE by calling appropriate handler.
+     */
+    protected void doDelete(HttpServletRequest req, HttpServletResponse res)
+        throws ServletException, IOException
+    {
+        AtomHandler handler = createAtomRequestHandler(req);
+        String userName = handler.getAuthenticatedUsername();
+        if (userName != null) 
+        {
+            String[] pathInfo = getPathInfo(req);
+            try
+            {
+                if (handler.isEntryURI(pathInfo)) 
+                {
+                    handler.deleteEntry(pathInfo); 
+                    res.setStatus(HttpServletResponse.SC_OK);
+                }
+                else if (handler.isResourceURI(pathInfo)) 
+                {
+                    handler.deleteResource(pathInfo); 
+                    res.setStatus(HttpServletResponse.SC_OK);
+                }
+                else
+                {
+                    res.setStatus(HttpServletResponse.SC_NOT_FOUND);
+                }
+            }
+            catch (Exception e)
+            {
+                res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+                e.printStackTrace(res.getWriter());
+                mLogger.error(e);
+            }
+        }
+        else 
+        {
+            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        }
+    }
+    
+    //-----------------------------------------------------------------------------
+    /**
+     * Convenience method to return the PathInfo from the request.  
+     */
+    protected String[] getPathInfo(HttpServletRequest request)
+    {
+        String mPathInfo = request.getPathInfo();
+        mPathInfo = (mPathInfo!=null) ? mPathInfo : "";
+        return StringUtils.split(mPathInfo,"/");   
+    }
+
+    /** 
+     * Utility method to make up for a Rome shortcoming:
+     * Rome can only serialize entire feeds, not individual elements
+     */
+    public static void serializeEntry(Entry entry, Writer writer) 
+        throws IllegalArgumentException, FeedException, IOException
+    {
+        // Build a feed containing only the entry
+        List entries = new ArrayList();
+        entries.add(entry);
+        Feed feed1 = new Feed();
+        feed1.setFeedType(AtomServlet.FEED_TYPE);
+        feed1.setEntries(entries);
+        
+        // Get Rome to output feed as a JDOM document
+        WireFeedOutput wireFeedOutput = new WireFeedOutput();
+        Document feedDoc = wireFeedOutput.outputJDom(feed1);
+        
+        // Grab entry element from feed and get JDOM to serialize it
+        Element entryElement= (Element)feedDoc.getRootElement().getChildren().get(0);
+        XMLOutputter outputter = new XMLOutputter();
+        outputter.setFormat(Format.getPrettyFormat());
+        
+        StringWriter sw = new StringWriter();  // DEBUG
+        outputter.output(entryElement, sw);    // DEBUG
+        System.out.println(sw.toString());     // DEBUG    
+        writer.write(sw.toString());           // DEBUG
+        
+        //outputter.output(entryElement, writer);
+    }
+    
+    /** 
+     * Utility method to make up for a Rome shortcoming:
+     * Rome can only parse Atom data with XML document root 'feed'
+     */
+    public static Entry parseEntry(Reader rd) 
+        throws JDOMException, IOException, IllegalArgumentException, FeedException 
+    {
+        // Parse entry into JDOM tree        
+        SAXBuilder builder = new SAXBuilder();
+        Document entryDoc = builder.build(rd);
+        Element fetchedEntryElement = entryDoc.getRootElement();
+        fetchedEntryElement.detach();
+        
+        // Put entry into a JDOM document with 'feed' root so that Rome can handle it
+        Feed feed = new Feed();
+        feed.setFeedType(FEED_TYPE);
+        WireFeedOutput wireFeedOutput = new WireFeedOutput();
+        Document feedDoc = wireFeedOutput.outputJDom(feed); 
+        feedDoc.getRootElement().addContent(fetchedEntryElement);
+        
+        WireFeedInput input = new WireFeedInput();
+        Feed parsedFeed = (Feed)input.build(feedDoc);
+        return (Entry)parsedFeed.getEntries().get(0);
+    }
+}

Added: incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/RollerAtomHandler.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/RollerAtomHandler.java?rev=290745&view=auto
==============================================================================
--- incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/RollerAtomHandler.java (added)
+++ incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/RollerAtomHandler.java Wed Sep 21 10:08:31 2005
@@ -0,0 +1,829 @@
+/*
+ * Copyright 2005 David M Johnson (For RSS and Atom In Action)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.roller.presentation.atomapi04;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.sql.Timestamp;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.struts.util.RequestUtils;
+import org.roller.RollerException;
+import org.roller.model.FileManager;
+import org.roller.model.Roller;
+import org.roller.model.WeblogManager;
+import org.roller.pojos.UserData;
+import org.roller.pojos.PermissionsData;
+import org.roller.pojos.WeblogCategoryData;
+import org.roller.pojos.WeblogEntryData;
+import org.roller.pojos.WebsiteData;
+import org.roller.presentation.LoginServlet;
+import org.roller.presentation.RollerContext;
+import org.roller.util.RollerMessages;
+import org.roller.util.Utilities;
+
+import com.sun.syndication.feed.atom.Content;
+import com.sun.syndication.feed.atom.Category;
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.feed.atom.Link;
+import com.sun.syndication.io.impl.Base64;
+import org.roller.RollerException;
+
+/**
+ * Roller's Atom Protocol implementation.
+ * <pre>
+ * Here are the URIs suppored:
+ *
+ *    URI type             URI form                          Handled by
+ *    --------             --------                          ----------
+ *    Introspection URI    /                                 getIntrosection()
+ *    Collection URI       /blog-name/<collection-name>      getCollection()
+ *    Collection-next URI  /blog-name/<collection-name>/id   getCollection()
+ *    Member URI           /blog-name/<object-name>          post<object-name>()
+ *    Member URI           /blog-name/<object-name>/id       get<object-name>()
+ *    Member URI           /blog-name/<object-name>/id       put<object-name>()
+ *    Member URI           /blog-name/<object-name>/id       delete<object-name>()
+ *
+ *    Until group blogging is supported weblogHandle == blogname.
+ *
+ *    Collection-names   Object-names
+ *    ----------------   ------------
+ *       entries           entry
+ *       resources         resource
+ *       categories        categories
+ * soon:
+ *       users             user
+ *       templates         template
+ * </pre>
+ *
+ * @author David M Johnson
+ */
+public class RollerAtomHandler implements AtomHandler {
+    private HttpServletRequest mRequest;
+    private Roller             mRoller;
+    private RollerContext      mRollerContext;
+    private String             mUsername;
+    private int                mMaxEntries = 20;
+    //private MessageDigest    md5Helper = null;
+    //private MD5Encoder       md5Encoder = new MD5Encoder();
+    
+    private static Log mLogger =
+            LogFactory.getFactory().getInstance(RollerAtomHandler.class);
+    
+    //---------------------------------------------------------------- construction
+    
+    /**
+     * Create Atom handler for a request and attempt to authenticate user.
+     * If user is authenticated, then getAuthenticatedUsername() will return
+     * then user's name, otherwise it will return null.
+     */
+    public RollerAtomHandler(HttpServletRequest request) {
+        mRequest = request;
+        mRoller = RollerContext.getRoller(request);
+        mRollerContext = RollerContext.getRollerContext(request);
+        
+        // TODO: decide what to do about authentication, is WSSE going to fly?
+        mUsername = authenticateWSSE(request);
+        
+        if (mUsername != null) {
+            try {
+                UserData user = mRoller.getUserManager().getUser(mUsername);
+                mRoller.setUser(user);
+            } catch (Exception e) {
+                mLogger.error("ERROR: setting user", e);
+            }
+        }
+        //        try
+        //        {
+        //            md5Helper = MessageDigest.getInstance("MD5");
+        //        }
+        //        catch (NoSuchAlgorithmException e)
+        //        {
+        //            mLogger.debug("ERROR creating MD5 helper", e);
+        //        }
+    }
+    
+    /**
+     * Return weblogHandle of authenticated user or null if there is none.
+     */
+    public String getAuthenticatedUsername() {
+        return mUsername;
+    }
+    
+    //---------------------------------------------------------------- introspection
+    
+    /**
+     * Return Atom service document for site, getting blog-name from pathInfo.
+     * Since a user can (currently) have only one blog, one workspace is returned.
+     * The workspace will contain collections for entries, categories and resources.
+     */
+    public AtomService getIntrospection(String[] pathInfo) throws Exception {
+        if (pathInfo.length == 0) {
+            String absUrl = mRollerContext.getAbsoluteContextUrl(mRequest);
+            AtomService service = new AtomService();
+            UserData user = mRoller.getUserManager().getUser(mUsername);
+            List perms = mRoller.getUserManager().getAllPermissions(user);
+            if (perms != null) {
+                for (Iterator iter=perms.iterator(); iter.hasNext();) {
+                    PermissionsData perm = (PermissionsData)iter.next();
+                    String handle = perm.getWebsite().getHandle();
+                    AtomService.Workspace workspace = new AtomService.Workspace();
+                    workspace.setTitle("Workspace: Collections for " + handle);
+                    service.addWorkspace(workspace);
+                    
+                    AtomService.Collection entryCol = new AtomService.Collection();
+                    entryCol.setTitle("Collection: Weblog Entries for " + handle);
+                    entryCol.setContents("entries");
+                    entryCol.setHref(absUrl + "/atom/"+handle+"/entries");
+                    workspace.addCollection(entryCol);
+                    
+                    AtomService.Collection catCol = new AtomService.Collection();
+                    catCol.setTitle("Collection: Categories for " + handle);
+                    catCol.setContents("categories");
+                    catCol.setHref(absUrl + "/atom/"+handle+"/categories");
+                    workspace.addCollection(catCol);
+                    
+                    AtomService.Collection uploadCol = new AtomService.Collection();
+                    uploadCol.setTitle("Collection: File uploads for " + handle);
+                    uploadCol.setContents("generic");
+                    uploadCol.setHref(absUrl + "/atom/"+handle+"/resources");
+                    workspace.addCollection(uploadCol);
+                }
+            }
+            return service;
+        }
+        throw new Exception("ERROR: bad URL in getIntrospection()");
+    }
+    
+    //----------------------------------------------------------------- collections
+    
+    /**
+     * Returns collection specified by pathInfo with no date range specified.
+     * Just calls the other getCollection(), but with offset = -1.
+     */
+    public AtomCollection getCollection(String[] pathInfo) throws Exception {
+        return getCollection(pathInfo, null, new Date(), -1);
+    }
+    
+    /**
+     * Returns collection specified by pathInfo, constrained by a date range and
+     * starting at an offset within the collection.Returns 20 items at a time.
+     * <pre>
+     * Supports these three collection URI forms:
+     *    /<blog-name>/entries
+     *    /<blog-name>/resources
+     *    /<blog-name>/categories
+     * </pre>
+     * @param pathInfo Path info from URI
+     * @param start    Don't include members updated before this date (null allowed)
+     * @param end      Don't include members updated after this date (null allowed)
+     * @param offset   Offset within collection (for paging)
+     */
+    public AtomCollection getCollection(
+            String[] pathInfo, Date start, Date end, int offset)
+            throws Exception {
+        if (pathInfo.length > 0 && pathInfo[1].equals("entries")) {
+            return getCollectionOfEntries(pathInfo, start, end, offset);
+        } else if (pathInfo.length > 0 && pathInfo[1].equals("resources")) {
+            return getCollectionOfResources(pathInfo, start, end, offset);
+        } else if (pathInfo.length > 0 && pathInfo[1].equals("categories")) {
+            return getCollectionOfCategories(pathInfo, start, end, offset);
+        }
+        throw new Exception("ERROR: bad URL in getCollection()");
+    }
+    
+    /**
+     * Helper method that returns collection of entries, called by getCollection().
+     */
+    public AtomCollection getCollectionOfEntries(
+            String[] pathInfo, Date start, Date end, int offset)
+            throws Exception {
+        String handle = pathInfo[0];
+        String absUrl = mRollerContext.getAbsoluteContextUrl(mRequest);
+        WebsiteData website = mRoller.getUserManager().getWebsiteByHandle(handle);
+        List entries = null;
+        if (canView(website)) {
+            if (pathInfo.length == 2) // handle /blogname/entries
+            {
+                // return most recent blog entries
+                if (offset == -1) {
+                    entries = mRoller.getWeblogManager().getWeblogEntries(
+                            website,           // website
+                            start,             // startDate
+                            end,               // endDate
+                            null,              // catName
+                            null, // status
+                            new Integer(mMaxEntries + 1)); // maxEntries
+                } else {
+                    entries = mRoller.getWeblogManager().getWeblogEntries(
+                            website,           // website
+                            start,             // startDate
+                            end,               // endDate
+                            null,              // catName
+                            null, // status
+                            offset,            // offset (for range paging)
+                            mMaxEntries + 1);  // maxEntries
+                }
+            } else if (pathInfo.length == 3) // handle /blogname/entries/entryid
+            {
+                // return entries previous to entry specified by pathInfo
+                String entryid = pathInfo[2];
+                WeblogManager wmgr = mRoller.getWeblogManager();
+                WeblogEntryData entry = wmgr.retrieveWeblogEntry(entryid);
+                entries = wmgr.getPreviousEntries(entry, null, mMaxEntries + 1);
+            } else throw new Exception("ERROR: bad URL");
+            
+            // build collection
+            AtomCollection col = new AtomCollection();
+            if (entries.size() > mMaxEntries) {
+                // there are more entries, so include next link
+                WeblogEntryData lastEntry =
+                        (WeblogEntryData)entries.get(entries.size() - 1);
+                col.setNext(createNextLink(lastEntry, start, end, offset));
+            }
+            // add up to max entries to collection
+            int count = 0;
+            Iterator iter = entries.iterator();
+            while (iter.hasNext() && count++ < mMaxEntries) {
+                WeblogEntryData rollerEntry = (WeblogEntryData)iter.next();
+                AtomCollection.Member member = new AtomCollection.Member();
+                member.setTitle(rollerEntry.getDisplayTitle());
+                member.setUpdated(rollerEntry.getUpdateTime());
+                member.setHref(absUrl
+                        + "/atom/" + handle + "/entry/" + rollerEntry.getId());
+                col.addMember(member);
+            }
+            return col;
+        }
+        throw new Exception("ERROR: not authorized");
+    }
+    
+    /**
+     * Helper method that returns collection of resources, called by getCollection().
+     */
+    public AtomCollection getCollectionOfResources(
+            String[] pathInfo, Date start, Date end, int offset) throws Exception {
+        String handle = pathInfo[0];
+        String absUrl = mRollerContext.getAbsoluteContextUrl(mRequest);
+        WebsiteData website = mRoller.getUserManager().getWebsiteByHandle(handle);
+        FileManager fmgr = mRoller.getFileManager();
+        File[] files = fmgr.getFiles(website);
+        if (canView(website)) {
+            AtomCollection col = new AtomCollection();
+            for (int i=0; i<files.length; i++) {
+                AtomCollection.Member member = new AtomCollection.Member();
+                member.setTitle(files[i].getName());
+                member.setUpdated(new Date(files[i].lastModified()));
+                member.setHref(absUrl
+                        + "/atom/" + website.getHandle() + "/resource/" + files[i].getName() );
+                col.addMember(member);
+            }
+            return col;
+        }
+        throw new Exception("ERROR: not authorized");
+    }
+    
+    /**
+     * Helper method that returns collection of categories, called by getCollection().
+     */
+    public AtomCollection getCollectionOfCategories(
+            String[] pathInfo, Date start, Date end, int offset) throws Exception {
+        String handle = pathInfo[0];
+        String absUrl = mRollerContext.getAbsoluteContextUrl(mRequest);
+        WebsiteData website = mRoller.getUserManager().getWebsiteByHandle(handle);
+        WeblogManager wmgr = mRoller.getWeblogManager();
+        List items = wmgr.getWeblogCategories(website);
+        if (canView(website)) {
+            AtomCollection col = new AtomCollection();
+            Iterator iter = items.iterator();
+            Date now = new Date();
+            while (iter.hasNext()) {
+                WeblogCategoryData item = (WeblogCategoryData)iter.next();
+                AtomCollection.Member member = new AtomCollection.Member();
+                String name = item.getPath();
+                if (name.equals("/")) continue;
+                member.setTitle(name);
+                member.setUpdated(now);
+                member.setHref(absUrl + "/atom/"  
+                    + website.getHandle() + "/category/" + item.getId());
+                col.addMember(member);
+            }
+            return col;
+        }
+        throw new Exception("ERROR: not authorized");
+    }
+    
+    //--------------------------------------------------------------------- entries
+    
+    /**
+     * Create entry in the entry collection (a Roller blog has only one).
+     */
+    public Entry postEntry(String[] pathInfo, Entry entry) throws Exception {
+        // authenticated client posted a weblog entry
+        String handle = pathInfo[0];
+        WebsiteData website = mRoller.getUserManager().getWebsiteByHandle(handle);
+        UserData creator = mRoller.getUserManager().getUser(mUsername);
+        if (canEdit(website)) {
+            // Save it and commit it
+            WeblogEntryData rollerEntry = createRollerEntry(website, entry);
+            rollerEntry.setCreator(creator);
+            rollerEntry.save();
+            mRoller.commit();
+            
+            // Throttle one entry per second
+            // (MySQL timestamp has 1 sec resolution, damnit)
+            Thread.sleep(1000);
+            
+            // TODO: ping the appropriate ping
+            // TODO: flush the cache on Atom post
+            //flushPageCache(mRequest);
+            
+            return createAtomEntry(rollerEntry);
+        }
+        throw new Exception("ERROR not authorized to edit website");
+    }
+    
+    /**
+     * Retrieve entry, URI like this /blog-name/entry/id
+     */
+    public Entry getEntry(String[] pathInfo) throws Exception {
+        if (pathInfo.length == 3) // URI is /blogname/entries/entryid
+        {
+            WeblogEntryData entry =
+                    mRoller.getWeblogManager().retrieveWeblogEntry(pathInfo[2]);
+            if (!canView(entry)) {
+                throw new Exception("ERROR not authorized to view entry");
+            } else if (entry != null) {
+                return createAtomEntry(entry);
+            }
+            throw new Exception("ERROR: entry not found");
+        }
+        throw new Exception("ERROR: bad URI");
+    }
+    
+    /**
+     * Update entry, URI like this /blog-name/entry/id
+     */
+    public Entry putEntry(String[] pathInfo, Entry entry) throws Exception {
+        if (pathInfo.length == 3) // URI is /blogname/entries/entryid
+        {
+            WeblogEntryData rollerEntry =
+                    mRoller.getWeblogManager().retrieveWeblogEntry(pathInfo[2]);
+            if (canEdit(rollerEntry)) {
+                rollerEntry.setTitle(entry.getTitle());
+                
+                // TODO: don't assume type is HTML or TEXT
+                rollerEntry.setText(entry.getContent().getValue());
+                
+                rollerEntry.setUpdateTime(new Timestamp(new Date().getTime()));
+                if (entry.getPublished() != null) {
+                    rollerEntry.setPubTime(
+                        new Timestamp(entry.getPublished().getTime()));
+                }
+                if (entry.getCategories() != null
+                        && entry.getCategories().size() > 0) {
+                    Category atomCat = (Category)entry.getCategories().get(0);
+                    WeblogCategoryData cat = 
+                        mRoller.getWeblogManager().getWeblogCategoryByPath(
+                            rollerEntry.getWebsite(), atomCat.getTerm());
+                    if (cat != null) {
+                        rollerEntry.setCategory(cat);
+                    }
+                }
+                rollerEntry.save();
+                mRoller.commit();
+                return createAtomEntry(rollerEntry);
+            }
+            throw new Exception("ERROR not authorized to put entry");
+        }
+        throw new Exception("ERROR: bad URI");
+    }
+    
+    /**
+     * Delete entry, URI like this /blog-name/entry/id
+     */
+    public void deleteEntry(String[] pathInfo) throws Exception {
+        if (pathInfo.length == 3) // URI is /blogname/entries/entryid
+        {
+            WeblogEntryData rollerEntry =
+                    mRoller.getWeblogManager().retrieveWeblogEntry(pathInfo[2]);
+            if (canEdit(rollerEntry)) {
+                rollerEntry.remove();
+                mRoller.commit();
+                return;
+            }
+            throw new Exception("ERROR not authorized to delete entry");
+        }
+        throw new Exception("ERROR: bad URI");
+    }
+    
+    //-------------------------------------------------------------------- resources
+    
+    /**
+     * Create new resource in generic collection (a Roller blog has only one).
+     * TODO: can we avoid saving temporary file?
+     * TODO: do we need to handle mutli-part MIME uploads?
+     * TODO: use Jakarta Commons File-upload?
+     */
+    public String postResource(String[] pathInfo,
+            String name, String contentType, InputStream is)
+            throws Exception {
+        // authenticated client posted a weblog entry
+        String handle = pathInfo[0];
+        WebsiteData website = mRoller.getUserManager().getWebsiteByHandle(handle);
+        if (canEdit(website) && pathInfo.length > 1) {
+            try {
+                FileManager fmgr = mRoller.getFileManager();
+                RollerMessages msgs = new RollerMessages();
+                
+                // save to temp file
+                if (name == null) {
+                    throw new Exception(
+                            "ERROR[postResource]: No 'name' present in HTTP headers");
+                }
+                File tempFile = File.createTempFile(name,"tmp");
+                FileOutputStream fos = new FileOutputStream(tempFile);
+                Utilities.copyInputToOutput(is, fos);
+                fos.close();
+                
+                // If save is allowed by Roller system-wide policies
+                if (fmgr.canSave(website, name, tempFile.length(), msgs)) {
+                    // Then save the file
+                    FileInputStream fis = new FileInputStream(tempFile);
+                    fmgr.saveFile(website, name, tempFile.length(), fis);
+                    fis.close();
+                    
+                    // TODO: build URL to uploaded file should be done in FileManager
+                    String uploadPath = RollerContext.getUploadPath(
+                            mRequest.getSession(true).getServletContext());
+                    uploadPath += "/" + website.getHandle() + "/" + name;
+                    return RequestUtils.printableURL(
+                            RequestUtils.absoluteURL(mRequest, uploadPath));
+                }
+                tempFile.delete();
+                throw new Exception("File upload denied because:" + msgs.toString());
+            } catch (Exception e) {
+                String msg = "ERROR in atom.postResource";
+                mLogger.error(msg,e);
+                throw new Exception(msg);
+            }
+        }
+        throw new Exception("ERROR not authorized to edit website");
+    }
+    
+    /**
+     * Get absolute path to resource specified by path info.
+     */
+    public String getResourceFilePath(String[] pathInfo) throws Exception {
+        // ==> /<blogname>/resources/<filename>
+        String uploadPath = RollerContext.getUploadPath(
+                mRequest.getSession(true).getServletContext());
+        return uploadPath + File.separator + pathInfo[2];
+    }
+    
+    /**
+     * Update resource specified by pathInfo using data from input stream.
+     * Expects pathInfo of form /blog-name/resources/name
+     */
+    public void putResource(String[] pathInfo,
+            String contentType, InputStream is) throws Exception {
+        if (pathInfo.length > 2) {
+            String name = pathInfo[2];
+            postResource(pathInfo, name, contentType, is);
+        }
+        throw new Exception("ERROR: bad pathInfo");
+    }
+    
+    /**
+     * Delete resource specified by pathInfo.
+     * Expects pathInfo of form /blog-name/resources/name
+     */
+    public void deleteResource(String[] pathInfo) throws Exception {
+        // authenticated client posted a weblog entry
+        String handle = pathInfo[0];
+        WebsiteData website = mRoller.getUserManager().getWebsiteByHandle(handle);
+        if (canEdit(website) && pathInfo.length > 1) {
+            try {
+                FileManager fmgr = mRoller.getFileManager();
+                fmgr.deleteFile(website, pathInfo[2]);
+            } catch (Exception e) {
+                String msg = "ERROR in atom.deleteResource";
+                mLogger.error(msg,e);
+                throw new Exception(msg);
+            }
+        }
+        throw new Exception("ERROR not authorized to edit website");
+    }
+    
+    //------------------------------------------------------------------ URI testers
+    
+    /**
+     * True if URL is the introspection URI.
+     */
+    public boolean isIntrospectionURI(String[] pathInfo) {
+        if (pathInfo.length==0) return true;
+        return false;
+    }
+    
+    /**
+     * True if URL is a entry URI.
+     */
+    public boolean isEntryURI(String[] pathInfo) {
+        if (pathInfo.length > 1 && pathInfo[1].equals("entry")) return true;
+        return false;
+    }
+    
+    /**
+     * True if URL is a resource URI.
+     */
+    public boolean isResourceURI(String[] pathInfo) {
+        if (pathInfo.length > 1 && pathInfo[1].equals("resource")) return true;
+        return false;
+    }
+    
+    /**
+     * True if URL is a category URI.
+     */
+    public boolean isCategoryURI(String[] pathInfo) {
+        if (pathInfo.length > 1 && pathInfo[1].equals("category")) return true;
+        return false;
+    }
+    
+    /**
+     * True if URL is a collection URI of any sort.
+     */
+    public boolean isCollectionURI(String[] pathInfo) {
+        if (pathInfo.length > 1 && pathInfo[1].equals("entries")) return true;
+        if (pathInfo.length > 1 && pathInfo[1].equals("resources")) return true;
+        if (pathInfo.length > 1 && pathInfo[1].equals("categories")) return true;
+        return false;
+    }
+    
+    /**
+     * True if URL is a entry collection URI.
+     */
+    public boolean isEntryCollectionURI(String[] pathInfo) {
+        if (pathInfo.length > 1 && pathInfo[1].equals("entries")) return true;
+        return false;
+    }
+    
+    /**
+     * True if URL is a resource collection URI.
+     */
+    public boolean isResourceCollectionURI(String[] pathInfo) {
+        if (pathInfo.length > 1 && pathInfo[1].equals("resources")) return true;
+        return false;
+    }
+    
+    /**
+     * True if URL is a category collection URI.
+     */
+    public boolean isCategoryCollectionURI(String[] pathInfo) {
+        if (pathInfo.length > 1 && pathInfo[1].equals("categories")) return true;
+        return false;
+    }
+    
+    //------------------------------------------------------------------ permissions
+    
+    /**
+     * Return true if user is allowed to edit an entry.
+     */
+    private boolean canEdit(WeblogEntryData entry) {
+        try {
+            return entry.canSave();
+        } catch (Exception e) {
+            mLogger.error("ERROR: checking website.canSave()");
+        }
+        return false;
+    }
+    
+    /**
+     * Return true if user is allowed to edit a website.
+     */
+    private boolean canEdit(WebsiteData website) {
+        try {
+            return website.canSave();
+        } catch (Exception e) {
+            mLogger.error("ERROR: checking website.canSave()");
+        }
+        return false;
+    }
+    
+    /**
+     * Return true if user is allowed to view an entry.
+     */
+    private boolean canView(WeblogEntryData entry) {
+        return canEdit(entry);
+    }
+    
+    /**
+     * Return true if user is allowed to view a website.
+     */
+    private boolean canView(WebsiteData website) {
+        return canEdit(website);
+    }
+    
+    //-------------------------------------------------------------- authentication
+    
+    /**
+     * Perform WSSE authentication based on information in request.
+     * Will not work if Roller password encryption is turned on.
+     */
+    protected String authenticateWSSE(HttpServletRequest request) {
+        String wsseHeader = request.getHeader("X-WSSE");
+        if (wsseHeader == null) return null;
+        
+        String ret = null;
+        String userName = null;
+        String created = null;
+        String nonce = null;
+        String passwordDigest = null;
+        String[] tokens = wsseHeader.split(",");
+        for (int i = 0; i < tokens.length; i++) {
+            int index = tokens[i].indexOf('=');
+            if (index != -1) {
+                String key = tokens[i].substring(0, index).trim();
+                String value = tokens[i].substring(index + 1).trim();
+                value = value.replaceAll("\"", "");
+                if (key.startsWith("UsernameToken")) {
+                    userName = value;
+                } else if (key.equalsIgnoreCase("nonce")) {
+                    nonce = value;
+                } else if (key.equalsIgnoreCase("passworddigest")) {
+                    passwordDigest = value;
+                } else if (key.equalsIgnoreCase("created")) {
+                    created = value;
+                }
+            }
+        }
+        String digest = null;
+        try {
+            UserData user = mRoller.getUserManager().getUser(userName);
+            digest = WSSEUtilities.generateDigest(
+                    WSSEUtilities.base64Decode(nonce),
+                    created.getBytes("UTF-8"),
+                    user.getPassword().getBytes("UTF-8"));
+            if (digest.equals(passwordDigest)) {
+                ret = userName;
+            }
+        } catch (Exception e) {
+            mLogger.error("ERROR in wsseAuthenticataion: " + e.getMessage(), e);
+        }
+        return ret;
+    }
+    
+    /**
+     * Untested (and currently unused) implementation of BASIC authentication
+     */
+    public String authenticateBASIC(HttpServletRequest request) {
+        boolean valid = false;
+        String userID = null;
+        String password = null;
+        try {
+            String authHeader = request.getHeader("Authorization");
+            if (authHeader != null) {
+                StringTokenizer st = new StringTokenizer(authHeader);
+                if (st.hasMoreTokens()) {
+                    String basic = st.nextToken();
+                    if (basic.equalsIgnoreCase("Basic")) {
+                        String credentials = st.nextToken();
+                        String userPass = new String(Base64.decode(credentials));
+                        int p = userPass.indexOf(":");
+                        if (p != -1) {
+                            userID = userPass.substring(0, p);
+                            UserData user = mRoller.getUserManager().getUser(userID);
+                            String realpassword = LoginServlet.getEncryptedPassword(
+                                    request, user.getUserName(), user.getPassword());
+                            password = userPass.substring(p+1);
+                            if (    (!userID.trim().equals(user.getUserName()))
+                            && (!password.trim().equals(realpassword))) {
+                                valid = true;
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            mLogger.debug(e);
+        }
+        if (valid) return userID;
+        return null;
+    }
+    
+    //----------------------------------------------------------- internal utilities
+    
+    /**
+     * Create next member list suitable for use in entry collection.
+     * Puts state date, end date and off set in request parameters.
+     */
+    private String createNextLink(
+            WeblogEntryData entry, Date start, Date end, int offset) {
+        SimpleDateFormat df = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ssZ" );
+        String absUrl = mRollerContext.getAbsoluteContextUrl();
+        String url = absUrl + "/atom/" + mUsername + "/entries/" + entry.getId();
+        if (offset != -1 && start != null && end != null) {
+            url  = url + "?Range=" + df.format(start) + "/" + df.format(end);
+        } else if (offset != -1 && start != null) {
+            url  = url + "?Range=" + df.format(start) + "/";
+        } else if (offset != -1 && end != null) {
+            url  = url + "?Range=/" + df.format(end);
+        }
+        if (offset != -1) {
+            url = url + "&offset=" + (offset + mMaxEntries);
+        }
+        return url;
+    }
+    
+    /**
+     * Create a Rome Atom entry based on a Roller entry.
+     * Content is escaped.
+     * Link is stored as rel=alternate link.
+     */
+    private Entry createAtomEntry(WeblogEntryData entry) {
+        Entry atomEntry = new Entry();
+        Content content = new Content();
+        content.setType(Content.HTML);
+        content.setValue(entry.getText());
+        
+        atomEntry.setId(        entry.getId());
+        atomEntry.setTitle(     entry.getTitle());
+        atomEntry.setContent(   content);
+        atomEntry.setPublished( entry.getPubTime());
+        atomEntry.setUpdated(   entry.getUpdateTime());
+        
+        List categories = new ArrayList();
+        Category atomCat = new Category();
+        atomCat.setTerm(entry.getCategory().getPath());
+        categories.add(atomCat);
+        atomEntry.setCategories(categories); 
+                
+        List links = new ArrayList();
+        Link altlink = new Link();
+        altlink.setRel("alternate");
+        altlink.setHref(entry.getPermaLink());
+        links.add(altlink);
+        atomEntry.setLinks(links);
+        
+        return atomEntry;
+    }
+    
+    /**
+     * Create a Roller weblog entry based on a Rome Atom entry object
+     */
+    private WeblogEntryData createRollerEntry(WebsiteData website, Entry entry) 
+        throws RollerException {
+
+        Timestamp current = new Timestamp(System.currentTimeMillis());
+        Timestamp pubTime = current;
+        Timestamp updateTime = current;
+        if (entry.getPublished() != null) {
+            pubTime = new Timestamp( entry.getPublished().getTime() );
+        }
+        if (entry.getUpdated() != null) {
+            updateTime = new Timestamp( entry.getUpdated().getTime() );
+        }
+        WeblogEntryData rollerEntry = new WeblogEntryData();
+        rollerEntry.setTitle(entry.getTitle());
+        rollerEntry.setText(entry.getContent().getValue());
+        rollerEntry.setPubTime(pubTime);
+        rollerEntry.setUpdateTime(updateTime);
+        rollerEntry.setWebsite(website);
+        rollerEntry.setStatus(WeblogEntryData.PUBLISHED);
+        
+        List categories = entry.getCategories();
+        if (categories != null && categories.size() > 0) {
+            Category cat = (Category)categories.get(0);
+            System.out.println(cat.getTerm());
+            WeblogCategoryData rollerCat = 
+                mRoller.getWeblogManager().getWeblogCategoryByPath(
+                    website, cat.getTerm());
+            rollerEntry.setCategory(rollerCat);
+        } else {
+            rollerEntry.setCategory(website.getBloggerCategory());
+        }
+        return rollerEntry;
+    }
+}

Added: incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/WSSEUtilities.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/WSSEUtilities.java?rev=290745&view=auto
==============================================================================
--- incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/WSSEUtilities.java (added)
+++ incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/WSSEUtilities.java Wed Sep 21 10:08:31 2005
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2005, Dave Johnson
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.roller.presentation.atomapi04;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import org.apache.commons.codec.binary.Base64;
+
+/**
+ * Utilties to support WSSE authentication.
+ * @author Dave Johnson
+ */
+public class WSSEUtilities {
+    public static synchronized String generateDigest(
+            byte[] nonce, byte[] created, byte[] password) {
+        String result = null;
+        try {
+            MessageDigest digester = MessageDigest.getInstance("SHA");
+            digester.reset();
+            digester.update(nonce);
+            digester.update(created);
+            digester.update(password);
+            byte[] digest = digester.digest();
+            result = new String(base64Encode(digest));
+        }
+        catch (NoSuchAlgorithmException e) {
+            result = null;
+        }
+        return result;
+    }
+    public static byte[] base64Decode(String value) throws IOException {
+        return Base64.decodeBase64(value.getBytes("UTF-8"));
+    }
+    public static String base64Encode(byte[] value) {
+        return new String(Base64.encodeBase64(value));
+    }
+    public static String generateWSSEHeader(String userName, String password) 
+    throws UnsupportedEncodingException {  
+       
+        byte[] nonceBytes = Long.toString(new Date().getTime()).getBytes();
+        String nonce = new String(WSSEUtilities.base64Encode(nonceBytes));
+        
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+        String created = sdf.format(new Date());
+        
+        String digest = WSSEUtilities.generateDigest(
+                nonceBytes, created.getBytes("UTF-8"), password.getBytes("UTF-8"));
+        
+        StringBuffer header = new StringBuffer("UsernameToken Username=\"");
+        header.append(userName);
+        header.append("\", ");
+        header.append("PasswordDigest=\"");
+        header.append(digest);
+        header.append("\", ");
+        header.append("Nonce=\"");
+        header.append(nonce);
+        header.append("\", ");
+        header.append("Created=\"");
+        header.append(created);
+        header.append("\"");
+        return header.toString();
+    }
+}

Added: incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/package.html
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/package.html?rev=290745&view=auto
==============================================================================
--- incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/package.html (added)
+++ incubator/roller/branches/roller_2.0/sandbox/atomprotocol/src/org/roller/presentation/atomapi04/package.html Wed Sep 21 10:08:31 2005
@@ -0,0 +1,10 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+  <title></title>
+</head>
+<body>
+ROME-based Atom Protocol implementation.
+
+</body>
+</html>