From e078d8ce8dd10d746795a2b97e7389afb04206bb Mon Sep 17 00:00:00 2001
From: kebernet <kebernet@e6edf6fb-f266-4316-afb4-e53d95876a76>
Date: Fri, 1 Apr 2011 04:59:49 +0000
Subject: [PATCH] Package move.

---
 .../atom/client/AtomClientFactory.java        |  47 +
 .../propono/atom/client/AuthStrategy.java     |  30 +
 .../atom/client/BasicAuthStrategy.java        |  41 +
 .../atom/client/ClientAtomService.java        | 138 +++
 .../propono/atom/client/ClientCategories.java |  70 ++
 .../propono/atom/client/ClientCollection.java | 217 +++++
 .../propono/atom/client/ClientEntry.java      | 259 ++++++
 .../propono/atom/client/ClientMediaEntry.java | 322 +++++++
 .../propono/atom/client/ClientWorkspace.java  |  66 ++
 .../propono/atom/client/EntryIterator.java    | 124 +++
 .../atom/client/GDataAuthStrategy.java        |  67 ++
 .../propono/atom/client/NoAuthStrategy.java   |  31 +
 .../propono/atom/client/OAuthStrategy.java    | 302 +++++++
 .../atom/client/atomclient-diagram.gif        | Bin 0 -> 19120 bytes
 .../propono/atom/common/AtomService.java      | 122 +++
 .../propono/atom/common/Categories.java       | 155 ++++
 .../propono/atom/common/Collection.java       | 252 ++++++
 .../propono/atom/common/Workspace.java        | 147 +++
 .../propono/atom/common/rome/AppModule.java   |  42 +
 .../atom/common/rome/AppModuleGenerator.java  |  85 ++
 .../atom/common/rome/AppModuleImpl.java       |  69 ++
 .../atom/common/rome/AppModuleParser.java     |  66 ++
 .../propono/atom/server/AtomException.java    |  53 ++
 .../propono/atom/server/AtomHandler.java      | 144 +++
 .../atom/server/AtomHandlerFactory.java       | 113 +++
 .../atom/server/AtomMediaResource.java        |  95 ++
 .../server/AtomNotAuthorizedException.java    |  49 +
 .../atom/server/AtomNotFoundException.java    |  48 +
 .../propono/atom/server/AtomRequest.java      | 139 +++
 .../propono/atom/server/AtomRequestImpl.java  | 114 +++
 .../propono/atom/server/AtomServlet.java      | 378 ++++++++
 .../server/FactoryConfigurationError.java     | 106 +++
 .../propono/atom/server/FactoryFinder.java    | 280 ++++++
 .../propono/atom/server/SecuritySupport.java  |  90 ++
 .../server/impl/FileBasedAtomHandler.java     | 443 +++++++++
 .../impl/FileBasedAtomHandlerFactory.java     |  37 +
 .../server/impl/FileBasedAtomService.java     | 188 ++++
 .../atom/server/impl/FileBasedCollection.java | 837 ++++++++++++++++++
 .../atom/server/impl/FileBasedWorkspace.java  |  34 +
 .../propono/atom/server/impl/FileStore.java   | 116 +++
 .../propono/blogclient/BaseBlogEntry.java     | 193 ++++
 .../rometools/propono/blogclient/Blog.java    | 213 +++++
 .../blogclient/BlogClientException.java       |  40 +
 .../propono/blogclient/BlogConnection.java    |  34 +
 .../blogclient/BlogConnectionFactory.java     |  80 ++
 .../propono/blogclient/BlogEntry.java         | 231 +++++
 .../propono/blogclient/BlogResource.java      |  39 +
 .../blogclient/atomprotocol/AtomBlog.java     | 206 +++++
 .../atomprotocol/AtomCollection.java          | 165 ++++
 .../atomprotocol/AtomConnection.java          |  92 ++
 .../blogclient/atomprotocol/AtomEntry.java    | 240 +++++
 .../atomprotocol/AtomEntryIterator.java       |  72 ++
 .../blogclient/atomprotocol/AtomResource.java | 134 +++
 .../propono/blogclient/blogclient-diagram.gif | Bin 0 -> 18568 bytes
 .../blogclient/metaweblog/MetaWeblogBlog.java | 436 +++++++++
 .../metaweblog/MetaWeblogConnection.java      | 105 +++
 .../metaweblog/MetaWeblogEntry.java           | 122 +++
 .../metaweblog/MetaWeblogResource.java        | 112 +++
 .../propono/utils/ProponoException.java       | 153 ++++
 .../rometools/propono/utils/Utilities.java    | 268 ++++++
 .../propono-version.properties                |   0
 src/main/{java => resources}/rome.properties  |   0
 .../propono/atom/client/AtomClientTest.java   | 459 ++++++++++
 .../atom/client/BloggerDotComTest.java        |  91 ++
 .../propono/atom/common/AtomServiceTest.java  | 160 ++++
 .../propono/atom/common/CollectionTest.java   |  68 ++
 .../atom/server/AtomClientServerTest.java     | 122 +++
 .../atom/server/TestAtomHandlerFactory.java   |  29 +
 .../atom/server/TestAtomHandlerImpl.java      |  31 +
 .../blogclient/SimpleBlogClientTest.java      | 220 +++++
 70 files changed, 10031 insertions(+)
 create mode 100644 src/main/java/org/rometools/propono/atom/client/AtomClientFactory.java
 create mode 100644 src/main/java/org/rometools/propono/atom/client/AuthStrategy.java
 create mode 100644 src/main/java/org/rometools/propono/atom/client/BasicAuthStrategy.java
 create mode 100644 src/main/java/org/rometools/propono/atom/client/ClientAtomService.java
 create mode 100644 src/main/java/org/rometools/propono/atom/client/ClientCategories.java
 create mode 100644 src/main/java/org/rometools/propono/atom/client/ClientCollection.java
 create mode 100644 src/main/java/org/rometools/propono/atom/client/ClientEntry.java
 create mode 100644 src/main/java/org/rometools/propono/atom/client/ClientMediaEntry.java
 create mode 100644 src/main/java/org/rometools/propono/atom/client/ClientWorkspace.java
 create mode 100644 src/main/java/org/rometools/propono/atom/client/EntryIterator.java
 create mode 100644 src/main/java/org/rometools/propono/atom/client/GDataAuthStrategy.java
 create mode 100644 src/main/java/org/rometools/propono/atom/client/NoAuthStrategy.java
 create mode 100644 src/main/java/org/rometools/propono/atom/client/OAuthStrategy.java
 create mode 100644 src/main/java/org/rometools/propono/atom/client/atomclient-diagram.gif
 create mode 100644 src/main/java/org/rometools/propono/atom/common/AtomService.java
 create mode 100644 src/main/java/org/rometools/propono/atom/common/Categories.java
 create mode 100644 src/main/java/org/rometools/propono/atom/common/Collection.java
 create mode 100644 src/main/java/org/rometools/propono/atom/common/Workspace.java
 create mode 100644 src/main/java/org/rometools/propono/atom/common/rome/AppModule.java
 create mode 100644 src/main/java/org/rometools/propono/atom/common/rome/AppModuleGenerator.java
 create mode 100644 src/main/java/org/rometools/propono/atom/common/rome/AppModuleImpl.java
 create mode 100644 src/main/java/org/rometools/propono/atom/common/rome/AppModuleParser.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/AtomException.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/AtomHandler.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/AtomHandlerFactory.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/AtomMediaResource.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/AtomNotAuthorizedException.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/AtomNotFoundException.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/AtomRequest.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/AtomRequestImpl.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/AtomServlet.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/FactoryConfigurationError.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/FactoryFinder.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/SecuritySupport.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/impl/FileBasedAtomHandler.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/impl/FileBasedAtomHandlerFactory.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/impl/FileBasedAtomService.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/impl/FileBasedCollection.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/impl/FileBasedWorkspace.java
 create mode 100644 src/main/java/org/rometools/propono/atom/server/impl/FileStore.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/BaseBlogEntry.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/Blog.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/BlogClientException.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/BlogConnection.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/BlogConnectionFactory.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/BlogEntry.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/BlogResource.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomBlog.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomCollection.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomConnection.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomEntry.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomEntryIterator.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomResource.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/blogclient-diagram.gif
 create mode 100644 src/main/java/org/rometools/propono/blogclient/metaweblog/MetaWeblogBlog.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/metaweblog/MetaWeblogConnection.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/metaweblog/MetaWeblogEntry.java
 create mode 100644 src/main/java/org/rometools/propono/blogclient/metaweblog/MetaWeblogResource.java
 create mode 100644 src/main/java/org/rometools/propono/utils/ProponoException.java
 create mode 100644 src/main/java/org/rometools/propono/utils/Utilities.java
 rename src/main/{java => resources}/propono-version.properties (100%)
 rename src/main/{java => resources}/rome.properties (100%)
 create mode 100644 src/test/java/org/rometools/propono/atom/client/AtomClientTest.java
 create mode 100644 src/test/java/org/rometools/propono/atom/client/BloggerDotComTest.java
 create mode 100644 src/test/java/org/rometools/propono/atom/common/AtomServiceTest.java
 create mode 100644 src/test/java/org/rometools/propono/atom/common/CollectionTest.java
 create mode 100644 src/test/java/org/rometools/propono/atom/server/AtomClientServerTest.java
 create mode 100644 src/test/java/org/rometools/propono/atom/server/TestAtomHandlerFactory.java
 create mode 100644 src/test/java/org/rometools/propono/atom/server/TestAtomHandlerImpl.java
 create mode 100644 src/test/java/org/rometools/propono/blogclient/SimpleBlogClientTest.java

diff --git a/src/main/java/org/rometools/propono/atom/client/AtomClientFactory.java b/src/main/java/org/rometools/propono/atom/client/AtomClientFactory.java
new file mode 100644
index 0000000..b023155
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/client/AtomClientFactory.java
@@ -0,0 +1,47 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.client;
+
+import org.rometools.propono.utils.ProponoException;
+import com.sun.syndication.io.impl.Atom10Parser;
+
+
+/**
+ * Creates AtomService or ClientCollection based on username, password and 
+ * end-point URI of Atom protocol service.
+ */
+public class AtomClientFactory {
+    
+    static {
+        Atom10Parser.setResolveURIs(true);
+    }
+    
+    /**
+     * Create AtomService by reading service doc from Atom Server.
+     */
+    public static ClientAtomService getAtomService(
+            String uri, AuthStrategy authStrategy) throws ProponoException {
+        return new ClientAtomService(uri, authStrategy);
+    }
+
+    /**
+     * Create ClientCollection bound to URI.
+     */
+    public static ClientCollection getCollection(
+            String uri, AuthStrategy authStrategy) throws ProponoException {
+        return new ClientCollection(uri, authStrategy);
+    }
+}
diff --git a/src/main/java/org/rometools/propono/atom/client/AuthStrategy.java b/src/main/java/org/rometools/propono/atom/client/AuthStrategy.java
new file mode 100644
index 0000000..6d5e41e
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/client/AuthStrategy.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2007 Dave Johnson (Blogapps project)
+ *
+ * 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.rometools.propono.atom.client;
+
+import org.rometools.propono.utils.ProponoException;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+
+
+public interface AuthStrategy {
+
+    /**
+     * Add authentication credenticals, tokens, etc. to HTTP method
+     */
+    void addAuthentication(HttpClient httpClient, HttpMethodBase method) 
+            throws ProponoException;
+}
diff --git a/src/main/java/org/rometools/propono/atom/client/BasicAuthStrategy.java b/src/main/java/org/rometools/propono/atom/client/BasicAuthStrategy.java
new file mode 100644
index 0000000..d38133e
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/client/BasicAuthStrategy.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2009 Dave Johnson (Blogapps project)
+ *
+ * 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.rometools.propono.atom.client;
+
+import com.sun.syndication.io.impl.Base64;
+import org.rometools.propono.utils.ProponoException;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+
+
+public class BasicAuthStrategy implements AuthStrategy {
+    private String credentials;
+
+    public BasicAuthStrategy(String username, String password) {
+        this.credentials =
+            new String(new Base64().encode((username + ":" + password).getBytes()));
+    }
+
+    public void init() throws ProponoException {
+        // op-op
+    }
+
+    public void addAuthentication(HttpClient httpClient, HttpMethodBase method) throws ProponoException {
+        httpClient.getParams().setAuthenticationPreemptive(true);
+        String header = "Basic " + credentials;
+        method.setRequestHeader("Authorization", header);
+    }
+}
diff --git a/src/main/java/org/rometools/propono/atom/client/ClientAtomService.java b/src/main/java/org/rometools/propono/atom/client/ClientAtomService.java
new file mode 100644
index 0000000..2eef955
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/client/ClientAtomService.java
@@ -0,0 +1,138 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.client;
+
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.io.impl.Atom10Parser;
+import org.rometools.propono.utils.ProponoException;
+import org.rometools.propono.atom.common.AtomService;
+import java.io.InputStreamReader;
+import java.util.Iterator;
+import java.util.List;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.input.SAXBuilder;
+
+
+/**
+ * This class models an Atom Publising Protocol Service Document.
+ * It extends the common 
+ * {@link com.sun.syndication.propono.atom.common.Collection} 
+ * class to add a <code>getEntry()</code> method and to return 
+ * {@link com.sun.syndication.propono.atom.client.ClientWorkspace} 
+ * objects instead of common 
+ * {@link com.sun.syndication.propono.atom.common.Workspace}s.
+ */
+public class ClientAtomService extends AtomService {
+    private static Log logger = LogFactory.getLog(ClientAtomService.class);
+    private String       uri = null;
+    private HttpClient   httpClient = null;
+    private AuthStrategy authStrategy = null;
+    
+    /**
+     * Create Atom blog service instance for specified URL and user account.
+     * @param url      End-point URL of Atom service
+     */
+    ClientAtomService(String uri, AuthStrategy authStrategy)
+        throws ProponoException {
+        this.uri = uri;
+        this.authStrategy = authStrategy;
+        Document doc = getAtomServiceDocument();
+        parseAtomServiceDocument(doc);
+    }
+    
+    /**
+     * Get full entry from service by entry edit URI.
+     */
+    public ClientEntry getEntry(String uri) throws ProponoException {      
+        GetMethod method = new GetMethod(uri);
+        authStrategy.addAuthentication(httpClient, method);
+        try {
+            httpClient.executeMethod(method);         
+            if (method.getStatusCode() != 200) {
+                throw new ProponoException("ERROR HTTP status code=" + method.getStatusCode());
+            }
+            Entry romeEntry = Atom10Parser.parseEntry(
+                    new InputStreamReader(method.getResponseBodyAsStream()), uri);
+            if (!romeEntry.isMediaEntry()) {
+                return new ClientEntry(this, null, romeEntry, false);
+            } else {
+                return new ClientMediaEntry(this, null, romeEntry, false);
+            }
+        } catch (Exception e) {
+            throw new ProponoException("ERROR: getting or parsing entry/media", e);
+        } finally {
+            method.releaseConnection();
+        }
+    }
+
+    void addAuthentication(HttpMethodBase method) throws ProponoException {
+        authStrategy.addAuthentication(httpClient, method);
+    }
+
+    AuthStrategy getAuthStrategy() {
+        return authStrategy;
+    }
+    
+    private Document getAtomServiceDocument() throws ProponoException {
+        GetMethod method = null;
+        int code = -1;
+        try {
+            httpClient = new HttpClient(new MultiThreadedHttpConnectionManager());
+            // TODO: make connection timeout configurable
+            httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(30000);            
+            
+            method = new GetMethod(uri);
+            authStrategy.addAuthentication(httpClient, method);
+            httpClient.executeMethod(method);
+            
+            SAXBuilder builder = new SAXBuilder();
+            return builder.build(method.getResponseBodyAsStream());
+            
+        } catch (Throwable t) {
+            String msg = "ERROR retrieving Atom Service Document, code: "+code;
+            logger.debug(msg, t);
+            throw new ProponoException(msg, t);
+        } finally {
+            if (method != null) method.releaseConnection();
+        }
+    }
+
+    /** Deserialize an Atom service XML document into an object */
+    private void parseAtomServiceDocument(Document document) throws ProponoException {
+        Element root = document.getRootElement();
+        List spaces = root.getChildren("workspace", AtomService.ATOM_PROTOCOL);
+        Iterator iter = spaces.iterator();
+        while (iter.hasNext()) {
+            Element e = (Element) iter.next();
+            addWorkspace(new ClientWorkspace(e, this, uri));
+        }
+    }
+    
+    /**
+     * Package access to httpClient.
+     */
+    HttpClient getHttpClient() {
+        return httpClient;
+    }
+}
+
diff --git a/src/main/java/org/rometools/propono/atom/client/ClientCategories.java b/src/main/java/org/rometools/propono/atom/client/ClientCategories.java
new file mode 100644
index 0000000..0fb5487
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/client/ClientCategories.java
@@ -0,0 +1,70 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+*  contributor license agreements.  The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.  For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/ 
+package org.rometools.propono.atom.client;
+
+import com.sun.syndication.feed.atom.Category;
+import org.rometools.propono.atom.common.*;
+import org.rometools.propono.utils.ProponoException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.JDOMException;
+import org.jdom.input.SAXBuilder;
+
+
+/** 
+ * Models an Atom protocol Categories element, which may contain ROME Atom 
+ * {@link com.sun.syndication.feed.atom.Category} elements. 
+ */
+public class ClientCategories extends Categories {    
+    private ClientCollection clientCollection = null;
+    
+    /** Load select from XML element */
+    public ClientCategories(Element e, ClientCollection clientCollection) throws ProponoException { 
+        this.clientCollection = clientCollection;
+        parseCategoriesElement(e);
+        if (getHref() != null) fetchContents();
+    }
+    
+    public void fetchContents() throws ProponoException {
+        GetMethod method = new GetMethod(getHrefResolved());
+        clientCollection.addAuthentication(method);
+        try {
+            clientCollection.getHttpClient().executeMethod(method);         
+            if (method.getStatusCode() != 200) {
+                throw new ProponoException("ERROR HTTP status code=" + method.getStatusCode());
+            }
+            SAXBuilder builder = new SAXBuilder();
+            Document catsDoc = builder.build(
+                    new InputStreamReader(method.getResponseBodyAsStream())); 
+            parseCategoriesElement(catsDoc.getRootElement());
+
+        } catch (IOException ioe) {
+            throw new ProponoException(
+                "ERROR: reading out-of-line categories", ioe);
+        } catch (JDOMException jde) {
+            throw new ProponoException(
+                "ERROR: parsing out-of-line categories", jde);
+        } finally {
+            method.releaseConnection();
+        }
+    }
+}
+
diff --git a/src/main/java/org/rometools/propono/atom/client/ClientCollection.java b/src/main/java/org/rometools/propono/atom/client/ClientCollection.java
new file mode 100644
index 0000000..e24d1bd
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/client/ClientCollection.java
@@ -0,0 +1,217 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.client;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List; 
+import java.io.InputStreamReader;
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.io.impl.Atom10Parser;
+import org.rometools.propono.atom.common.AtomService;
+import org.rometools.propono.atom.common.Categories;
+import org.rometools.propono.utils.ProponoException;
+import org.rometools.propono.atom.common.Collection;
+import org.rometools.propono.atom.common.Workspace;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.jdom.Element;
+
+/**
+ * Models an Atom collection, extends Collection and adds methods for adding,
+ * retrieving, updateing and deleting entries.
+ */
+public class ClientCollection extends Collection {
+    static final Log logger = LogFactory.getLog(ClientCollection.class);
+
+    private List         categories = new ArrayList();
+    private HttpClient   httpClient = null;
+    private AuthStrategy authStrategy = null;
+    private boolean      writable = true;
+
+    private ClientWorkspace workspace = null;
+    private ClientAtomService service = null;
+
+    ClientCollection(Element e, ClientWorkspace workspace, String baseURI)  throws ProponoException {
+        super(e, baseURI); 
+        this.workspace = workspace;
+        this.service = workspace.getAtomService();
+        this.httpClient = workspace.getAtomService().getHttpClient();
+        this.authStrategy = workspace.getAtomService().getAuthStrategy();
+        parseCollectionElement(e);
+    }
+
+    ClientCollection(String href, AuthStrategy authStrategy)  throws ProponoException {
+        super("Standalone connection", "text", href);
+        this.authStrategy = authStrategy;
+        try {
+            httpClient = new HttpClient(new MultiThreadedHttpConnectionManager());
+            // TODO: make connection timeout configurable
+            httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(30000); 
+        } catch (Throwable t) {
+            throw new ProponoException("ERROR creating HTTPClient", t);
+        }
+    }
+
+    void addAuthentication(HttpMethodBase method) throws ProponoException {
+        authStrategy.addAuthentication(httpClient, method);
+    }
+    
+    /**
+     * Package access to httpClient to allow use by ClientEntry and ClientMediaEntry.
+     */
+    HttpClient getHttpClient() {
+        return httpClient;
+    }
+
+    /** 
+     * Get iterator over entries in this collection. Entries returned are 
+     * considered to be partial entries cannot be saved/updated.
+     */
+    public Iterator getEntries() throws ProponoException {
+        return new EntryIterator(this); 
+    }
+    
+    /**
+     * Get full entry specified by entry edit URI. 
+     * Note that entry may or may not be associated with this collection.
+     * @return ClientEntry or ClientMediaEntry specified by URI.
+     */
+    public ClientEntry getEntry(String uri) throws ProponoException {      
+        GetMethod method = new GetMethod(uri);
+        authStrategy.addAuthentication(httpClient, method);
+        try {
+            httpClient.executeMethod(method);         
+            if (method.getStatusCode() != 200) {
+                throw new ProponoException("ERROR HTTP status code=" + method.getStatusCode());
+            }
+            Entry romeEntry = Atom10Parser.parseEntry(
+                    new InputStreamReader(method.getResponseBodyAsStream()), uri);
+            if (!romeEntry.isMediaEntry()) {
+                return new ClientEntry(service, this, romeEntry, false);
+            } else {
+                return new ClientMediaEntry(service, this, romeEntry, false);
+            }
+        } catch (Exception e) {
+            throw new ProponoException("ERROR: getting or parsing entry/media, HTTP code: ", e);
+        } finally {
+            method.releaseConnection();
+        }
+    }
+    
+    /**
+     * Get workspace or null if collection is not associated with a workspace.
+     */
+    public Workspace getWorkspace() {
+        return workspace;
+    }
+    
+    /**
+     * Determines if collection is writable.
+     */
+    public boolean isWritable() {
+        return writable;
+    }
+    
+    /** 
+     * Create new entry associated with collection, but do not save to server.
+     * @throws ProponoException if collecton is not writable.
+     */
+    public ClientEntry createEntry() throws ProponoException {
+        if (!isWritable()) throw new ProponoException("Collection is not writable");
+        return new ClientEntry(service, this);
+    }
+
+    /**
+     * Create new media entry assocaited with collection, but do not save.
+     * server. Depending on the Atom server, you may or may not be able to 
+     * persist the properties of the entry that is returned.
+     * @param title       Title to used for uploaded file.
+     * @param slug        String to be used in file-name of stored file
+     * @param contentType MIME content-type of file.
+     * @param bytes       Data to be uploaded as byte array.
+     * @throws ProponoException if collecton is not writable
+     */
+    public ClientMediaEntry createMediaEntry(
+            String title, String slug, String contentType, byte[] bytes) 
+            throws ProponoException {
+        if (!isWritable()) throw new ProponoException("Collection is not writable");
+        return new ClientMediaEntry(service, this, title, slug, contentType, bytes);
+    }
+    
+    /**
+     * Create new media entry assocaited with collection, but do not save.
+     * server. Depending on the Atom server, you may or may not be able to. 
+     * persist the properties of the entry that is returned.
+     * @param title       Title to used for uploaded file.
+     * @param slug        String to be used in file-name of stored file
+     * @param contentType MIME content-type of file.
+     * @param is          Data to be uploaded as InputStream.
+     * @throws ProponoException if collecton is not writable
+     */
+    public ClientMediaEntry createMediaEntry(
+            String title, String slug, String contentType, InputStream is) 
+            throws ProponoException {
+        if (!isWritable()) throw new ProponoException("Collection is not writable");
+        return new ClientMediaEntry(service, this, title, slug, contentType, is);
+    }
+    
+    /**
+     * Save to collection a new entry that was created by a createEntry() 
+     * or createMediaEntry() and save it to the server.
+     * @param entry Entry to be saved.
+     * @throws ProponoException on error, if collection is not writable or if entry is partial.
+     */
+    public void addEntry(ClientEntry entry) throws ProponoException {
+        if (!isWritable()) throw new ProponoException("Collection is not writable");
+        entry.addToCollection(this);
+    }
+
+    protected void parseCollectionElement(Element element) throws ProponoException {
+        if (workspace == null) return;
+        
+        setHref(element.getAttribute("href").getValue());
+        
+        Element titleElem = element.getChild("title", AtomService.ATOM_FORMAT);
+        if (titleElem != null) {
+            setTitle(titleElem.getText());
+            if (titleElem.getAttribute("type", AtomService.ATOM_FORMAT) != null) {
+                setTitleType(titleElem.getAttribute("type", AtomService.ATOM_FORMAT).getValue());
+            }
+        }
+                
+        List acceptElems = element.getChildren("accept",  AtomService.ATOM_PROTOCOL);
+        if (acceptElems != null && acceptElems.size() > 0) {
+            for (Iterator it = acceptElems.iterator(); it.hasNext();) {
+                Element acceptElem = (Element)it.next();
+                addAccept(acceptElem.getTextTrim());
+            }
+        }
+        
+        // Loop to parse <app:categories> element to Categories objects
+        List catsElems = element.getChildren("categories", AtomService.ATOM_PROTOCOL);
+        for (Iterator catsIter = catsElems.iterator(); catsIter.hasNext();) {
+            Element catsElem = (Element) catsIter.next();  
+            Categories cats = new ClientCategories(catsElem, this);
+            addCategories(cats);
+        }
+    }
+}
diff --git a/src/main/java/org/rometools/propono/atom/client/ClientEntry.java b/src/main/java/org/rometools/propono/atom/client/ClientEntry.java
new file mode 100644
index 0000000..8e3e151
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/client/ClientEntry.java
@@ -0,0 +1,259 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.client;
+
+import com.sun.syndication.feed.atom.Content;
+import com.sun.syndication.feed.atom.Link;
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.io.impl.Atom10Generator;
+import com.sun.syndication.io.impl.Atom10Parser;
+import org.rometools.propono.utils.ProponoException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+import org.apache.commons.httpclient.methods.DeleteMethod;
+import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
+import org.apache.commons.httpclient.methods.PutMethod;
+import org.apache.commons.httpclient.methods.StringRequestEntity;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.rometools.propono.utils.Utilities;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.beanutils.BeanUtils;
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+import org.apache.commons.httpclient.methods.PostMethod;
+
+/**
+ * Client implementation of Atom entry, extends ROME Entry to add methods for
+ * easily getting/setting content, updating and removing the entry from the server.
+ */
+public class ClientEntry extends Entry {
+    private static final Log  logger = LogFactory.getLog(ClientEntry.class); 
+    
+    boolean partial = false;    
+    
+    private ClientAtomService service = null;
+    private ClientCollection collection = null;
+    
+    public ClientEntry(ClientAtomService service, ClientCollection collection) {
+        super();
+        this.service = service;
+        this.collection = collection;
+    }
+
+    public ClientEntry(ClientAtomService service, ClientCollection collection, 
+            Entry entry, boolean partial) throws ProponoException { 
+        super();
+        this.service = service;
+        this.collection = collection;
+        this.partial = partial;
+        try {
+            BeanUtils.copyProperties(this, entry);
+        } catch (Exception e) {
+            throw new ProponoException("ERROR: copying fields from ROME entry", e);
+        }
+    }
+        
+    /** 
+     * Set content of entry.
+     * @param contentString content string.
+     * @param type Must be "text" for plain text, "html" for escaped HTML, 
+     *             "xhtml" for XHTML or a valid MIME content-type.
+     */
+    public void setContent(String contentString, String type) {
+        Content newContent = new Content();
+        newContent.setType(type == null ? Content.HTML : type);
+        newContent.setValue(contentString);
+        ArrayList contents = new ArrayList();
+        contents.add(newContent);
+        setContents(contents);
+    }
+    
+    /**
+     * Convenience method to set first content object in content collection.
+     * Atom 1.0 allows only one content element per entry.
+     */
+    public void setContent(Content c) {
+        ArrayList contents = new ArrayList();
+        contents.add(c);
+        setContents(contents);
+    }
+    
+    /**
+     * Convenience method to get first content object in content collection.
+     * Atom 1.0 allows only one content element per entry.
+     */
+    public Content getContent() {
+        if (getContents() != null && getContents().size() > 0) {
+            Content c = (Content)getContents().get(0);
+            return c;
+        }
+        return null;
+    }
+    
+    /**
+     * Determines if entries are equal based on edit URI.
+     */
+    public boolean equals(Object o) {
+        if (o instanceof ClientEntry) {
+            ClientEntry other = (ClientEntry)o;
+            if (other.getEditURI() != null && getEditURI() != null) {
+                return other.getEditURI().equals(getEditURI());
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * Update entry by posting new representation of entry to server.
+     * Note that you should not attempt to update entries that you get from
+     * iterating over a collection they may be "partial" entries. If you want 
+     * to update an entry, you must get it via one of the <code>getEntry()</code>
+     * methods in 
+     * {@link com.sun.syndication.propono.atom.common.Collection} or 
+     * {@link com.sun.syndication.propono.atom.common.AtomService}.
+     * @throws ProponoException If entry is a "partial" entry.
+     */
+    public void update() throws ProponoException {
+        if (partial) throw new ProponoException("ERROR: attempt to update partial entry");
+        EntityEnclosingMethod method = new PutMethod(getEditURI());
+        addAuthentication(method);
+        StringWriter sw = new StringWriter();
+        int code = -1;
+        try {
+            Atom10Generator.serializeEntry(this, sw);
+            method.setRequestEntity(new StringRequestEntity(sw.toString()));
+            method.setRequestHeader(
+                "Content-type", "application/atom+xml; charset=utf-8");        
+            getHttpClient().executeMethod(method);
+            InputStream is = method.getResponseBodyAsStream();
+            if (method.getStatusCode() != 200 && method.getStatusCode() != 201) {
+                throw new ProponoException(
+                    "ERROR HTTP status=" + method.getStatusCode() + " : " + Utilities.streamToString(is));
+            }
+
+        } catch (Exception e) {
+            String msg = "ERROR: updating entry, HTTP code: " + code;
+            logger.debug(msg, e);
+            throw new ProponoException(msg, e);
+        } finally {
+            method.releaseConnection();
+        }      
+    }
+    
+    /**
+     * Remove entry from server.
+     */
+    public void remove() throws ProponoException {
+        if (getEditURI() == null) {
+            throw new ProponoException("ERROR: cannot delete unsaved entry");
+        }
+        DeleteMethod method = new DeleteMethod(getEditURI());
+        addAuthentication(method);
+        try {
+            getHttpClient().executeMethod(method);
+        } catch (IOException ex) {
+            throw new ProponoException("ERROR: removing entry, HTTP code", ex);
+        } finally {
+            method.releaseConnection();
+        }
+    }
+    
+    void setCollection(ClientCollection collection) {
+        this.collection = collection;
+    }
+
+    ClientCollection getCollection() {
+        return collection;
+    }
+
+    /**
+     * Get the URI that can be used to edit the entry via HTTP PUT or DELETE.
+     */ 
+    public String getEditURI() {
+        for (int i=0; i<getOtherLinks().size(); i++) {
+            Link link = (Link)getOtherLinks().get(i);
+            if (link.getRel() != null && link.getRel().equals("edit")) {
+                return link.getHrefResolved();
+            }
+        }
+        return null;
+    }
+    
+    void addToCollection(ClientCollection col) throws ProponoException {
+        setCollection(col);
+        EntityEnclosingMethod method = new PostMethod(getCollection().getHrefResolved());
+        addAuthentication(method);
+        StringWriter sw = new StringWriter();
+        int code = -1;
+        try {
+            Atom10Generator.serializeEntry(this, sw);
+            method.setRequestEntity(new StringRequestEntity(sw.toString()));
+            method.setRequestHeader(
+                "Content-type", "application/atom+xml; charset=utf-8");        
+            getHttpClient().executeMethod(method);
+            InputStream is = method.getResponseBodyAsStream();
+            code = method.getStatusCode();
+            if (code != 200 && code != 201) {
+                throw new ProponoException(
+                    "ERROR HTTP status=" + code + " : " + Utilities.streamToString(is));
+            }
+            Entry romeEntry = Atom10Parser.parseEntry(
+                new InputStreamReader(is), getCollection().getHrefResolved());
+            BeanUtils.copyProperties(this, romeEntry);
+
+        } catch (Exception e) {  
+            String msg = "ERROR: saving entry, HTTP code: " + code;
+            logger.debug(msg, e);
+            throw new ProponoException(msg, e);
+        } finally {
+            method.releaseConnection();
+        }
+        Header locationHeader = method.getResponseHeader("Location");
+        if (locationHeader == null) {
+            logger.warn("WARNING added entry, but no location header returned");
+        } else if (getEditURI() == null) {
+            List links = getOtherLinks();
+            Link link = new Link();
+            link.setHref(locationHeader.getValue());
+            link.setRel("edit");
+            links.add(link);
+            setOtherLinks(links);
+        }
+    }
+    
+    void addAuthentication(HttpMethodBase method) throws ProponoException {
+        if (service != null) {
+            service.addAuthentication(method);
+        } else if (collection != null) {
+            collection.addAuthentication(method);
+        }
+    }
+    
+    HttpClient getHttpClient() {
+        if (service != null) {
+            return service.getHttpClient();
+        } else if (collection != null) {
+            return collection.getHttpClient();
+        }
+        return null;
+    }
+
+}
diff --git a/src/main/java/org/rometools/propono/atom/client/ClientMediaEntry.java b/src/main/java/org/rometools/propono/atom/client/ClientMediaEntry.java
new file mode 100644
index 0000000..5ac42e6
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/client/ClientMediaEntry.java
@@ -0,0 +1,322 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.client;
+
+import com.sun.syndication.feed.atom.Content;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.feed.atom.Link;
+import com.sun.syndication.io.FeedException;
+import com.sun.syndication.io.impl.Atom10Generator;
+import com.sun.syndication.io.impl.Atom10Parser;
+import org.rometools.propono.utils.ProponoException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
+import org.apache.commons.httpclient.methods.PutMethod;
+import org.apache.commons.httpclient.methods.StringRequestEntity;
+import org.rometools.propono.utils.Utilities;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.beanutils.BeanUtils;
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jdom.JDOMException;
+
+/**
+ * Client implementation of Atom media-link entry, an Atom entry that provides
+ * meta-data for a media file (e.g. uploaded image or audio file). 
+ */
+public class ClientMediaEntry extends ClientEntry {
+    private static final Log  logger = LogFactory.getLog(ClientMediaEntry.class);  
+    
+    private String slug = null;
+    private byte[] bytes;
+    private InputStream inputStream;
+    
+    /**
+     * Create ClientMedieEntry for service and collection.
+     */
+    public ClientMediaEntry(ClientAtomService service, ClientCollection collection) {
+        super(service, collection);
+    }
+
+    public ClientMediaEntry(ClientAtomService service, ClientCollection collection, 
+            Entry entry, boolean partial) throws ProponoException { 
+        super(service, collection, entry, partial);
+    }  
+    
+    public ClientMediaEntry(ClientAtomService service, ClientCollection collection, 
+            String title, String slug, String contentType, InputStream is) {
+        this(service, collection); 
+        this.inputStream = is;
+        setTitle(title);
+        setSlug(slug);
+        Content content = new Content();
+        content.setType(contentType);
+        List contents = new ArrayList();
+        contents.add(content);
+        setContents(contents);
+    } 
+        
+    public ClientMediaEntry(ClientAtomService service, ClientCollection collection, 
+            String title, String slug, String contentType, byte[] bytes) {
+        this(service, collection); 
+        this.bytes = bytes;
+        setTitle(title);
+        setSlug(slug);
+        Content content = new Content();
+        content.setType(contentType);
+        List contents = new ArrayList();
+        contents.add(content);
+        setContents(contents);
+    } 
+        
+    /** 
+     * Get bytes of media resource associated with entry.
+     * @return Bytes or null if none available or if entry uses an InputStream instead.
+     */
+    public byte[] getBytes() {
+        return bytes;
+    }
+
+    /**
+     * Set media resource data as a byte array, don't try this if you have already
+     * set the data as an InputStream. 
+     */
+    public void setBytes(byte[] bytes) {
+        if (inputStream != null) {
+            throw new IllegalStateException("ERROR: already has inputStream, cannot set both inputStream and bytes"); 
+        }
+        this.bytes = bytes;
+    }
+
+    /**
+     * Get input stream for media resource associated with this entry.
+     * @return InputStream or null if none available or if entry uses bytes instead.
+     */
+    public InputStream getInputStream() {
+        return inputStream;
+    }
+
+    /**
+     * Set media resource data as an input stream, don't try this if you have already
+     * set the data as a byte array.
+     */
+    public void setInputStream(InputStream inputStream) {
+        if (bytes != null) {
+            throw new IllegalStateException("ERROR: already has bytes, cannot set both bytes and inputStream");
+        }
+        this.inputStream = inputStream;
+    }
+
+    /**
+     * Get media link URI for editing the media resource associated with this
+     * entry via HTTP PUT or DELETE.
+     */
+    public String getMediaLinkURI() {
+        for (int i=0; i<getOtherLinks().size(); i++) {
+            Link link = (Link)getOtherLinks().get(i);
+            if (link.getRel() != null && link.getRel().equals("edit-media")) {
+                return link.getHrefResolved();
+            }
+        }
+        return null;
+    }
+       
+    /**
+     * Get media resource as an InputStream, should work regardless of whether
+     * you set the media resource data as an InputStream or as a byte array.
+     */
+    public InputStream getAsStream() throws ProponoException {
+        if (getContents() != null && getContents().size() > 0) {
+            Content c = (Content)getContents().get(0);
+            if (c.getSrc() != null) {
+                return getResourceAsStream();
+            } else if (inputStream != null) {
+                return inputStream;
+            } else if (bytes != null) {
+                return new ByteArrayInputStream(bytes);
+            } else {
+                throw new ProponoException("ERROR: no src URI or binary data to return");
+            } 
+        }
+        else {
+            throw new ProponoException("ERROR: no content found in entry");
+        }
+    }
+    
+    private InputStream getResourceAsStream() throws ProponoException {
+        if (getEditURI() == null) {
+            throw new ProponoException("ERROR: not yet saved to server");
+        }
+        GetMethod method = new GetMethod(((Content)getContents()).getSrc());
+        try {
+            getCollection().getHttpClient().executeMethod(method);
+            if (method.getStatusCode() != 200) {
+                throw new ProponoException("ERROR HTTP status=" + method.getStatusCode());
+            }
+            return method.getResponseBodyAsStream();
+        } catch (IOException e) {
+            throw new ProponoException("ERROR: getting media entry", e);
+        }
+    } 
+    
+    /**
+     * Update entry on server. 
+     */
+    public void update() throws ProponoException {
+        if (partial) throw new ProponoException("ERROR: attempt to update partial entry");
+        EntityEnclosingMethod method = null;
+        Content updateContent = (Content)getContents().get(0);
+        try {
+            if (getMediaLinkURI() != null && getBytes() != null) {
+                // existing media entry and new file, so PUT file to edit-media URI
+                method = new PutMethod(getMediaLinkURI());
+                if (inputStream != null) {
+                    method.setRequestEntity(new InputStreamRequestEntity(inputStream));
+                } else {
+                    method.setRequestEntity(new InputStreamRequestEntity(new ByteArrayInputStream(getBytes())));
+                }
+                
+                method.setRequestHeader("Content-type", updateContent.getType());
+            } 
+            else if (getEditURI() != null) {
+                // existing media entry and NO new file, so PUT entry to edit URI
+                method = new PutMethod(getEditURI());
+                StringWriter sw = new StringWriter();
+                Atom10Generator.serializeEntry(this, sw);
+                method.setRequestEntity(new StringRequestEntity(sw.toString()));
+                method.setRequestHeader(
+                    "Content-type", "application/atom+xml; charset=utf8"); 
+            } else {
+                throw new ProponoException("ERROR: media entry has no edit URI or media-link URI");
+            }
+            this.getCollection().addAuthentication(method);
+            method.addRequestHeader("Title", getTitle());
+            getCollection().getHttpClient().executeMethod(method); 
+            if (inputStream != null) inputStream.close();
+            InputStream is = method.getResponseBodyAsStream();
+            if (method.getStatusCode() != 200 && method.getStatusCode() != 201) {
+                throw new ProponoException(
+                    "ERROR HTTP status=" + method.getStatusCode() + " : " + Utilities.streamToString(is));
+            }
+            
+        } catch (Exception e) {
+            throw new ProponoException("ERROR: saving media entry");
+        }
+        if (method.getStatusCode() != 201) { 
+            throw new ProponoException("ERROR HTTP status=" + method.getStatusCode());
+        }
+    }
+            
+    /** Package access, to be called by DefaultClientCollection */
+    void addToCollection(ClientCollection col) throws ProponoException {
+        setCollection(col);
+        EntityEnclosingMethod method = new PostMethod(col.getHrefResolved());
+        getCollection().addAuthentication(method);
+        StringWriter sw = new StringWriter();
+        boolean error = false;
+        try {
+            Content c = (Content)getContents().get(0);
+            if (inputStream != null) {
+                method.setRequestEntity(new InputStreamRequestEntity(inputStream));
+            } else {
+                method.setRequestEntity(new InputStreamRequestEntity(new ByteArrayInputStream(getBytes())));
+            }
+            method.setRequestHeader("Content-type", c.getType()); 
+            method.setRequestHeader("Title", getTitle());
+            method.setRequestHeader("Slug", getSlug());
+            getCollection().getHttpClient().executeMethod(method);
+            if (inputStream != null) inputStream.close();
+            InputStream is = method.getResponseBodyAsStream();
+            if (method.getStatusCode() == 200 || method.getStatusCode() == 201) {
+                Entry romeEntry = Atom10Parser.parseEntry(
+                   new InputStreamReader(is), col.getHrefResolved());
+                BeanUtils.copyProperties(this, romeEntry);                        
+                
+            } else {
+                throw new ProponoException(
+                    "ERROR HTTP status-code=" + method.getStatusCode() 
+                 + " status-line: " + method.getStatusLine()); 
+            }
+        } catch (IOException ie) {
+            throw new ProponoException("ERROR: saving media entry", ie);
+        } catch (JDOMException je) {
+            throw new ProponoException("ERROR: saving media entry", je);
+        } catch (FeedException fe) {
+            throw new ProponoException("ERROR: saving media entry", fe);
+        } catch (IllegalAccessException ae) {
+            throw new ProponoException("ERROR: saving media entry", ae);
+        } catch (InvocationTargetException te) {
+            throw new ProponoException("ERROR: saving media entry", te);
+        }
+        Header locationHeader = method.getResponseHeader("Location");
+        if (locationHeader == null) {
+            logger.warn("WARNING added entry, but no location header returned");
+        } else if (getEditURI() == null) {
+            List links = getOtherLinks();
+            Link link = new Link();
+            link.setHref(locationHeader.getValue());
+            link.setRel("edit");
+            links.add(link);
+            setOtherLinks(links);
+        }
+    }
+
+    /** Set string to be used in file name of new media resource on server. */
+    public String getSlug() {
+        return slug;
+    }
+
+    /** Get string to be used in file name of new media resource on server. */
+    public void setSlug(String slug) {
+        this.slug = slug;
+    }
+
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/org/rometools/propono/atom/client/ClientWorkspace.java b/src/main/java/org/rometools/propono/atom/client/ClientWorkspace.java
new file mode 100644
index 0000000..3fc47c7
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/client/ClientWorkspace.java
@@ -0,0 +1,66 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.client;
+
+import org.rometools.propono.atom.common.AtomService;
+import org.rometools.propono.atom.common.Workspace;
+import org.rometools.propono.atom.common.Workspace;
+import org.rometools.propono.utils.ProponoException;
+import java.util.Iterator;
+import java.util.List;
+import org.jdom.Element;
+
+
+/**
+ * Represents Atom protocol workspace on client-side.
+ * It extends the common 
+ * {@link com.sun.syndication.propono.atom.common.Workspace} 
+ * to return 
+ * {@link com.sun.syndication.propono.atom.client.ClientCollection} 
+ * objects instead of common 
+ * {@link com.sun.syndication.propono.atom.common.Collection}s.
+ */
+public class ClientWorkspace extends Workspace {
+    private ClientAtomService atomService = null;
+        
+    ClientWorkspace(Element e, ClientAtomService atomService, String baseURI) throws ProponoException {
+        super("dummy", "dummy"); 
+        this.atomService = atomService;      
+        parseWorkspaceElement(e, baseURI);   
+    }       
+                
+    /**
+     * Package access to parent service.
+     */
+    ClientAtomService getAtomService() {
+        return atomService;
+    }
+    
+    /** Deserialize a Atom workspace XML element into an object */
+    protected void parseWorkspaceElement(Element element, String baseURI) throws ProponoException {       
+        Element titleElem = element.getChild("title", AtomService.ATOM_FORMAT);
+        setTitle(titleElem.getText());
+        if (titleElem.getAttribute("type", AtomService.ATOM_FORMAT) != null) {
+            setTitleType(titleElem.getAttribute("type", AtomService.ATOM_FORMAT).getValue());
+        }       
+        List collections = element.getChildren("collection", AtomService.ATOM_PROTOCOL);
+        Iterator iter = collections.iterator();
+        while (iter.hasNext()) {
+            Element e = (Element) iter.next();
+            addCollection(new ClientCollection(e, this, baseURI)); 
+        }
+    }   
+}
diff --git a/src/main/java/org/rometools/propono/atom/client/EntryIterator.java b/src/main/java/org/rometools/propono/atom/client/EntryIterator.java
new file mode 100644
index 0000000..13d8226
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/client/EntryIterator.java
@@ -0,0 +1,124 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.client;
+
+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.WireFeedInput;
+import org.rometools.propono.utils.ProponoException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jdom.Document;
+import org.jdom.input.SAXBuilder;
+
+
+/** 
+ * Enables iteration over entries in Atom protocol collection.
+ */
+public class EntryIterator implements Iterator {
+    static final Log logger = LogFactory.getLog(EntryIterator.class);
+    private final ClientCollection collection;
+
+    int      maxEntries = 20;
+    int      offset = 0;
+    Iterator members = null;
+    Feed     col = null;
+    String   collectionURI;
+    String   nextURI;
+    
+    EntryIterator(ClientCollection collection) throws ProponoException {
+        this.collection = collection;
+        collectionURI = collection.getHrefResolved();
+        nextURI = collectionURI;
+        getNextEntries();
+    }
+    
+    /**
+     * Returns true if more entries are available. 
+     */
+    public boolean hasNext() {
+        if (!members.hasNext()) {
+            try { 
+                getNextEntries(); 
+            } catch (Exception ignored) {
+                logger.error("ERROR getting next entries", ignored);
+            }
+        }
+        return members.hasNext();
+    }
+    
+    /**
+     * Get next entry in collection.
+     */
+    public Object next() {
+        if (hasNext()) {
+            Entry romeEntry = (Entry)members.next();
+            try {
+                if (!romeEntry.isMediaEntry()) {
+                    return new ClientEntry(null, collection, romeEntry, true);
+                } else { 
+                    return new ClientMediaEntry(null, collection, romeEntry, true);
+                }
+            } catch (ProponoException e) {
+                throw new RuntimeException("Unexpected exception creating ClientEntry or ClientMedia", e);
+            }
+        }
+        throw new NoSuchElementException();
+    }
+    
+    /**
+     * Remove entry is not implemented.
+     */
+    public void remove() {
+        // optional method, not implemented
+    }
+    
+    private void getNextEntries() throws ProponoException {  
+        if (nextURI == null) return;
+        GetMethod colGet = new GetMethod( collection.getHrefResolved(nextURI) );
+        collection.addAuthentication(colGet);
+        try {
+            collection.getHttpClient().executeMethod(colGet);
+            SAXBuilder builder = new SAXBuilder();
+            Document doc = builder.build(colGet.getResponseBodyAsStream());
+            WireFeedInput feedInput = new WireFeedInput();
+            col = (Feed) feedInput.build(doc);
+        }  catch (Exception e) {
+            throw new ProponoException("ERROR: fetching or parsing next entries, HTTP code: " + (colGet != null ? colGet.getStatusCode() : -1), e);
+        } finally {
+            colGet.releaseConnection();
+        }
+        members = col.getEntries().iterator();
+        offset += col.getEntries().size();
+
+        nextURI = null;
+        List altLinks = col.getOtherLinks();
+        if (altLinks != null) {
+            Iterator iter = altLinks.iterator();
+            while (iter.hasNext()) {
+                Link link = (Link)iter.next();
+                if ("next".equals(link.getRel())) {
+                    nextURI = link.getHref();
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/rometools/propono/atom/client/GDataAuthStrategy.java b/src/main/java/org/rometools/propono/atom/client/GDataAuthStrategy.java
new file mode 100644
index 0000000..fcb7920
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/client/GDataAuthStrategy.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2009 Dave Johnson (Blogapps project)
+ *
+ * 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.rometools.propono.atom.client;
+
+import org.rometools.propono.utils.ProponoException;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+import org.apache.commons.httpclient.NameValuePair;
+import org.apache.commons.httpclient.methods.PostMethod;
+
+
+public class GDataAuthStrategy implements AuthStrategy {
+    private String email;
+    private String password;
+    private String service;
+    private String authToken;
+
+    public GDataAuthStrategy(String email, String password, String service) throws ProponoException {
+        this.email = email;
+        this.password = password;
+        this.service = service;
+        init();
+    }
+
+    private void init() throws ProponoException {
+        try {
+            HttpClient httpClient = new HttpClient();
+            PostMethod method = new PostMethod("https://www.google.com/accounts/ClientLogin");
+            NameValuePair[] data = {
+              new NameValuePair("Email", email),
+              new NameValuePair("Passwd", password),
+              new NameValuePair("accountType", "GOOGLE"),
+              new NameValuePair("service", service),
+              new NameValuePair("source", "ROME Propono Atompub Client")
+            };
+            method.setRequestBody(data);
+            httpClient.executeMethod(method);
+
+            String responseBody = method.getResponseBodyAsString();
+            int authIndex = responseBody.indexOf("Auth=");
+
+            authToken = "GoogleLogin auth=" + responseBody.trim().substring(authIndex + 5);
+
+        } catch (Throwable t) {
+            t.printStackTrace();
+            throw new ProponoException("ERROR obtaining Google authentication string", t);
+        }
+    }
+
+    public void addAuthentication(HttpClient httpClient, HttpMethodBase method) throws ProponoException {
+        httpClient.getParams().setAuthenticationPreemptive(true);
+        method.setRequestHeader("Authorization", authToken);
+    }
+}
diff --git a/src/main/java/org/rometools/propono/atom/client/NoAuthStrategy.java b/src/main/java/org/rometools/propono/atom/client/NoAuthStrategy.java
new file mode 100644
index 0000000..2e620e3
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/client/NoAuthStrategy.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2009 Dave Johnson (Blogapps project)
+ *
+ * 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.rometools.propono.atom.client;
+
+import org.rometools.propono.utils.ProponoException;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+
+/**
+ * No authentication
+ */
+public class NoAuthStrategy implements AuthStrategy {
+
+    public void addAuthentication(HttpClient httpClient, HttpMethodBase method) throws ProponoException {
+        // no-op
+    }
+
+}
diff --git a/src/main/java/org/rometools/propono/atom/client/OAuthStrategy.java b/src/main/java/org/rometools/propono/atom/client/OAuthStrategy.java
new file mode 100644
index 0000000..b0da2e9
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/client/OAuthStrategy.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2009 Dave Johnson (Blogapps project)
+ *
+ * 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.rometools.propono.atom.client;
+
+import org.rometools.propono.utils.ProponoException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import net.oauth.OAuth;
+import net.oauth.OAuthAccessor;
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthMessage;
+import net.oauth.OAuthServiceProvider;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+import org.apache.commons.httpclient.NameValuePair;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.util.ParameterParser;
+
+
+/**
+ * Strategy for using OAuth.
+ */
+public class OAuthStrategy implements AuthStrategy {
+
+    private State state = State.UNAUTHORIZED;
+
+    private enum State {
+        UNAUTHORIZED,  // have not sent any requests
+        REQUEST_TOKEN, // have a request token
+        AUTHORIZED,    // are authorized
+        ACCESS_TOKEN   // have access token, ready to make calls
+    }
+
+    private String username;
+    private String consumerKey;
+    private String consumerSecret;
+    private String keyType;
+
+    private String reqUrl;
+    private String authzUrl;
+    private String accessUrl;
+
+    private String nonce;
+    private long timestamp;
+
+    private String requestToken = null;
+    private String accessToken = null;
+    private String tokenSecret = null;
+
+    /**
+     * Create OAuth authentcation strategy and negotiate with services to
+     * obtain access token to be used in subsequent calls.
+     * 
+     * @param username  Username to be used in authentication
+     * @param key       Consumer key
+     * @param secret    Consumer secret
+     * @param keyType   Key type (e.g. "HMAC-SHA1")
+     * @param reqUrl    URL of request token service
+     * @param authzUrl  URL of authorize service
+     * @param accessUrl URL of acess token service
+     * @throws ProponoException on any sort of initialization error
+     */
+    public OAuthStrategy(
+        String username, String key, String secret, String keyType,
+        String reqUrl, String authzUrl, String accessUrl) throws ProponoException {
+
+        this.username       = username;
+        this.reqUrl         = reqUrl;
+        this.authzUrl       = authzUrl;
+        this.accessUrl      = accessUrl;
+        this.consumerKey    = key;
+        this.consumerSecret = secret;
+        this.keyType        = keyType;
+
+        this.nonce = UUID.randomUUID().toString();
+        this.timestamp = (long)(new Date().getTime()/1000L);
+
+        init();
+    }
+
+    private void init() throws ProponoException {
+        callOAuthUri(reqUrl);
+        callOAuthUri(authzUrl);
+        callOAuthUri(accessUrl);
+    }
+
+    public void addAuthentication(HttpClient httpClient, HttpMethodBase method) throws ProponoException {
+
+        if (state != State.ACCESS_TOKEN) {
+            throw new ProponoException("ERROR: authentication strategy failed init");
+        }
+
+        // add OAuth name/values to request query string
+
+        // wish we didn't have to parse them apart first, ugh
+        List originalqlist = null;
+        if (method.getQueryString() != null) {
+            String qstring = method.getQueryString().trim();
+            qstring = qstring.startsWith("?") ? qstring.substring(1) : qstring;
+            originalqlist = new ParameterParser().parse(qstring, '&');
+        } else {
+            originalqlist = new ArrayList();
+        }
+
+        // put query string into hashmap form to please OAuth.net classes
+        Map params = new HashMap();
+        for (Iterator it = originalqlist.iterator(); it.hasNext();) {
+            NameValuePair pair = (NameValuePair)it.next();
+            params.put(pair.getName(), pair.getValue());
+        }
+
+        // add OAuth params to query string
+        params.put("xoauth_requestor_id", username);
+        params.put("oauth_consumer_key", consumerKey);
+        params.put("oauth_signature_method", keyType);
+        params.put("oauth_timestamp", Long.toString(timestamp));
+        params.put("oauth_nonce", nonce);
+        params.put("oauth_token", accessToken);
+        params.put("oauth_token_secret", tokenSecret);
+
+        // sign complete URI
+        String finalUri = null;
+        OAuthServiceProvider provider =
+            new OAuthServiceProvider(reqUrl, authzUrl, accessUrl);
+        OAuthConsumer consumer =
+            new OAuthConsumer(null, consumerKey, consumerSecret, provider);
+        OAuthAccessor accessor = new OAuthAccessor(consumer);
+        accessor.tokenSecret = tokenSecret;
+        OAuthMessage message;
+        try {
+            message = new OAuthMessage(
+               method.getName(), method.getURI().toString(), params.entrySet());
+            message.sign(accessor);
+            
+            finalUri = OAuth.addParameters(message.URL, message.getParameters());
+
+        } catch (Exception ex) {
+            throw new ProponoException("ERROR: OAuth signing request", ex);
+        }
+
+        // pull query string off and put it back onto method
+        method.setQueryString(finalUri.substring(finalUri.lastIndexOf("?")));
+    }
+
+
+    private void callOAuthUri(String uri) throws ProponoException {
+
+        final HttpClient httpClient = new HttpClient();
+
+        final HttpMethodBase method;
+        final String content;
+
+        Map<String, String> params = new HashMap<String, String>();
+        if (params == null) {
+            params = new HashMap<String, String>();
+        }
+        params.put("oauth_version", "1.0");
+        if (username != null) {
+            params.put("xoauth_requestor_id", username);
+        }
+        params.put("oauth_consumer_key", consumerKey);
+        params.put("oauth_signature_method", keyType);
+        params.put("oauth_timestamp", Long.toString(timestamp));
+        params.put("oauth_nonce", nonce);
+        params.put("oauth_callback", "none");
+
+        OAuthServiceProvider provider =
+            new OAuthServiceProvider(reqUrl, authzUrl, accessUrl);
+        OAuthConsumer consumer =
+            new OAuthConsumer(null, consumerKey, consumerSecret, provider);
+        OAuthAccessor accessor = new OAuthAccessor(consumer);
+
+        if (state == State.UNAUTHORIZED) {
+
+            try {
+                OAuthMessage message = new OAuthMessage("GET", uri, params.entrySet());
+                message.sign(accessor);
+
+                String finalUri = OAuth.addParameters(message.URL, message.getParameters());
+                method = new GetMethod(finalUri);
+                httpClient.executeMethod(method);
+                content = method.getResponseBodyAsString();
+
+            } catch (Exception e) {
+                throw new ProponoException("ERROR fetching request token", e);
+            }
+
+        } else if (state == State.REQUEST_TOKEN) {
+
+            try {
+                params.put("oauth_token", requestToken);
+                params.put("oauth_token_secret", tokenSecret);
+                accessor.tokenSecret = tokenSecret;
+
+                OAuthMessage message = new OAuthMessage("POST", uri, params.entrySet());
+                message.sign(accessor);
+
+                String finalUri = OAuth.addParameters(message.URL, message.getParameters());
+                method = new PostMethod(finalUri);
+                httpClient.executeMethod(method);
+                content = method.getResponseBodyAsString();
+
+            } catch (Exception e) {
+                throw new ProponoException("ERROR fetching request token", e);
+            }
+
+        } else if (state == State.AUTHORIZED) {
+            
+            try {
+                params.put("oauth_token", accessToken);
+                params.put("oauth_token_secret", tokenSecret);
+                accessor.tokenSecret = tokenSecret;
+
+                OAuthMessage message = new OAuthMessage("GET", uri, params.entrySet());
+                message.sign(accessor);
+
+                String finalUri = OAuth.addParameters(message.URL, message.getParameters());
+                method = new GetMethod(finalUri);
+                httpClient.executeMethod(method);
+                content = method.getResponseBodyAsString();
+                
+            } catch (Exception e) {
+                throw new ProponoException("ERROR fetching request token", e);
+            }
+            
+        } else {
+            method = null;
+            content = null;
+            return;
+        }
+
+
+        String token = null;
+        String secret = null;
+
+        if (content != null) {
+            String[] settings = content.split("&");
+            for (int i=0; i<settings.length; i++) {
+                String[] setting = settings[i].split("=");
+                if (setting.length > 1) {
+                    if ("oauth_token".equals(setting[0])) {
+                        token = setting[1];
+                    } else if ("oauth_token_secret".equals(setting[0])) {
+                        secret = setting[1];
+                    }
+                }
+            }
+        }
+
+        switch (state) {
+
+            case UNAUTHORIZED:
+                if (token != null && secret != null) {
+                    requestToken = token;
+                    tokenSecret = secret;
+                    state = State.REQUEST_TOKEN;
+                } else {
+                    throw new ProponoException("ERROR: requestToken or tokenSecret is null");
+                }
+                break;
+
+            case REQUEST_TOKEN:
+                if (method.getStatusCode() == 200) {
+                    state = State.AUTHORIZED;
+                } else {
+                    throw new ProponoException("ERROR: authorization returned code: " + method.getStatusCode());
+                }
+                break;
+
+            case AUTHORIZED:
+                if (token != null && secret != null) {
+                    accessToken = token;
+                    tokenSecret = secret;
+                    state = State.ACCESS_TOKEN;
+                } else {
+                    throw new ProponoException("ERROR: accessToken or tokenSecret is null");
+                }
+                break;
+        }
+    }
+}
+
+
diff --git a/src/main/java/org/rometools/propono/atom/client/atomclient-diagram.gif b/src/main/java/org/rometools/propono/atom/client/atomclient-diagram.gif
new file mode 100644
index 0000000000000000000000000000000000000000..21b109493156d9d46d41e44dc7c0e3a859a74389
GIT binary patch
literal 19120
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

literal 0
HcmV?d00001

diff --git a/src/main/java/org/rometools/propono/atom/common/AtomService.java b/src/main/java/org/rometools/propono/atom/common/AtomService.java
new file mode 100644
index 0000000..9ef7d8c
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/common/AtomService.java
@@ -0,0 +1,122 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+*  contributor license agreements.  The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.  For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/ 
+package org.rometools.propono.atom.common;
+
+
+import org.rometools.propono.utils.ProponoException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.Namespace;
+
+/**
+ * Models an Atom Publishing Protocol Service Document.
+ * Is able to read a Service document from a JDOM Document
+ * and to write Service document out as a JDOM Document.
+ */
+public class AtomService {
+
+    private List workspaces = new ArrayList();
+    
+    /** Namespace for Atom Syndication Format */
+    public static Namespace ATOM_FORMAT =
+        Namespace.getNamespace("atom","http://www.w3.org/2005/Atom");
+    
+    /** Namespace for Atom Publishing Protocol */
+    public static Namespace ATOM_PROTOCOL = 
+        Namespace.getNamespace("app","http://www.w3.org/2007/app");  
+        
+    /**
+     * Create new and empty Atom service
+     */
+    public AtomService() {
+    }
+    
+    /**
+     * Add Workspace to service.
+     */
+    public void addWorkspace(Workspace workspace) {
+        workspaces.add(workspace);
+    }
+    
+    /**
+     * Get Workspaces available from service.
+     */
+    public List getWorkspaces() {
+        return workspaces;
+    }
+    
+    /**
+     * Set Workspaces of service.
+     */
+    public void setWorkspaces(List workspaces) {
+        this.workspaces = workspaces;
+    }
+    
+    /**
+     * Find workspace by title.
+     * @param title Match this title
+     * @return Matching Workspace or null if none found.
+     */
+    public Workspace findWorkspace(String title) {
+        for (Iterator it = workspaces.iterator(); it.hasNext();) {
+            Workspace ws = (Workspace) it.next();
+            if (title.equals(ws.getTitle())) {
+                return ws;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Deserialize an Atom service XML document into an object
+     */
+    public static AtomService documentToService(Document document) throws ProponoException {
+        AtomService service = new AtomService();
+        Element root = document.getRootElement();
+        List spaces = root.getChildren("workspace", ATOM_PROTOCOL);
+        Iterator iter = spaces.iterator();
+        while (iter.hasNext()) {
+            Element e = (Element) iter.next();
+            service.addWorkspace(Workspace.elementToWorkspace(e));
+        }
+        return service;
+    }
+    
+    /**
+     * Serialize an AtomService object into an XML document
+     */
+    public Document serviceToDocument() {
+        AtomService service = this;
+        
+        Document doc = new Document();
+        Element root = new Element("service", ATOM_PROTOCOL);
+        doc.setRootElement(root);
+        Iterator iter = service.getWorkspaces().iterator();
+        while (iter.hasNext()) {
+            Workspace space = (Workspace) iter.next();
+            root.addContent(space.workspaceToElement());
+        }
+        return doc;
+    }
+
+}
+
diff --git a/src/main/java/org/rometools/propono/atom/common/Categories.java b/src/main/java/org/rometools/propono/atom/common/Categories.java
new file mode 100644
index 0000000..c80b70e
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/common/Categories.java
@@ -0,0 +1,155 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+*  contributor license agreements.  The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.  For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/ 
+package org.rometools.propono.atom.common;
+
+import com.sun.syndication.feed.atom.Category;
+import com.sun.syndication.io.impl.Atom10Parser;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.jdom.Element;
+
+
+/** 
+ * Models an Atom protocol Categories element, which may contain ROME Atom 
+ * {@link com.sun.syndication.feed.atom.Category} elements. 
+ */
+public class Categories {
+    private List categories = new ArrayList(); // of Category objects
+    private String baseURI = null;
+    private Element categoriesElement = null;
+    private String href = null;
+    private String scheme = null;
+    private boolean fixed = false;    
+    
+    public Categories() {        
+    }
+    
+    /** Load select from XML element */
+    public Categories(Element e, String baseURI) {
+        this.categoriesElement = e; 
+        this.baseURI = baseURI;
+        parseCategoriesElement(e);
+    }
+    
+    /** Add category list of those specified */
+    public void addCategory(Category cat) {
+        categories.add(cat);
+    }
+    
+    /** 
+     * Iterate over Category objects 
+     * @return List of ROME Atom {@link com.sun.syndication.feed.atom.Category}
+     */
+    public List getCategories() {
+        return categories;
+    }
+
+    /** True if clients MUST use one of the categories specified */
+    public boolean isFixed() {
+        return fixed;
+    }
+
+    /** True if clients MUST use one of the categories specified */
+    public void setFixed(boolean fixed) {
+        this.fixed = fixed;
+    }
+
+    /** Category URI scheme to use for Categories without a scheme */
+    public String getScheme() {
+        return scheme;
+    }
+
+    /** Category URI scheme to use for Categories without a scheme */
+    public void setScheme(String scheme) {
+        this.scheme = scheme;
+    }
+    
+    /** URI of out-of-line categories */
+    public String getHref() {
+        return href;
+    }
+
+    /** URI of out-of-line categories */
+    public void setHref(String href) {
+        this.href = href;
+    }
+
+    /** Get unresolved URI of the collection, or null if impossible to determine */
+    public String getHrefResolved() {
+        if (Atom10Parser.isAbsoluteURI(href)) {
+            return href;        
+        } else if (baseURI != null && categoriesElement != null) {
+            return Atom10Parser.resolveURI(
+                baseURI, categoriesElement, href);
+        }
+        return null;
+    }
+        
+    public Element categoriesToElement() {
+        Categories cats = this;
+        Element catsElem = new Element("categories", AtomService.ATOM_PROTOCOL);
+        catsElem.setAttribute("fixed", cats.isFixed() ? "yes" : "no", AtomService.ATOM_PROTOCOL);
+        if (cats.getScheme() != null) {
+            catsElem.setAttribute("scheme", cats.getScheme(), AtomService.ATOM_PROTOCOL);
+        }
+        if (cats.getHref() != null) {
+            catsElem.setAttribute("href", cats.getHref(), AtomService.ATOM_PROTOCOL);
+        } else {
+            // Loop to create <atom:category> elements
+            for (Iterator catIter = cats.getCategories().iterator(); catIter.hasNext();) {
+                Category cat = (Category) catIter.next();
+                Element catElem = new Element("category", AtomService.ATOM_FORMAT);
+                catElem.setAttribute("term", cat.getTerm(), AtomService.ATOM_FORMAT);
+                if (cat.getScheme() != null) { // optional
+                    catElem.setAttribute("scheme", cat.getScheme(), AtomService.ATOM_FORMAT);
+                }
+                if (cat.getLabel() != null) { // optional
+                    catElem.setAttribute("label", cat.getLabel(), AtomService.ATOM_FORMAT);
+                }
+                catsElem.addContent(catElem);
+            }  
+        }
+        return catsElem;
+    }
+    
+    protected void parseCategoriesElement(Element catsElem) {
+        if (catsElem.getAttribute("href", AtomService.ATOM_PROTOCOL) != null) {
+            setHref(catsElem.getAttribute("href", AtomService.ATOM_PROTOCOL).getValue());
+        }
+        if (catsElem.getAttribute("fixed", AtomService.ATOM_PROTOCOL) != null) { 
+            if ("yes".equals(catsElem.getAttribute("fixed", AtomService.ATOM_PROTOCOL).getValue())) {
+               setFixed(true);
+            }
+        }
+        if (catsElem.getAttribute("scheme", AtomService.ATOM_PROTOCOL) != null) { 
+            setScheme(catsElem.getAttribute("scheme", AtomService.ATOM_PROTOCOL).getValue());
+        }
+        // Loop to parse <atom:category> elemenents to Category objects
+        List catElems = catsElem.getChildren("category", AtomService.ATOM_FORMAT);
+        for (Iterator catIter = catElems.iterator(); catIter.hasNext();) {                
+            Element catElem = (Element) catIter.next();
+            Category cat = new Category();
+            cat.setTerm(catElem.getAttributeValue("term", AtomService.ATOM_FORMAT));                
+            cat.setLabel(catElem.getAttributeValue("label", AtomService.ATOM_FORMAT)); 
+            cat.setScheme(catElem.getAttributeValue("scheme", AtomService.ATOM_FORMAT));
+            addCategory(cat);
+        }
+    }
+}
+
diff --git a/src/main/java/org/rometools/propono/atom/common/Collection.java b/src/main/java/org/rometools/propono/atom/common/Collection.java
new file mode 100644
index 0000000..1f9ef94
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/common/Collection.java
@@ -0,0 +1,252 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+*  contributor license agreements.  The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.  For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/ 
+package org.rometools.propono.atom.common;
+
+import com.sun.syndication.io.impl.Atom10Parser;
+import org.rometools.propono.utils.ProponoException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.jdom.Element;
+
+
+/**
+ * Models an Atom workspace collection.    
+ */
+public class Collection {
+    
+    public static final String ENTRY_TYPE = "application/atom+xml;type=entry";
+    
+    private Element collectionElement = null;
+    private String baseURI = null;
+    private String title = null;
+    private String titleType = null; // may be TEXT, HTML, XHTML
+    private List accepts = new ArrayList(); // of Strings
+    private String listTemplate = null;
+    private String href = null;
+    private List categories = new ArrayList(); // of Categories objects    
+    
+    /**
+     * Collection MUST have title and href.
+     * @param title    Title for collection
+     * @param titleType Content type of title (null for plain text)
+     * @param href     Collection URI.
+     */
+    public Collection(String title, String titleType, String href) {
+        this.title = title;
+        this.titleType = titleType;
+        this.href = href;
+    }
+
+    /** Load self from XML element */
+    public Collection(Element e) throws ProponoException {
+        this.collectionElement = e;
+        this.parseCollectionElement(e); 
+    }
+    
+    /** Load self from XML element and base URI for resolving relative URIs */
+    public Collection(Element e, String baseURI) throws ProponoException {
+        this.collectionElement = e;
+        this.baseURI = baseURI;
+        this.parseCollectionElement(e); 
+    }
+    
+    /**
+     * List of content-type ranges accepted by collection.
+     */
+    public List getAccepts() {
+        return accepts;
+    }
+    
+    public void addAccept(String accept) {
+        this.accepts.add(accept);
+    }
+    
+    public void setAccepts(List accepts) {
+        this.accepts = accepts;
+    }
+    
+    /** The URI of the collection */
+    public String getHref() {
+        return href;
+    }
+    
+    /**
+     * Set URI of collection
+     */
+    public void setHref(String href) {
+        this.href = href;
+    }
+    
+    /** Get resolved URI of the collection, or null if impossible to determine */
+    public String getHrefResolved() {
+        if (Atom10Parser.isAbsoluteURI(href)) {
+            return href;        
+        } else if (baseURI != null && collectionElement != null) {
+            int lastslash = baseURI.lastIndexOf("/");
+            return Atom10Parser.resolveURI(baseURI.substring(0, lastslash), collectionElement, href);
+        }
+        return null;
+    }
+        
+    /** Get resolved URI using collection's baseURI, or null if impossible to determine */
+    public String getHrefResolved(String relativeUri) {
+        if (Atom10Parser.isAbsoluteURI(relativeUri)) {
+            return relativeUri;        
+        } else if (baseURI != null && collectionElement != null) {
+            int lastslash = baseURI.lastIndexOf("/");
+            return Atom10Parser.resolveURI(baseURI.substring(0, lastslash), collectionElement, relativeUri);
+        }
+        return null;
+    }
+        
+    /** Must have human readable title */
+    public String getTitle() {
+        return title;
+    }
+    
+    /**
+     * Set title of collection.
+     */
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    /**
+     * Type of title ("text", "html" or "xhtml")
+     */
+    public String getTitleType() {
+        return titleType;
+    }
+
+    /**
+     * Type of title ("text", "html" or "xhtml")
+     */
+    public void setTitleType(String titleType) {
+        this.titleType = titleType;
+    }
+
+    /** Workspace can have multiple Categories objects */
+    public void addCategories(Categories cats) {
+        categories.add(cats);
+    }
+    
+    /**
+     * Get categories allowed by collection.
+     * @return Collection of {@link com.sun.syndication.propono.atom.common.Categories} objects.
+     */
+    public List getCategories() {
+        return categories;
+    }
+    
+    /**
+     * Returns true if contentType is accepted by collection.
+     */
+    public boolean accepts(String ct) {
+        for (Iterator it = accepts.iterator(); it.hasNext();) {
+            String accept = (String)it.next();
+            if (accept != null && accept.trim().equals("*/*")) return true;
+            String entryType = "application/atom+xml";
+            boolean entry = entryType.equals(ct);
+            if (entry && null == accept) {
+                return true;
+            } else if (entry && "entry".equals(accept)) {
+                return true;
+            } else if (entry && entryType.equals(accept)) {
+                return true;
+            } else {
+                String[] rules = (String[])accepts.toArray(new String[accepts.size()]);
+                for (int i=0; i<rules.length; i++) {
+                    String rule = rules[i].trim();
+                    if (rule.equals(ct)) return true;
+                    int slashstar = rule.indexOf("/*");
+                    if (slashstar > 0) {
+                        rule = rule.substring(0, slashstar + 1);
+                        if (ct.startsWith(rule)) return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * Serialize an AtomService.Collection into an XML element
+     */
+    public Element collectionToElement() {
+        Collection collection = this;
+        Element element = new Element("collection", AtomService.ATOM_PROTOCOL);
+        element.setAttribute("href", collection.getHref());
+                       
+        Element titleElem = new Element("title", AtomService.ATOM_FORMAT);
+        titleElem.setText(collection.getTitle());
+        if (collection.getTitleType() != null && !collection.getTitleType().equals("TEXT")) {
+            titleElem.setAttribute("type", collection.getTitleType(), AtomService.ATOM_FORMAT);
+        }
+        element.addContent(titleElem);
+        
+        // Loop to create <app:categories> elements            
+        for (Iterator it = collection.getCategories().iterator(); it.hasNext();) {
+            Categories cats = (Categories)it.next();
+            element.addContent(cats.categoriesToElement());          
+        }
+        
+        for (Iterator it = collection.getAccepts().iterator(); it.hasNext();) {
+            String range = (String)it.next();
+            Element acceptElem = new Element("accept", AtomService.ATOM_PROTOCOL);
+            acceptElem.setText(range);
+            element.addContent(acceptElem);
+        }
+        
+        return element;
+    }
+    
+    /** Deserialize an Atom service collection XML element into an object */
+    public Collection elementToCollection(Element element) throws ProponoException {
+        return new Collection(element);
+    }
+    
+    protected void parseCollectionElement(Element element) throws ProponoException {
+        setHref(element.getAttribute("href").getValue());
+        
+        Element titleElem = element.getChild("title", AtomService.ATOM_FORMAT);
+        if (titleElem != null) {
+            setTitle(titleElem.getText());
+            if (titleElem.getAttribute("type", AtomService.ATOM_FORMAT) != null) {
+                setTitleType(titleElem.getAttribute("type", AtomService.ATOM_FORMAT).getValue());
+            }
+        }
+        
+        List acceptElems = element.getChildren("accept",  AtomService.ATOM_PROTOCOL);
+        if (acceptElems != null && acceptElems.size() > 0) {
+            for (Iterator it = acceptElems.iterator(); it.hasNext();) {
+                Element acceptElem = (Element)it.next();
+                addAccept(acceptElem.getTextTrim());
+            }
+        }
+        
+        // Loop to parse <app:categories> element to Categories objects
+        List catsElems = element.getChildren("categories", AtomService.ATOM_PROTOCOL);
+        for (Iterator catsIter = catsElems.iterator(); catsIter.hasNext();) {
+            Element catsElem = (Element) catsIter.next();  
+            Categories cats = new Categories(catsElem, baseURI);
+            addCategories(cats);
+        }
+    } 
+}
+
diff --git a/src/main/java/org/rometools/propono/atom/common/Workspace.java b/src/main/java/org/rometools/propono/atom/common/Workspace.java
new file mode 100644
index 0000000..90d1830
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/common/Workspace.java
@@ -0,0 +1,147 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+*  contributor license agreements.  The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.  For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/ 
+package org.rometools.propono.atom.common;
+
+import org.rometools.propono.utils.ProponoException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.jdom.Element;
+
+
+/**
+ * Models an Atom workspace.
+ */
+public class Workspace {
+    private String title = null;
+    private String titleType = null; // may be TEXT, HTML, XHTML
+    private List collections = new ArrayList(); 
+    
+    /**
+     * Collection MUST have title.
+     * @param title    Title for collection
+     * @param titleType Content type of title (null for plain text)
+     */
+    public Workspace(String title, String titleType) {
+        this.title = title;
+        this.titleType = titleType;
+    }
+    
+    public Workspace(Element elem) throws ProponoException {
+        parseWorkspaceElement(elem);
+    }
+    
+    /** Iterate over collections in workspace */
+    public List getCollections() {
+        return collections;
+    }
+    
+    /** Add new collection to workspace */
+    public void addCollection(Collection col) {
+        collections.add(col);
+    }
+    
+    /**
+     * DefaultWorkspace must have a human readable title
+     */
+    public String getTitle() {
+        return title;
+    }
+    
+    /**
+     * Set title of workspace.
+     */
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    /**
+     * Get title type ("text", "html", "xhtml" or MIME content-type)
+     */
+    public String getTitleType() {
+        return titleType;
+    }
+
+    /**
+     * Set title type ("text", "html", "xhtml" or MIME content-type)
+     */
+    public void setTitleType(String titleType) {
+        this.titleType = titleType;
+    }
+    
+    /**
+     * Find collection by title and/or content-type.
+     * @param title Title or null to match all titles.
+     * @param contentType Content-type or null to match all content-types.
+     * @return First Collection that matches title and/or content-type.
+     */
+    public Collection findCollection(String title, String contentType) {
+        for (Iterator it = collections.iterator(); it.hasNext();) {
+            Collection col = (Collection) it.next();
+            if (title != null && col.accepts(contentType)) {
+                return col;
+            } else if (col.accepts(contentType)) {
+                return col;
+            }
+        }
+        return null;
+    }
+        
+    /** Deserialize a Atom workspace XML element into an object */
+    public static Workspace elementToWorkspace(Element element) throws ProponoException {  
+        return new Workspace(element);
+    }
+    
+    /**
+     * Serialize an AtomService.DefaultWorkspace object into an XML element
+     */
+    public Element workspaceToElement() {
+        Workspace space = this;
+        
+        Element element = new Element("workspace", AtomService.ATOM_PROTOCOL);
+        
+        Element titleElem = new Element("title", AtomService.ATOM_FORMAT);
+        titleElem.setText(space.getTitle());
+        if (space.getTitleType() != null && !space.getTitleType().equals("TEXT")) {
+            titleElem.setAttribute("type", space.getTitleType(), AtomService.ATOM_FORMAT);
+        }
+        element.addContent(titleElem);
+        
+        Iterator iter = space.getCollections().iterator();
+        while (iter.hasNext()) {
+            Collection col = (Collection) iter.next();
+            element.addContent(col.collectionToElement());
+        }
+        return element;
+    }
+
+    /** Deserialize a Atom workspace XML element into an object */
+    protected void parseWorkspaceElement(Element element) throws ProponoException {       
+        Element titleElem = element.getChild("title", AtomService.ATOM_FORMAT);
+        setTitle(titleElem.getText());
+        if (titleElem.getAttribute("type", AtomService.ATOM_FORMAT) != null) {
+            setTitleType(titleElem.getAttribute("type", AtomService.ATOM_FORMAT).getValue());
+        }       
+        List collections = element.getChildren("collection", AtomService.ATOM_PROTOCOL);
+        Iterator iter = collections.iterator();
+        while (iter.hasNext()) {
+            Element e = (Element) iter.next();
+            addCollection(new Collection(e));
+        }
+    }
+}
diff --git a/src/main/java/org/rometools/propono/atom/common/rome/AppModule.java b/src/main/java/org/rometools/propono/atom/common/rome/AppModule.java
new file mode 100644
index 0000000..ca95f52
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/common/rome/AppModule.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ * 
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.  For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package org.rometools.propono.atom.common.rome;
+
+import com.sun.syndication.feed.module.Module;
+import java.util.Date;
+
+/**
+ * ROME Extension Module to Atom protocol extensions to Atom format.
+ */
+public interface AppModule extends Module {
+    public static final String URI = "http://www.w3.org/2007/app";
+    
+    /** True if entry is a draft */
+    public Boolean getDraft();
+    
+    /** Set to true if entry is a draft */
+    public void setDraft(Boolean draft);
+    
+    /** Time of last edit */
+    public Date getEdited();
+    
+    /** Set time of last edit */
+    public void setEdited(Date edited);
+}
diff --git a/src/main/java/org/rometools/propono/atom/common/rome/AppModuleGenerator.java b/src/main/java/org/rometools/propono/atom/common/rome/AppModuleGenerator.java
new file mode 100644
index 0000000..98613ea
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/common/rome/AppModuleGenerator.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ * 
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.  For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package org.rometools.propono.atom.common.rome;
+
+import com.sun.syndication.io.impl.DateParser;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.jdom.Element;
+import org.jdom.Namespace;
+
+import com.sun.syndication.feed.module.Module;
+import com.sun.syndication.io.ModuleGenerator;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Creates JDOM representation for APP Extension Module.
+ */
+public class AppModuleGenerator implements ModuleGenerator {
+    private static final Namespace APP_NS  = 
+        Namespace.getNamespace("app", AppModule.URI);
+
+    public String getNamespaceUri() {
+        return AppModule.URI;
+    }
+
+    private static final Set NAMESPACES;
+
+    static {
+        Set nss = new HashSet();
+        nss.add(APP_NS);
+        NAMESPACES = Collections.unmodifiableSet(nss);
+    }
+    
+    /** Get namespaces associated with this module */
+    public Set getNamespaces() {
+        return NAMESPACES;
+    }
+
+    /** Generate JDOM element for module and add it to parent element */
+    public void generate(Module module, Element parent) {
+        AppModule m = (AppModule)module;
+        
+        if (m.getDraft() != null) {
+            String draft = m.getDraft().booleanValue() ? "yes" : "no";
+            Element control = new Element("control", APP_NS);
+            control.addContent(generateSimpleElement("draft", draft));
+            parent.addContent(control);
+        }
+        if (m.getEdited() != null) {
+            Element edited = new Element("edited", APP_NS);
+            // Inclulde millis in date/time
+            SimpleDateFormat dateFormater = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+            dateFormater.setTimeZone(TimeZone.getTimeZone("GMT"));
+            edited.addContent(dateFormater.format(m.getEdited()));
+            parent.addContent(edited);
+        }
+    }
+
+    private Element generateSimpleElement(String name, String value)  {
+        Element element = new Element(name, APP_NS);
+        element.addContent(value);
+        return element;
+    }
+}
diff --git a/src/main/java/org/rometools/propono/atom/common/rome/AppModuleImpl.java b/src/main/java/org/rometools/propono/atom/common/rome/AppModuleImpl.java
new file mode 100644
index 0000000..84952b0
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/common/rome/AppModuleImpl.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ * Copyright 2011 The ROME Teams
+ * 
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.  For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package org.rometools.propono.atom.common.rome;
+
+import com.sun.syndication.feed.CopyFrom;
+import com.sun.syndication.feed.module.ModuleImpl;
+import java.util.Date;
+
+/**
+ * Bean representation of APP module.
+ */
+public class AppModuleImpl extends ModuleImpl implements AppModule {
+    private boolean draft = false;
+    private Date edited = null;
+    
+    public AppModuleImpl() {
+        super(AppModule.class, AppModule.URI);
+    }
+    
+    /** True if entry is draft */
+    public Boolean getDraft() {
+        return draft ? Boolean.TRUE : Boolean.FALSE;
+    }
+    
+    /** Set to true if entry is draft */
+    public void setDraft(Boolean draft) {
+        this.draft = draft.booleanValue();
+    }
+    
+    /** Time of last edit */
+    public Date getEdited() {
+        return edited;
+    }
+
+    /** Set time of last edit */
+    public void setEdited(Date edited) {
+        this.edited = edited;
+    }
+    
+    /** Get interface class of module */
+    public Class getInterface() {
+        return AppModule.class;
+    }
+    
+    /** Copy from other module */
+    public void copyFrom(CopyFrom obj) {
+        AppModule m = (AppModule)obj;
+        setDraft(m.getDraft());
+        setEdited(m.getEdited());
+    }
+}
diff --git a/src/main/java/org/rometools/propono/atom/common/rome/AppModuleParser.java b/src/main/java/org/rometools/propono/atom/common/rome/AppModuleParser.java
new file mode 100644
index 0000000..b71f925
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/common/rome/AppModuleParser.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ * 
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.  For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package org.rometools.propono.atom.common.rome;
+
+import com.sun.syndication.io.impl.DateParser;
+import org.jdom.Element;
+import org.jdom.Namespace;
+
+import com.sun.syndication.feed.module.Module;
+import com.sun.syndication.io.ModuleParser;
+
+/**
+ * Parses APP module information from a JDOM element and into 
+ * <code>AppModule</code> form.
+ */
+public class AppModuleParser implements ModuleParser {
+
+    /** Get URI of module namespace */
+    public String getNamespaceUri() {
+        return AppModule.URI;
+    }
+
+    /** Get namespace of module */
+    public Namespace getContentNamespace() {
+        return Namespace.getNamespace(AppModule.URI);
+    }
+    
+    /** Parse JDOM element into module */
+    public Module parse(Element elem) {
+        boolean foundSomething = false;
+        AppModule m = new AppModuleImpl();
+        Element control = elem.getChild("control", getContentNamespace());
+        if (control != null) {
+            Element draftElem = control.getChild("draft", getContentNamespace());
+            if (draftElem != null) {
+                if ("yes".equals(draftElem.getText())) m.setDraft(Boolean.TRUE); 
+                if ("no".equals(draftElem.getText())) m.setDraft(Boolean.FALSE);                
+            }
+        }
+        Element edited = elem.getChild("edited", getContentNamespace());
+        if (edited != null) {
+            try {
+                m.setEdited(DateParser.parseW3CDateTime(edited.getTextTrim()));
+            } catch (Exception ignored) {}
+        }
+        return m;
+    }
+}
+
diff --git a/src/main/java/org/rometools/propono/atom/server/AtomException.java b/src/main/java/org/rometools/propono/atom/server/AtomException.java
new file mode 100644
index 0000000..7b88a1f
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/AtomException.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ * 
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.  For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package org.rometools.propono.atom.server;
+
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * Exception thrown by {@link com.sun.syndication.propono.atom.server.AtomHandler} 
+ * and extended by other Propono Atom exception classes.
+ */
+public class AtomException extends Exception {
+    /** Construct new exception */
+    public AtomException() {
+        super();
+    }
+    /** Construct new exception with message */
+    public AtomException(String msg) {
+        super(msg);
+    }
+    /** Contruct new exception with message and wrapping existing exception */
+    public AtomException(String msg, Throwable t) {
+        super(msg, t);
+    }
+    /** Construct new exception to wrap existing one. */
+    public AtomException(Throwable t) {
+        super(t);
+    }
+    /* Get HTTP status code associated with exception (HTTP 500 server error) */
+    /**
+     * Get HTTP status associated with exception.
+     */
+    public int getStatus() {
+        return HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+    }
+}
diff --git a/src/main/java/org/rometools/propono/atom/server/AtomHandler.java b/src/main/java/org/rometools/propono/atom/server/AtomHandler.java
new file mode 100644
index 0000000..9480b32
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/AtomHandler.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ * 
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.  For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package org.rometools.propono.atom.server;
+
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.feed.atom.Feed;
+import org.rometools.propono.atom.common.AtomService;
+import org.rometools.propono.atom.common.Categories;
+
+/**
+ * Interface for handling single Atom protocol requests.
+ *
+ * <p>To create your own Atom protocol implementation you must implement this 
+ * interface and create a concrete sub-class of 
+ * {@link com.sun.syndication.propono.atom.server.AtomHandlerFactory}
+ * which is capable of instantiating it.</p>
+ */
+public interface AtomHandler
+{
+    /** 
+     * Get username of authenticated user. Return the username of the
+     * authenticated user 
+     */
+    public String getAuthenticatedUsername();
+
+    /**
+     * Return 
+     * {@link com.sun.syndication.propono.atom.common.AtomService} 
+     * object that contains the 
+     * {@link com.sun.syndication.propono.atom.common.Workspace} objects 
+     * available to the currently authenticated user and within those the 
+     * {@link com.sun.syndication.propono.atom.common.Collection} avalaible.
+     */
+    public AtomService getAtomService(AtomRequest req) throws AtomException;
+    
+    /**
+     * Get categories, a list of Categories objects
+     */
+    public Categories getCategories(AtomRequest req) throws AtomException;
+
+    /**
+     * Return collection or portion of collection specified by request.
+     * @param req Details of HTTP request
+     */
+    public Feed getCollection(AtomRequest req) throws AtomException;
+
+    /**
+     * Store new entry in collection specified by request and return 
+     * representation of entry as it is stored on server.
+     * @param req Details of HTTP request
+     * @return Location URL of new entry
+     */
+    public Entry postEntry(AtomRequest req, Entry entry) throws AtomException;
+
+    /**
+     * Get entry specified by request.
+     * @param req Details of HTTP request
+     */
+    public Entry getEntry(AtomRequest req) throws AtomException;
+
+    /**
+     * Get media resource specified by request.
+     * @param req Details of HTTP request
+     */
+    public AtomMediaResource getMediaResource(AtomRequest req) throws AtomException;
+
+    /**
+     * Update entry specified by request and return new entry as represented
+     * on the server.
+     * @param req Details of HTTP request
+     */
+    public void putEntry(AtomRequest req, Entry entry) throws AtomException;
+
+
+    /**
+     * Delete entry specified by request.
+     * @param req Details of HTTP request
+     */
+    public void deleteEntry(AtomRequest req) throws AtomException;
+
+    /**
+     * Store media data in collection specified by request, create an Atom
+     * media-link entry to store metadata for the new media file and return 
+     * that entry to the caller.
+     * @param req Details of HTTP request
+     * @param entry    New entry initialzied with only title and content type
+     * @return Location URL of new media entry
+     */
+    public Entry postMedia(AtomRequest req, Entry entry) throws AtomException;
+
+    /**
+     * Update the media file part of a media-link entry.
+     * @param req Details of HTTP request
+     */
+    public void putMedia(AtomRequest req) throws AtomException;
+
+    /**
+     * Return true if specified request represents URI of a Service Document.
+     * @param req Details of HTTP request
+     */
+    public boolean isAtomServiceURI(AtomRequest req);
+
+    /**
+     * Return true if specified request represents URI of a Categories Document.
+     * @param req Details of HTTP request
+     */
+    public boolean isCategoriesURI(AtomRequest req);
+
+    /**
+     * Return true if specified request represents URI of a collection.
+     * @param req Details of HTTP request
+     */
+    public boolean isCollectionURI(AtomRequest req);
+
+    /**
+     * Return true if specified request represents URI of an Atom entry.
+     * @param req Details of HTTP request
+     */
+    public boolean isEntryURI(AtomRequest req);
+
+    /**
+     * Return true if specified patrequesthinfo represents media-edit URI.
+     * @param req Details of HTTP request
+     */
+    public boolean isMediaEditURI(AtomRequest req);
+}
+
diff --git a/src/main/java/org/rometools/propono/atom/server/AtomHandlerFactory.java b/src/main/java/org/rometools/propono/atom/server/AtomHandlerFactory.java
new file mode 100644
index 0000000..8a53639
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/AtomHandlerFactory.java
@@ -0,0 +1,113 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.server;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Defines a factory that enables the 
+ * {@link com.sun.syndication.propono.atom.server.AtomServlet} to obtain an 
+ * {@link com.sun.syndication.propono.atom.server.AtomHandler} that handles an Atom request.
+ *
+ * <p>To create your own Atom protocol implementation you must sub-class this
+ * class with your own factory that is capable of creating instances of your
+ * {@link com.sun.syndication.propono.atom.server.AtomHandler} impementation.</p>
+ */
+public abstract class AtomHandlerFactory {
+
+    private static Log log =
+            LogFactory.getFactory().getInstance(AtomHandlerFactory.class);
+    
+    private static final String DEFAULT_PROPERTY_NAME = 
+        "com.sun.syndication.propono.atom.server.AtomHandlerFactory";
+    
+    private static final String FALLBACK_IMPL_NAME =
+         "com.sun.syndication.propono.atom.server.impl.FileBasedAtomHandlerFactory";
+            
+    /*
+     * <p>Protected constructor to prevent instantiation.
+     * Use {@link #newInstance()}.</p>
+     */
+    protected AtomHandlerFactory() {
+    }
+
+    /**
+     * Obtain a new instance of a <code>AtomHandlerFactory</code>. This static 
+     * method creates a new factory instance. This method uses the following 
+     * ordered lookup procedure to determine the <code>AtomHandlerFactory</code> 
+     * implementation class to load:
+     * <ul>
+     *    <li>
+     *       Use the <code>com.sun.syndication.propono.atom.server.AtomHandlerFactory</code> 
+     *       system property.
+     *    </li>
+     *    <li>
+     *       Use the properties file "/propono.properties" in the classpath. 
+     *       This configuration file is in standard <code>java.util.Properties</code> 
+     *       format and contains the fully qualified name of the implementation 
+     *       class with the key being the system property defined above.
+     *
+     *       The propono.properties file is read only once by Propono and it's 
+     *       values are then cached for future use.  If the file does not exist
+     *       when the first attempt is made to read from it, no further attempts 
+     *       are made to check for its existence.  It is not possible to change 
+     *       the value of any property in propono.properties after it has been 
+     *       read for the first time.
+     *    </li>
+     *    <li>
+     *       If not available, to determine the classname. The Services API will look
+     *       for a classname in the file: 
+     *       <code>META-INF/services/com.sun.syndication.AtomHandlerFactory</code>
+     *       in jars available to the runtime.
+     *    </li>
+     *    <li>
+     *       Platform default <code>AtomHandlerFactory</code> instance.
+     *    </li>
+     * </ul>
+     *
+     * Once an application has obtained a reference to a <code>AtomHandlerFactory</code> 
+     * it can use the factory to configure and obtain parser instances.
+     *
+     * @return New instance of a <code>AtomHandlerFactory</code>
+     *
+     * @throws FactoryConfigurationError if the implementation is not available 
+     * or cannot be instantiated.
+     */
+    public static AtomHandlerFactory newInstance() {
+        try {
+            return (AtomHandlerFactory)
+                FactoryFinder.find(DEFAULT_PROPERTY_NAME, FALLBACK_IMPL_NAME);
+        } catch (FactoryFinder.ConfigurationError e) {
+            log.error("ERROR: finding factory", e);
+            throw new FactoryConfigurationError(e.getException(), e.getMessage());
+        }
+    }
+
+    /**
+     * Creates a new instance of a {@link com.sun.syndication.propono.atom.server.AtomHandler}
+     * using the currently configured parameters.
+     *
+     * @return A new instance of a AtomHandler.
+     *
+     * @throws AtomConfigurationException if a AtomHandler cannot be created 
+     * which satisfies the configuration requested.
+     */
+    public abstract AtomHandler newAtomHandler(
+            HttpServletRequest req, HttpServletResponse res);
+}
diff --git a/src/main/java/org/rometools/propono/atom/server/AtomMediaResource.java b/src/main/java/org/rometools/propono/atom/server/AtomMediaResource.java
new file mode 100644
index 0000000..41d1cf3
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/AtomMediaResource.java
@@ -0,0 +1,95 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.server;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.Date;
+import javax.activation.FileTypeMap;
+import javax.activation.MimetypesFileTypeMap;
+
+/**
+ * Represents a media link entry.
+ */
+public class AtomMediaResource {
+    private String      contentType = null;
+    private long        contentLength = 0;
+    private InputStream inputStream = null;
+    private Date        lastModified = null;
+    private static FileTypeMap map = null;
+    
+    static {
+         // TODO: figure out why PNG is missing from Java MIME types
+        map = FileTypeMap.getDefaultFileTypeMap();
+        if (map instanceof MimetypesFileTypeMap) {
+            try {
+                ((MimetypesFileTypeMap)map).addMimeTypes("image/png png PNG");
+            } catch (Exception ignored) {}
+        }       
+    }
+    
+    public AtomMediaResource(File resource) throws FileNotFoundException {
+        contentType = map.getContentType(resource.getName());
+        contentLength = resource.length();
+        lastModified = new Date(resource.lastModified());
+        inputStream = new FileInputStream(resource);
+    }
+    
+    public AtomMediaResource(String name, long length, Date lastModified, InputStream is) 
+            throws FileNotFoundException {
+        this.contentType = map.getContentType(name);
+        this.contentLength = length;
+        this.lastModified = lastModified; 
+        this.inputStream = is;
+    }
+    
+    public String getContentType() {
+        return contentType;
+    }
+
+    public void setContentType(String contentType) {
+        this.contentType = contentType;
+    }
+
+    public long getContentLength() {
+        return contentLength;
+    }
+
+    public void setContentLength(long contentLength) {
+        this.contentLength = contentLength;
+    }
+
+    public InputStream getInputStream() {
+        return inputStream;
+    }
+
+    public void setInputStream(InputStream inputStream) {
+        this.inputStream = inputStream;
+    }
+
+    public Date getLastModified() {
+        return lastModified;
+    }
+
+    public void setLastModified(Date lastModified) {
+        this.lastModified = lastModified;
+    }
+    
+
+}
diff --git a/src/main/java/org/rometools/propono/atom/server/AtomNotAuthorizedException.java b/src/main/java/org/rometools/propono/atom/server/AtomNotAuthorizedException.java
new file mode 100644
index 0000000..c5ce57a
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/AtomNotAuthorizedException.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ * 
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.  For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package org.rometools.propono.atom.server;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Exception to be thrown by <code>AtomHandler</code> implementations in the 
+ * case that a user is not authorized to access a resource.
+ */
+public class AtomNotAuthorizedException extends AtomException {
+    /** Construct new exception */
+    public AtomNotAuthorizedException() {
+        super();
+    }
+    /** Construct new exception with message */
+    public AtomNotAuthorizedException(String msg) {
+        super(msg);
+    }
+    /** Construct new exception with message and root cause */
+    public AtomNotAuthorizedException(String msg, Throwable t) {
+        super(msg, t);
+    }
+    /** Construct new exception to wrap root cause*/
+    public AtomNotAuthorizedException(Throwable t) {
+        super(t);
+    }
+    /** Get HTTP status code of exception (HTTP 403 unauthorized) */
+    public int getStatus() {
+        return HttpServletResponse.SC_UNAUTHORIZED;
+    }
+}
diff --git a/src/main/java/org/rometools/propono/atom/server/AtomNotFoundException.java b/src/main/java/org/rometools/propono/atom/server/AtomNotFoundException.java
new file mode 100644
index 0000000..ec6f779
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/AtomNotFoundException.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ * 
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.  For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package org.rometools.propono.atom.server;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Exception thrown by AtomHandler in that case a resource is not found.
+ */
+public class AtomNotFoundException extends AtomException {
+    /** Construct new exception */
+    public AtomNotFoundException() {
+        super();
+    }
+    /** Construct new exception with message */
+    public AtomNotFoundException(String msg) {
+        super(msg);
+    }
+    /** Construct new exception with message and root cause */
+    public AtomNotFoundException(String msg, Throwable t) {
+        super(msg, t);
+    }
+    /** Construct new exception with root cause */
+    public AtomNotFoundException(Throwable t) {
+        super(t);
+    }
+    /** Get HTTP status code associated with exception (404 not found) */
+    public int getStatus() {
+        return HttpServletResponse.SC_NOT_FOUND;
+    }
+}
diff --git a/src/main/java/org/rometools/propono/atom/server/AtomRequest.java b/src/main/java/org/rometools/propono/atom/server/AtomRequest.java
new file mode 100644
index 0000000..1610684
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/AtomRequest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.  For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package org.rometools.propono.atom.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.Principal;
+import java.util.Enumeration;
+import java.util.Map;
+
+/**
+ * Represents HTTP request to be processed by AtomHandler.
+ */
+public interface AtomRequest {
+
+    /** 
+     * Returns any extra path information associated with the URL the client 
+     * sent when it made this request. 
+     */
+    public String getPathInfo();
+
+    /** 
+     * Returns the query string that is contained in the request URL after 
+     * the path. 
+     */
+    public String getQueryString();
+
+    /** 
+     * Returns the login of the user making this request, if the user has 
+     * been authenticated, or null if the user has not been authenticated. 
+     */
+    public String getRemoteUser();
+
+    /** 
+     * Returns a boolean indicating whether the authenticated user is included
+     * in the specified logical "role". 
+     */
+    public boolean isUserInRole(String arg0);
+
+    /** 
+     * Returns a java.security.Principal object containing the name of the 
+     * current authenticated user. 
+     */
+    public Principal getUserPrincipal();
+
+    /** 
+     * Returns the part of this request's URL from the protocol name up to the 
+     * query string in the first line of the HTTP request. 
+     */
+    public String getRequestURI();
+
+    /** 
+     * Reconstructs the URL the client used to make the request. 
+     */
+    public StringBuffer getRequestURL();
+
+    /** 
+     * Returns the length, in bytes, of the request body and made available by 
+     * the input stream, or -1 if the length is not known. 
+     */
+    public int getContentLength();
+
+    /** 
+     * Returns the MIME type of the body of the request, or null if the type 
+     * is not known. */
+    public String getContentType();
+
+    /** 
+     * Returns the value of a request parameter as a String, or null if the 
+     * parameter does not exist. 
+     */
+    public String getParameter(String arg0);
+
+    /** 
+     * Returns an Enumeration of String  objects containing the names of the 
+     * parameters contained in this request. 
+     */
+    public Enumeration getParameterNames();
+
+    /** 
+     * Returns an array of String objects containing all of the values the 
+     * given request parameter has, or null if the parameter does not exist. 
+     */
+    public String[] getParameterValues(String arg0);
+
+    /** 
+     * Returns a java.util.Map of the parameters of this request. 
+     */
+    public Map getParameterMap();
+
+    /** 
+     * Retrieves the body of the request as binary data using a 
+     * ServletInputStream. 
+     */
+    public InputStream getInputStream() throws IOException;
+
+    /** 
+     * Returns the value of the specified request header as a long value that 
+     * represents a Date object. */
+    public long getDateHeader(String arg0);
+
+    /** 
+     * Returns the value of the specified request header as a String. 
+     */
+    public String getHeader(String arg0);
+
+    /** 
+     * Returns all the values of the specified request header as an Enumeration 
+     * of String objects. 
+     */
+    public Enumeration getHeaders(String arg0);
+
+    /** 
+     * Returns an enumeration of all the header names this request contains. 
+     */
+    public Enumeration getHeaderNames();
+
+    /** 
+     * Returns the value of the specified request header as an int. 
+     */
+    public int getIntHeader(String arg0);
+}
diff --git a/src/main/java/org/rometools/propono/atom/server/AtomRequestImpl.java b/src/main/java/org/rometools/propono/atom/server/AtomRequestImpl.java
new file mode 100644
index 0000000..0efcdf9
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/AtomRequestImpl.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.  For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package org.rometools.propono.atom.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.Principal;
+import java.util.Enumeration;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Default request implementation.
+ */
+public class AtomRequestImpl implements AtomRequest {
+    private HttpServletRequest wrapped = null;
+            
+    public AtomRequestImpl(HttpServletRequest wrapped) {
+        this.wrapped = wrapped;
+    }
+
+    public String getPathInfo() {
+        return wrapped.getPathInfo() != null ? wrapped.getPathInfo() : "";
+    }
+
+    public String getQueryString() {
+        return wrapped.getQueryString();
+    }
+
+    public String getRemoteUser() {
+        return wrapped.getRemoteUser();
+    }
+
+    public boolean isUserInRole(String arg0) {
+        return wrapped.isUserInRole(arg0);
+    }
+
+    public Principal getUserPrincipal() {
+        return wrapped.getUserPrincipal();
+    }
+
+    public String getRequestURI() {
+        return wrapped.getRequestURI();
+    }
+
+    public StringBuffer getRequestURL() {
+        return wrapped.getRequestURL();
+    }
+
+    public int getContentLength() {
+        return wrapped.getContentLength();
+    }
+
+    public String getContentType() {
+        return wrapped.getContentType();
+    }
+
+    public String getParameter(String arg0) {
+        return wrapped.getParameter(arg0);
+    }
+
+    public Enumeration getParameterNames() {
+        return wrapped.getParameterNames();
+    }
+
+    public String[] getParameterValues(String arg0) {
+        return wrapped.getParameterValues(arg0);
+    }
+
+    public Map getParameterMap() {
+        return wrapped.getParameterMap();
+    }
+    
+    public InputStream getInputStream() throws IOException { 
+        return wrapped.getInputStream();
+    }    
+
+    public long getDateHeader(String arg0) {
+        return wrapped.getDateHeader(arg0);
+    }
+
+    public String getHeader(String arg0) {
+        return wrapped.getHeader(arg0);
+    }
+
+    public Enumeration getHeaders(String arg0) {
+        return wrapped.getHeaders(arg0);
+    }
+
+    public Enumeration getHeaderNames() {
+        return wrapped.getHeaderNames();
+    }
+
+    public int getIntHeader(String arg0) {
+        return wrapped.getIntHeader(arg0);
+    }
+}
diff --git a/src/main/java/org/rometools/propono/atom/server/AtomServlet.java b/src/main/java/org/rometools/propono/atom/server/AtomServlet.java
new file mode 100644
index 0000000..51c4d2e
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/AtomServlet.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ * 
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.  For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package org.rometools.propono.atom.server;
+
+import com.sun.syndication.feed.atom.Content;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Writer;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jdom.Document;
+import org.jdom.output.Format;
+import org.jdom.output.XMLOutputter;
+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.WireFeedOutput;
+import com.sun.syndication.io.impl.Atom10Generator;
+import com.sun.syndication.io.impl.Atom10Parser;
+import org.rometools.propono.atom.common.AtomService;
+import org.rometools.propono.atom.common.Categories;
+import org.rometools.propono.utils.Utilities;
+import java.io.BufferedReader;
+import java.util.Collections;
+import java.util.Iterator;
+import javax.servlet.ServletConfig;
+
+/**
+ * Atom Servlet implements Atom protocol by calling an 
+ * {@link com.sun.syndication.propono.atom.server.AtomHandler}
+ * implementation. This servlet takes care of parsing incoming XML into ROME
+ * Atom {@link com.sun.syndication.feed.atom.Entry} objects, passing those to the handler and serializing
+ * to the response the entries and feeds returned by the handler.
+ */
+public class AtomServlet extends HttpServlet {
+   
+    /**
+     * Get feed type support by Servlet, "atom_1.0"
+     */
+    public static final String FEED_TYPE = "atom_1.0";
+    private static String contextDirPath = null;
+
+    private static Log log =
+            LogFactory.getFactory().getInstance(AtomServlet.class);
+    
+    static {
+        Atom10Parser.setResolveURIs(true);
+    }
+    
+    //-----------------------------------------------------------------------------
+    /**
+     * Create an Atom request handler.
+     * TODO: make AtomRequestHandler implementation configurable.
+     */
+    private AtomHandler createAtomRequestHandler(
+            HttpServletRequest request, HttpServletResponse response)
+    throws ServletException {
+        AtomHandlerFactory ahf = AtomHandlerFactory.newInstance();
+        return ahf.newAtomHandler(request, response);
+    }
+
+    //-----------------------------------------------------------------------------
+    /**
+     * Handles an Atom GET by calling handler and writing results to response.
+     */
+    protected void doGet(HttpServletRequest req, HttpServletResponse res)
+    throws ServletException, IOException { 
+        log.debug("Entering");
+        AtomHandler handler = createAtomRequestHandler(req, res);
+        String userName = handler.getAuthenticatedUsername();
+        if (userName != null) {
+            AtomRequest areq = new AtomRequestImpl(req);
+            try {
+                if (handler.isAtomServiceURI(areq)) {
+                    // return an Atom Service document
+                    AtomService service = handler.getAtomService(areq);
+                    Document doc = service.serviceToDocument(); 
+                    res.setContentType("application/atomsvc+xml; charset=utf-8");
+                    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.isCategoriesURI(areq)) {
+                    Categories cats = handler.getCategories(areq);
+                    res.setContentType("application/xml");
+                    Writer writer = res.getWriter();
+                    Document catsDoc = new Document();
+                    catsDoc.setRootElement(cats.categoriesToElement());
+                    XMLOutputter outputter = new XMLOutputter();
+                    outputter.output(catsDoc, writer);
+                    writer.close();
+                    res.setStatus(HttpServletResponse.SC_OK);
+                } 
+                else if (handler.isCollectionURI(areq)) {
+                    // return a collection
+                    Feed col = handler.getCollection(areq);
+                    col.setFeedType(FEED_TYPE);
+                    WireFeedOutput wireFeedOutput = new WireFeedOutput();
+                    Document feedDoc = wireFeedOutput.outputJDom(col);
+                    res.setContentType("application/atom+xml; charset=utf-8");
+                    Writer writer = res.getWriter();
+                    XMLOutputter outputter = new XMLOutputter();
+                    outputter.setFormat(Format.getPrettyFormat());
+                    outputter.output(feedDoc, writer);
+                    writer.close();
+                    res.setStatus(HttpServletResponse.SC_OK);
+                } 
+                else if (handler.isEntryURI(areq)) {
+                    // return an entry
+                    Entry entry = handler.getEntry(areq);
+                    if (entry != null) {
+                        res.setContentType("application/atom+xml; type=entry; charset=utf-8");
+                        Writer writer = res.getWriter();
+                        Atom10Generator.serializeEntry(entry, writer);
+                        writer.close();
+                    } else {
+                        res.setStatus(HttpServletResponse.SC_NOT_FOUND);
+                    }
+                }
+                else if (handler.isMediaEditURI(areq)) {
+                    AtomMediaResource entry = handler.getMediaResource(areq);
+                    res.setContentType(entry.getContentType());
+                    res.setContentLength((int)entry.getContentLength());
+                    Utilities.copyInputToOutput(entry.getInputStream(), res.getOutputStream());
+                    res.getOutputStream().flush();
+                    res.getOutputStream().close();
+                }
+                else {
+                    res.setStatus(HttpServletResponse.SC_NOT_FOUND);
+                }
+            } catch (AtomException ae) {
+                res.sendError(ae.getStatus(), ae.getMessage());
+                log.debug("ERROR processing GET", ae);
+            } catch (Exception e) {
+                res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+                log.debug("ERROR processing GET", e);
+            }
+        } else {
+            res.setHeader("WWW-Authenticate", "BASIC realm=\"AtomPub\"");
+            res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+        }
+        log.debug("Exiting");
+    }
+    
+    //-----------------------------------------------------------------------------
+    /**
+     * 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 {
+        log.debug("Entering");
+        AtomHandler handler = createAtomRequestHandler(req, res);
+        String userName = handler.getAuthenticatedUsername();
+        if (userName != null) {
+            AtomRequest areq = new AtomRequestImpl(req);
+            try {
+                if (handler.isCollectionURI(areq)) {
+                    
+                    if (req.getContentType().startsWith("application/atom+xml")) {
+
+                        // parse incoming entry
+                        Entry entry = Atom10Parser.parseEntry(new BufferedReader(
+                            new InputStreamReader(req.getInputStream(), "UTF-8")), null);
+
+                        // call handler to post it
+                        Entry newEntry = handler.postEntry(areq, entry);
+
+                        // set Location and Content-Location headers                        
+                        for (Iterator it = newEntry.getOtherLinks().iterator(); it.hasNext();) {
+                            Link link = (Link) it.next();
+                            if ("edit".equals(link.getRel())) {
+                                res.addHeader("Location", link.getHrefResolved());
+                                break;
+                            }
+                        }
+                        for (Iterator it = newEntry.getAlternateLinks().iterator(); it.hasNext();) {
+                            Link link = (Link) it.next();
+                            if ("alternate".equals(link.getRel())) {
+                                res.addHeader("Content-Location", link.getHrefResolved());
+                                break;
+                            }
+                        }
+
+                        // write entry back out to response
+                        res.setStatus(HttpServletResponse.SC_CREATED);
+                        res.setContentType("application/atom+xml; type=entry; charset=utf-8");
+                
+                        Writer writer = res.getWriter();
+                        Atom10Generator.serializeEntry(newEntry, writer);
+                        writer.close(); 
+                    
+                    } else if (req.getContentType() != null) {
+                                              
+                        // get incoming title and slug from HTTP header
+                        String title = areq.getHeader("Title");
+
+                        // create new entry for resource, set title and type
+                        Entry resource = new Entry();
+                        resource.setTitle(title);
+                        Content content = new Content();
+                        content.setType(areq.getContentType());
+                        resource.setContents(Collections.singletonList(content));
+        
+                        // hand input stream off to hander to post file
+                        Entry newEntry = handler.postMedia(areq, resource);
+
+                        // set Location and Content-Location headers
+                        for (Iterator it = newEntry.getOtherLinks().iterator(); it.hasNext();) {
+                            Link link = (Link) it.next();
+                            if ("edit".equals(link.getRel())) {
+                                res.addHeader("Location", link.getHrefResolved());
+                                break;
+                            }
+                        }
+                        for (Iterator it = newEntry.getAlternateLinks().iterator(); it.hasNext();) {
+                            Link link = (Link) it.next();
+                            if ("alternate".equals(link.getRel())) {
+                                res.addHeader("Content-Location", link.getHrefResolved());
+                                break;
+                            }
+                        }
+     
+                        res.setStatus(HttpServletResponse.SC_CREATED);                       
+                        res.setContentType("application/atom+xml; type=entry; charset=utf-8");
+                        
+                        Writer writer = res.getWriter();
+                        Atom10Generator.serializeEntry(newEntry, writer);
+                        writer.close(); 
+                        
+                    } else {
+                        res.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE,
+                                "No content-type specified in request");
+                    }
+                    
+                } else {
+                    res.sendError(HttpServletResponse.SC_NOT_FOUND,
+                            "Invalid collection specified in request");
+                }
+            } catch (AtomException ae) {
+                res.sendError(ae.getStatus(), ae.getMessage());
+                log.debug("ERROR processing POST", ae);
+            } catch (Exception e) {
+                res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+                log.debug("ERROR processing POST", e);
+            }
+        } else {
+            res.setHeader("WWW-Authenticate", "BASIC realm=\"AtomPub\"");
+            res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+        }
+        log.debug("Exiting");
+    }
+    
+    //-----------------------------------------------------------------------------
+    /**
+     * 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 {
+        log.debug("Entering");
+        AtomHandler handler = createAtomRequestHandler(req, res);
+        String userName = handler.getAuthenticatedUsername();
+        if (userName != null) {
+            AtomRequest areq = new AtomRequestImpl(req);
+            try {
+                if (handler.isEntryURI(areq)) {
+                    
+                    // parse incoming entry
+                    Entry unsavedEntry = Atom10Parser.parseEntry(new BufferedReader(
+                        new InputStreamReader(req.getInputStream(), "UTF-8")), null);
+                    
+                    // call handler to put entry
+                    handler.putEntry(areq, unsavedEntry);
+                    
+                    res.setStatus(HttpServletResponse.SC_OK);
+                    
+                } else if (handler.isMediaEditURI(areq)) {
+                    
+                    // hand input stream to handler
+                    handler.putMedia(areq);
+                                        
+                    res.setStatus(HttpServletResponse.SC_OK);
+                    
+                } else {
+                    res.setStatus(HttpServletResponse.SC_NOT_FOUND);
+                }
+            } catch (AtomException ae) {
+                res.sendError(ae.getStatus(), ae.getMessage());
+                log.debug("ERROR processing PUT", ae);
+            } catch (Exception e) {
+                res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+                log.debug("ERROR processing PUT", e);
+            }
+        } else {
+            res.setHeader("WWW-Authenticate", "BASIC realm=\"AtomPub\"");
+            // Wanted to use sendError() here but Tomcat sends 403 forbidden 
+            // when I do that, so sticking with setStatus() for time being.
+            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        }
+        log.debug("Exiting");
+    }
+    
+    //-----------------------------------------------------------------------------
+    /**
+     * Handle Atom DELETE by calling appropriate handler.
+     */
+    protected void doDelete(HttpServletRequest req, HttpServletResponse res)
+    throws ServletException, IOException {
+        log.debug("Entering");
+        AtomHandler handler = createAtomRequestHandler(req, res);
+        String userName = handler.getAuthenticatedUsername();
+        if (userName != null) {
+            AtomRequest areq = new AtomRequestImpl(req);
+            try {
+                if (handler.isEntryURI(areq)) {
+                    handler.deleteEntry(areq);
+                    res.setStatus(HttpServletResponse.SC_OK);
+                } 
+                else {
+                    res.setStatus(HttpServletResponse.SC_NOT_FOUND);
+                }
+            } catch (AtomException ae) {
+                res.sendError(ae.getStatus(), ae.getMessage());
+                log.debug("ERROR processing DELETE", ae);
+            } catch (Exception e) {
+                res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+                log.debug("ERROR processing DELETE", e);
+            }
+        } else {
+            res.setHeader("WWW-Authenticate", "BASIC realm=\"AtomPub\"");
+            // Wanted to use sendError() here but Tomcat sends 403 forbidden 
+            // when I do that, so sticking with setStatus() for time being.
+            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        }
+        log.debug("Exiting");
+    }
+    
+    /**
+     * Initialize servlet.
+     */
+    public void init( ServletConfig config ) throws ServletException {
+        super.init( config );
+        contextDirPath = getServletContext().getRealPath("/");
+        
+    }
+    
+    /**
+     * Get absolute path to Servlet context directory.
+     */
+    public static String getContextDirPath() {
+        return contextDirPath;
+    }
+}
diff --git a/src/main/java/org/rometools/propono/atom/server/FactoryConfigurationError.java b/src/main/java/org/rometools/propono/atom/server/FactoryConfigurationError.java
new file mode 100644
index 0000000..2ae8c6a
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/FactoryConfigurationError.java
@@ -0,0 +1,106 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.server;
+
+/**
+ * Thrown when a problem with configuration with the 
+ * {@link com.sun.syndication.propono.atom.server.AtomHandlerFactory} exists. 
+ * This error will typically be thrown when the class of a parser factory 
+ * specified in the system properties cannot be found or instantiated.
+ */
+public class FactoryConfigurationError extends Error {
+    
+    /**
+     * <code>Exception</code> that represents the error.
+     */
+    private Exception exception;
+    
+    /**
+     * Create a new <code>FactoryConfigurationError</code> with no
+     * detail mesage.
+     */    
+    public FactoryConfigurationError() {
+        super();
+        this.exception = null;
+    }
+    
+    /**
+     * Create a new <code>FactoryConfigurationError</code> with
+     * the <code>String </code> specified as an error message.
+     *
+     * @param msg The error message for the exception.
+     */    
+    public FactoryConfigurationError(String msg) {
+        super(msg);
+        this.exception = null;
+    }
+    
+    
+    /**
+     * Create a new <code>FactoryConfigurationError</code> with a
+     * given <code>Exception</code> base cause of the error.
+     *
+     * @param e The exception to be encapsulated in a
+     * FactoryConfigurationError.
+     */    
+    public FactoryConfigurationError(Exception e) {
+        super(e.toString());
+        this.exception = e;
+    }
+    
+    /**
+     * Create a new <code>FactoryConfigurationError</code> with the
+     * given <code>Exception</code> base cause and detail message.
+     *
+     * @param e The exception to be encapsulated in a
+     * FactoryConfigurationError
+     * @param msg The detail message.
+     */    
+    public FactoryConfigurationError(Exception e, String msg) {
+        super(msg);
+        this.exception = e;
+    }
+    
+    
+    /**
+     * Return the message (if any) for this error . If there is no
+     * message for the exception and there is an encapsulated
+     * exception then the message of that exception, if it exists will be
+     * returned. Else the name of the encapsulated exception will be
+     * returned.
+     *
+     * @return The error message.
+     */    
+    public String getMessage() {
+        String message = super.getMessage();
+        
+        if (message == null && exception != null) {
+            return exception.getMessage();
+        }
+        
+        return message;
+    }
+    
+    /**
+     * Return the actual exception (if any) that caused this exception to
+     * be raised.
+     *
+     * @return The encapsulated exception, or null if there is none.
+     */    
+    public Exception getException() {
+        return exception;
+    }
+}
diff --git a/src/main/java/org/rometools/propono/atom/server/FactoryFinder.java b/src/main/java/org/rometools/propono/atom/server/FactoryFinder.java
new file mode 100644
index 0000000..2545703
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/FactoryFinder.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * 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.rometools.propono.atom.server;
+
+import java.io.File;
+import java.io.FileInputStream;
+
+import java.util.Properties;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+
+/**
+ * Find {@link com.sun.syndication.propono.atom.server.AtomHandlerFactory} based on properties files.
+ */
+class FactoryFinder {
+    private static boolean debug = false;
+    static Properties cacheProps= new Properties();
+    static SecuritySupport ss = new SecuritySupport() ;
+    static boolean firstTime = true;
+    
+    private static void dPrint(String msg) {
+        if (debug) {
+            System.err.println("Propono: " + msg);
+        }
+    }
+    
+    /**
+     * Create an instance of a class using the specified ClassLoader and
+     * optionally fall back to the current ClassLoader if not found.
+     *
+     * @param className Name of the concrete class corresponding to the
+     * service provider
+     *
+     * @param cl ClassLoader to use to load the class, null means to use
+     * the bootstrap ClassLoader
+     *
+     * @param doFallback true if the current ClassLoader should be tried as
+     * a fallback if the class is not found using cl
+     */
+    private static Object newInstance(
+            String className, ClassLoader cl, boolean doFallback)
+            throws ConfigurationError {
+        
+        try {
+            Class providerClass;
+            if (cl == null) {
+                // If classloader is null Use the bootstrap ClassLoader.
+                // Thus Class.forName(String) will use the current
+                // ClassLoader which will be the bootstrap ClassLoader.
+                providerClass = Class.forName(className);
+            } else {
+                try {
+                    providerClass = cl.loadClass(className);
+                } catch (ClassNotFoundException x) {
+                    if (doFallback) {
+                        // Fall back to current classloader
+                        cl = FactoryFinder.class.getClassLoader();
+                        providerClass = cl.loadClass(className);
+                    } else {
+                        throw x;
+                    }
+                }
+            }
+            
+            Object instance = providerClass.newInstance();
+            dPrint("created new instance of " + providerClass +
+                    " using ClassLoader: " + cl);
+            return instance;
+        } catch (ClassNotFoundException x) {
+            throw new ConfigurationError(
+                 "Provider " + className + " not found", x);
+        } catch (Exception x) {
+            throw new ConfigurationError(
+                "Provider " + className + " could not be instantiated: " + x, x);
+        }
+    }
+    
+    /**
+     * Finds the implementation Class object in the specified order.  Main
+     * entry point.
+     * @return Class object of factory, never null
+     *
+     * @param factoryId             Name of the factory to find, same as
+     *                              a property name
+     * @param fallbackClassName     Implementation class name, if nothing else
+     *                              is found.  Use null to mean no fallback.
+     *
+     * Package private so this code can be shared.
+     */
+    static Object find(String factoryId, String fallbackClassName)
+    throws ConfigurationError {
+        
+        // Figure out which ClassLoader to use for loading the provider
+        // class.  If there is a Context ClassLoader then use it.
+        
+        ClassLoader classLoader = ss.getContextClassLoader();
+        
+        if (classLoader == null) {
+            // if we have no Context ClassLoader
+            // so use the current ClassLoader
+            classLoader = FactoryFinder.class.getClassLoader();
+        }
+        
+        dPrint("find factoryId =" + factoryId);
+        
+        // Use the system property first
+        try {
+            String systemProp = ss.getSystemProperty(factoryId);
+            if( systemProp!=null) {
+                dPrint("found system property, value=" + systemProp);
+                return newInstance(systemProp, classLoader, true );
+            }
+        } catch (SecurityException se) {
+            //if first option fails due to any reason we should try next option in the
+            //look up algorithm.
+        }
+        
+        // try to read from /propono.properties
+        try {
+            String javah = ss.getSystemProperty("java.home");
+            String configFile = "/propono.properties";
+            String factoryClassName = null;
+            if(firstTime){
+                synchronized(cacheProps){
+                    if (firstTime) {
+                        try {
+                           InputStream is = FactoryFinder.class.getResourceAsStream(configFile);
+                           firstTime = false;
+                           if (is != null) {
+                               dPrint("Read properties file: " + configFile);
+                               cacheProps.load(is);
+                           }
+                        } catch (Exception intentionallyIgnored) {}
+                    }
+                }
+            }
+            factoryClassName = cacheProps.getProperty(factoryId);
+            
+            if(factoryClassName != null){
+                dPrint("found in $java.home/propono.properties, value=" + factoryClassName);
+                return newInstance(factoryClassName, classLoader, true);
+            }
+        } catch(Exception ex) {
+            if( debug ) ex.printStackTrace();
+        }
+        
+        // Try Jar Service Provider Mechanism
+        Object provider = findJarServiceProvider(factoryId);
+        if (provider != null) {
+            return provider;
+        }
+        if (fallbackClassName == null) {
+            throw new ConfigurationError(
+                    "Provider for " + factoryId + " cannot be found", null);
+        }
+        
+        dPrint("loaded from fallback value: " + fallbackClassName);
+        return newInstance(fallbackClassName, classLoader, true);
+    }
+    
+    /*
+     * Try to find provider using Jar Service Provider Mechanism
+     *
+     * @return instance of provider class if found or null
+     */
+    private static Object findJarServiceProvider(String factoryId)
+    throws ConfigurationError {
+        
+        String serviceId = "META-INF/services/" + factoryId;
+        InputStream is = null;
+        
+        // First try the Context ClassLoader
+        ClassLoader cl = ss.getContextClassLoader();
+        if (cl != null) {
+            is = ss.getResourceAsStream(cl, serviceId);
+            
+            // If no provider found then try the current ClassLoader
+            if (is == null) {
+                cl = FactoryFinder.class.getClassLoader();
+                is = ss.getResourceAsStream(cl, serviceId);
+            }
+        } else {
+            // No Context ClassLoader, try the current
+            // ClassLoader
+            cl = FactoryFinder.class.getClassLoader();
+            is = ss.getResourceAsStream(cl, serviceId);
+        }
+        
+        if (is == null) {
+            // No provider found
+            return null;
+        }
+        
+        dPrint("found jar resource=" + serviceId +
+                " using ClassLoader: " + cl);
+        
+        // Read the service provider name in UTF-8 as specified in
+        // the jar spec.  Unfortunately this fails in Microsoft
+        // VJ++, which does not implement the UTF-8
+        // encoding. Theoretically, we should simply let it fail in
+        // that case, since the JVM is obviously broken if it
+        // doesn't support such a basic standard.  But since there
+        // are still some users attempting to use VJ++ for
+        // development, we have dropped in a fallback which makes a
+        // second attempt using the platform's default encoding. In
+        // VJ++ this is apparently ASCII, which is a subset of
+        // UTF-8... and since the strings we'll be reading here are
+        // also primarily limited to the 7-bit ASCII range (at
+        // least, in English versions), this should work well
+        // enough to keep us on the air until we're ready to
+        // officially decommit from VJ++. [Edited comment from
+        // jkesselm]
+        BufferedReader rd;
+        try {
+            rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+        } catch (java.io.UnsupportedEncodingException e) {
+            rd = new BufferedReader(new InputStreamReader(is));
+        }
+        
+        String factoryClassName = null;
+        try {
+            // XXX Does not handle all possible input as specified by the
+            // Jar Service Provider specification
+            factoryClassName = rd.readLine();
+            rd.close();
+        } catch (IOException x) {
+            // No provider found
+            return null;
+        }
+        
+        if (factoryClassName != null &&
+                ! "".equals(factoryClassName)) {
+            dPrint("found in resource, value="
+                    + factoryClassName);
+            
+            // Note: here we do not want to fall back to the current
+            // ClassLoader because we want to avoid the case where the
+            // resource file was found using one ClassLoader and the
+            // provider class was instantiated using a different one.
+            return newInstance(factoryClassName, cl, false);
+        }
+        
+        // No provider found
+        return null;
+    }
+    
+    static class ConfigurationError extends Error {
+        private Exception exception;
+        
+        /**
+         * Construct a new instance with the specified detail string and
+         * exception.
+         */
+        ConfigurationError(String msg, Exception x) {
+            super(msg);
+            this.exception = x;
+        }
+        
+        Exception getException() {
+            return exception;
+        }
+    }
+    
+}
diff --git a/src/main/java/org/rometools/propono/atom/server/SecuritySupport.java b/src/main/java/org/rometools/propono/atom/server/SecuritySupport.java
new file mode 100644
index 0000000..3d603d9
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/SecuritySupport.java
@@ -0,0 +1,90 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.server;
+
+import java.security.*;
+import java.net.*;
+import java.io.*;
+import java.util.*;
+
+/**
+ * This class is duplicated for each subpackage, it is package private and 
+ * therefore is not exposed as part of the public API.
+ */
+class SecuritySupport  {
+        
+    ClassLoader getContextClassLoader() {
+        return (ClassLoader)
+        AccessController.doPrivileged(new PrivilegedAction() {
+            public Object run() {
+                ClassLoader cl = null;
+                try {
+                    cl = Thread.currentThread().getContextClassLoader();
+                } catch (SecurityException ex) { }
+                return cl;
+            }
+        });
+    }
+    
+    String getSystemProperty(final String propName) {
+        return (String)
+        AccessController.doPrivileged(new PrivilegedAction() {
+            public Object run() {
+                return System.getProperty(propName);
+            }
+        });
+    }
+    
+    FileInputStream getFileInputStream(final File file)
+    throws FileNotFoundException {
+        try {
+            return (FileInputStream)
+            AccessController.doPrivileged(new PrivilegedExceptionAction() {
+                public Object run() throws FileNotFoundException {
+                    return new FileInputStream(file);
+                }
+            });
+        } catch (PrivilegedActionException e) {
+            throw (FileNotFoundException)e.getException();
+        }
+    }
+    
+    InputStream getResourceAsStream(final ClassLoader cl,
+            final String name) {
+        return (InputStream)
+        AccessController.doPrivileged(new PrivilegedAction() {
+            public Object run() {
+                InputStream ris;
+                if (cl == null) {
+                    ris = ClassLoader.getSystemResourceAsStream(name);
+                } else {
+                    ris = cl.getResourceAsStream(name);
+                }
+                return ris;
+            }
+        });
+    }
+    
+    boolean doesFileExist(final File f) {
+        return ((Boolean)
+        AccessController.doPrivileged(new PrivilegedAction() {
+            public Object run() {
+                return new Boolean(f.exists());
+            }
+        })).booleanValue();
+    }
+    
+}
diff --git a/src/main/java/org/rometools/propono/atom/server/impl/FileBasedAtomHandler.java b/src/main/java/org/rometools/propono/atom/server/impl/FileBasedAtomHandler.java
new file mode 100644
index 0000000..7d8a8fe
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/impl/FileBasedAtomHandler.java
@@ -0,0 +1,443 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.server.impl;
+
+import org.rometools.propono.atom.server.AtomMediaResource;
+import org.apache.commons.codec.binary.Base64;
+import org.rometools.propono.atom.server.AtomHandler;
+import org.rometools.propono.atom.server.AtomException;
+import org.rometools.propono.atom.server.AtomServlet;
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.feed.atom.Feed;
+import org.rometools.propono.atom.common.AtomService;
+import org.rometools.propono.atom.common.Categories;
+import org.rometools.propono.atom.server.AtomRequest;
+import java.io.File;
+import java.util.StringTokenizer;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import javax.servlet.http.HttpServletRequest;
+import org.apache.commons.lang.StringUtils;
+
+
+/**
+ * File-based {@link com.sun.syndication.propono.atom.server.AtomHandler}
+ * implementation that stores entries and media-entries to disk. Implemented 
+ * using {@link com.sun.syndication.propono.atom.server.impl.FileBasedAtomService}.
+ */
+public class FileBasedAtomHandler implements AtomHandler {
+
+    private static Log log =
+            LogFactory.getFactory().getInstance(FileBasedAtomHandler.class);
+    
+    private static String fileStoreDir = null;
+
+    private String userName = null;
+    private String atomProtocolURL = null;
+    private String contextURI = null;    
+    private String uploadurl = null;
+    
+    private FileBasedAtomService service = null;
+    
+    /**
+     * Construct handler to handle one request.
+     * @param req Request to be handled.
+     */
+    public FileBasedAtomHandler( HttpServletRequest req ) {     
+        this(req, AtomServlet.getContextDirPath());
+    } 
+    
+    /**
+     * Contruct handler for one request, using specified file storage directory.
+     * @param req       Request to be handled.
+     * @param uploaddir File storage upload dir.
+     */
+    public FileBasedAtomHandler(HttpServletRequest req, String uploaddir) {
+        log.debug("ctor");
+        
+        userName = authenticateBASIC(req);        
+        
+        atomProtocolURL = req.getScheme() + "://" + req.getServerName() + ":" 
+                + req.getServerPort()  + req.getContextPath() + req.getServletPath();
+        
+        contextURI = req.getScheme() + "://" + req.getServerName() + ":" 
+                + req.getServerPort()  + req.getContextPath();
+    
+        try {
+            service = new FileBasedAtomService(userName, uploaddir, 
+                    contextURI, req.getContextPath(), req.getServletPath());            
+        } catch (Throwable t) {
+            throw new RuntimeException("ERROR creating FileBasedAtomService", t);
+        }
+    }
+    
+    /** 
+     * Method used for validating user. Developers can overwrite this method 
+     * and use credentials stored in Database or LDAP to confirm if the user is 
+     * allowed to access this service.
+     * @param login    user submitted login id
+     * @param password user submitted password 
+     */
+    public boolean validateUser(String login, String password) {
+        return true;
+    }
+    
+    /**
+     * Get username of authenticated user
+     * @return User name.
+     */
+    public String getAuthenticatedUsername() {
+        // For now return userName as the login id entered for authorization
+        return userName;
+    }
+    
+    /**
+     * Get base URI of Atom protocol implementation.
+     * @return Base URI of Atom protocol implemenation.
+     */
+    public String getAtomProtocolURL( ) {
+        if ( atomProtocolURL == null ) {
+            return "app";
+        } else {
+            return  atomProtocolURL;
+        }
+    }
+    
+    /**
+     * Return introspection document
+     * @throws com.sun.syndication.propono.atom.server.AtomException Unexpected exception.
+     * @return AtomService object with workspaces and collections.
+     */
+    public AtomService getAtomService(AtomRequest areq) throws AtomException {               
+        return service;
+    }
+    
+    /**
+     * Returns null because we use in-line categories.
+     * @throws com.sun.syndication.propono.atom.server.AtomException Unexpected exception.
+     * @return Categories object
+     */
+    public Categories getCategories(AtomRequest areq) throws AtomException {    
+        log.debug("getCollection");
+        String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+        String handle = pathInfo[0];
+        String collection = pathInfo[1];
+        FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+        return (Categories)col.getCategories(true).get(0); 
+    }
+    
+    /**
+     * Get collection specified by pathinfo.
+     * @param areq Details of HTTP request
+     * @return ROME feed representing collection.
+     * @throws com.sun.syndication.propono.atom.server.AtomException Invalid collection or other exception.
+     */
+    public Feed getCollection(AtomRequest areq) throws AtomException {
+        log.debug("getCollection");
+        String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+        String handle = pathInfo[0];
+        String collection = pathInfo[1];
+        FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+        return col.getFeedDocument();        
+    }
+    
+    /**
+     * Create a new entry specified by pathInfo and posted entry. We save the 
+     * submitted Atom entry verbatim, but we do set the id and reset the update
+     * time.  
+     *
+     * @param entry Entry to be added to collection.
+     * @param areq Details of HTTP request
+     * @throws com.sun.syndication.propono.atom.server.AtomException On invalid collection or other error.
+     * @return Entry as represented on server.
+     */
+    public Entry postEntry(AtomRequest areq, Entry entry) throws AtomException {
+        log.debug("postEntry");
+        
+        String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+        String handle = pathInfo[0];
+        String collection = pathInfo[1];
+        FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+        try {
+            return col.addEntry(entry); 
+            
+        } catch (Exception fe) {
+            fe.printStackTrace();
+            throw new AtomException( fe );
+        }
+    }
+    
+    /**
+     * Get entry specified by pathInfo.
+     * @param areq Details of HTTP request
+     * @throws com.sun.syndication.propono.atom.server.AtomException On invalid pathinfo or other error.
+     * @return ROME Entry object.
+     */
+    public Entry getEntry(AtomRequest areq) throws AtomException {
+        log.debug("getEntry");
+        String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+        String handle = pathInfo[0];
+        String collection = pathInfo[1];
+        String fileName = pathInfo[2];
+        FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+        try {              
+            return col.getEntry(fileName);
+                
+        } catch (Exception re) {
+            if (re instanceof AtomException) throw (AtomException)re;
+            throw new AtomException("ERROR: getting entry", re);
+        }
+    }
+    
+    /**
+     * Update entry specified by pathInfo and posted entry.
+     * 
+     * @param entry 
+     * @param areq Details of HTTP request
+     * @throws com.sun.syndication.propono.atom.server.AtomException 
+     */
+    public void putEntry(AtomRequest areq, Entry entry) throws AtomException {
+        log.debug("putEntry");
+        String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+        String handle = pathInfo[0];
+        String collection = pathInfo[1];
+        String fileName = pathInfo[2];
+        FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+        try {            
+            col.updateEntry(entry, fileName);
+            
+        } catch ( Exception fe ) {
+            throw new AtomException( fe );
+        }
+    }
+    
+    
+    /**
+     * Delete entry specified by pathInfo.
+     * @param areq Details of HTTP request
+     */
+    public void deleteEntry(AtomRequest areq) throws AtomException {
+        log.debug("deleteEntry");
+        String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+        String handle = pathInfo[0];
+        String collection = pathInfo[1];
+        String fileName = pathInfo[2];
+        FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+        try {
+            col.deleteEntry(fileName); 
+
+        } catch (Exception e) {
+            String msg = "ERROR in atom.deleteResource";
+            log.error(msg,e);
+            throw new AtomException(msg);
+        }
+    }
+    
+
+    /**
+     * Store media data in collection specified by pathInfo, create an Atom
+     * media-link entry to store metadata for the new media file and return 
+     * that entry to the caller.
+     * @param areq Details of HTTP request
+     * @param entry    New entry initialzied with only title and content type
+     * @return Location URL of new media entry
+     */
+    public Entry postMedia(AtomRequest areq, Entry entry) throws AtomException {
+        
+        // get incoming slug from HTTP header
+        String slug = areq.getHeader("Slug");
+        
+        if (log.isDebugEnabled()) {
+            log.debug("postMedia - title: "+entry.getTitle()+" slug:"+slug);
+        }       
+                        
+        try {
+            File tempFile = null;
+            String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+            String handle = pathInfo[0];
+            String collection = pathInfo[1];
+            FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+            try {
+                col.addMediaEntry(entry, slug, areq.getInputStream());
+
+            } catch (Exception e) {
+                e.printStackTrace();
+                String msg = "ERROR reading posted file";
+                log.error(msg,e);
+                throw new AtomException(msg, e);
+            } finally {
+                if (tempFile != null) tempFile.delete();
+            }
+            
+        } catch (Exception re) {
+            throw new AtomException("ERROR: posting media");
+        }
+        return entry;
+   }
+    
+    /**
+     * Update the media file part of a media-link entry.
+     * @param areq Details of HTTP request
+     * Assuming pathInfo of form /user-name/resource/name
+     */
+    public void putMedia(AtomRequest areq) throws AtomException {
+        
+        log.debug("putMedia");
+        String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+        String handle =     pathInfo[0];
+        String collection = pathInfo[1];
+        String fileName =   pathInfo[3];
+        FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+        try {
+            col.updateMediaEntry(fileName, areq.getContentType(), areq.getInputStream());
+            
+        } catch (Exception re) {
+            throw new AtomException("ERROR: posting media");
+        }
+    }
+    
+    public AtomMediaResource getMediaResource(AtomRequest areq) throws AtomException {
+        log.debug("putMedia");
+        String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+        String handle =     pathInfo[0];
+        String collection = pathInfo[1];
+        String fileName =   pathInfo[3];
+        FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+        try {
+            return col.getMediaResource(fileName);
+            
+        } catch (Exception re) {
+            throw new AtomException("ERROR: posting media");
+        }
+    }
+
+    /**
+     * Return true if specified pathinfo represents URI of service doc.
+     */
+    public boolean isAtomServiceURI(AtomRequest areq) {        
+        String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+        if (pathInfo.length==0) return true; 
+        return false;
+    }
+    
+    /**
+     * Return true if specified pathinfo represents URI of category doc.
+     */
+    public boolean isCategoriesURI(AtomRequest areq) {        
+        log.debug("isCategoriesDocumentURI");
+        String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+        if (pathInfo.length == 3 && "categories".equals(pathInfo[2])) {
+            return true;
+        }
+        return false;
+    }
+    
+    /**
+     * Return true if specified pathinfo represents URI of a collection.
+     */
+    public boolean isCollectionURI(AtomRequest areq) {
+        log.debug("isCollectionURI");
+        // workspace/collection-plural
+        // if length is 2 and points to a valid collection then YES
+        String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+        if (pathInfo.length == 2) {
+            String handle     = pathInfo[0];
+            String collection = pathInfo[1];
+            if (service.findCollectionByHandle(handle, collection) != null) {
+                return true;
+            }
+        }
+        return false;
+        
+    }
+    
+    /**
+     * Return true if specified pathinfo represents URI of an Atom entry.
+     */
+    public boolean isEntryURI(AtomRequest areq) {
+        log.debug("isEntryURI");
+        // workspace/collection-singular/fsid
+        // if length is 3 and points to a valid collection then YES
+        String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+        if (pathInfo.length == 3) {
+            String handle     = pathInfo[0];
+            String collection = pathInfo[1];
+            if (service.findCollectionByHandle(handle, collection) != null) {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * Return true if specified pathinfo represents media-edit URI.
+     */
+    public boolean isMediaEditURI(AtomRequest areq) {
+        log.debug("isMediaEditURI");
+        // workspace/collection-singular/fsid/media/fsid
+        // if length is 4, points to a valid collection and fsid is mentioned twice then YES
+        String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+        if (pathInfo.length == 4) {
+            String handle     = pathInfo[0];
+            String collection = pathInfo[1];
+            String media      = pathInfo[2];
+            String fsid      = pathInfo[3];
+            if (service.findCollectionByHandle(handle, collection) != null && media.equals("media")) {
+                return true;
+            }
+        }
+        return false;
+        
+    }
+    
+    /**
+     * BASIC authentication.
+     */
+    public String authenticateBASIC(HttpServletRequest request) {
+        log.debug("authenticateBASIC");
+        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.decodeBase64(credentials.getBytes()));
+                        int p = userPass.indexOf(":");
+                        if (p != -1) {
+                            userID = userPass.substring(0, p);
+                            password = userPass.substring(p+1);
+                            
+                            //  Validate the User.
+                            valid = validateUser( userID, password );
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.debug(e);
+        }
+        if (valid) {
+            //For now assume userID as userName
+            return userID;
+        }
+        return null;
+    }
+}
diff --git a/src/main/java/org/rometools/propono/atom/server/impl/FileBasedAtomHandlerFactory.java b/src/main/java/org/rometools/propono/atom/server/impl/FileBasedAtomHandlerFactory.java
new file mode 100644
index 0000000..12070db
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/impl/FileBasedAtomHandlerFactory.java
@@ -0,0 +1,37 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.server.impl;
+
+import org.rometools.propono.atom.server.AtomHandlerFactory;
+import org.rometools.propono.atom.server.AtomHandler;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Extends {@link com.sun.syndication.propono.atom.server.AtomHandlerFactory} to create and return 
+ * {@link com.sun.syndication.propono.atom.server.impl.FileBasedAtomHandler}.
+ */
+public class FileBasedAtomHandlerFactory extends AtomHandlerFactory {
+    
+    /**
+     * Create new AtomHandler.
+     */
+    public AtomHandler newAtomHandler( 
+            HttpServletRequest req, HttpServletResponse res ) {
+        return new FileBasedAtomHandler(req);
+    }    
+}
+      
diff --git a/src/main/java/org/rometools/propono/atom/server/impl/FileBasedAtomService.java b/src/main/java/org/rometools/propono/atom/server/impl/FileBasedAtomService.java
new file mode 100644
index 0000000..5f2eeec
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/impl/FileBasedAtomService.java
@@ -0,0 +1,188 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.server.impl;
+
+import org.rometools.propono.atom.common.AtomService;
+import org.rometools.propono.utils.Utilities;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+
+/**
+ * File based Atom service.
+ * Supports one workspace per user.
+ * Collections in workspace are defined in /propono.properties, for example:
+ *
+ * <pre>
+ *    # Define list of collections to be offered
+ *    propono.atomserver.filebased.collections=entries,gifimages
+ *
+ *    # Defines 'entries' collection, accepts entries
+ *    propono.atomserver.filebased.collection.entries.title=Entries
+ *    propono.atomserver.filebased.collection.entries.singular=entry
+ *    propono.atomserver.filebased.collection.entries.plural=entries
+ *    propono.atomserver.filebased.collection.entries.accept=application/atom+xml;type=entry
+ *    propono.atomserver.filebased.collection.entries.categories=general,category1,category2
+ *
+ *    # Defines 'gifimages' collection, accepts only GIF files
+ *    propono.atomserver.filebased.collection.gifimages.title=GIF Images
+ *    propono.atomserver.filebased.collection.gifimages.singular=gif
+ *    propono.atomserver.filebased.collection.gifimages.plural=gifs
+ *    propono.atomserver.filebased.collection.gifimages.accept=image/gif
+ *    propono.atomserver.filebased.collection.gifimages.categories=general,category1,category2
+ * </pre>
+ *
+ * If no such properties are found, then service will fall back to two 
+ * collections: 'entries' for Atom entries and 'resources' for any content-type.
+ * 
+ *
+ * <p><b>URI structure used for accessing collections and entries</b></p>
+ * 
+ * <p>Collection feed (URI allows GET to get collection, POST to add to it)<br/>
+ *     <code>[servlet-context-uri]/app/[workspace-handle]/[collection-plural] </code>
+ * </p>
+ * 
+ * <p>Collection entry (URI allows GET, PUT and DELETE)<br/>
+ *     <code>[servlet-context-uri]/app/[workspace-handle]/[collection-singular]/[entryid] </code>
+ * </p>
+ * 
+ * <p>Collection entry media (URI allows GET, PUT and DELETE)<br/>
+ *     <code>[servlet-context-uri]/app/[workspace-handle]/[collection-singular]/media/[entryid]</code>
+ * </p>
+ * 
+ * <p>Categories URI if not using inline categories (URI allows GET)<br/>
+ *     <code>[servlet-context-uri]/app/[workspace-handle]/[collection-plural]/categories</code>
+ * </p>
+ * 
+ *
+ * <p><b>Directory structure used to store collections and entries</b></p>
+ *
+ * <p>Collection feed (kept constantly up to date)<br/>
+ *     <code>[servlet-context-dir]/[workspace-handle]/[collection-plural]/feed.xml</code> 
+ * </p>
+ *
+ * <p>Collection entry (individual entries also stored as entry.xml files)<br/>
+ *     <code>[servlet-context-dir]/[workspace-handle]/[collection-plural]/id/entry.xml</code>
+ * </p>
+ *
+ * <p>Collection entry media (media file stored under entry directory)<br/>
+ *     <code>[servlet-context-dir]/[workspace-handle]/[collection-plural]/id/media/id</code>
+ * </p>
+ */
+public class FileBasedAtomService extends AtomService {
+    private Map workspaceMap = new TreeMap();
+    private Map collectionMap = new TreeMap();
+    private static Properties cacheProps = new Properties();
+    private boolean firstTime = true;
+    
+    /** 
+     * Creates a new instance of FileBasedAtomService.
+     */
+    public FileBasedAtomService(
+            String userName, String baseDir, String contextURI, String contextPath, String servletPath) throws Exception {
+        String workspaceHandle = userName;
+
+        // One workspace per user        
+        FileBasedWorkspace workspace = new FileBasedWorkspace(workspaceHandle, baseDir);
+        workspaceMap.put(userName, workspace);
+                       
+        if (firstTime) {
+            synchronized(cacheProps) {
+               InputStream is = getClass().getResourceAsStream("/propono.properties");
+               if (is != null) cacheProps.load(is);
+               firstTime = false;
+            }
+        }
+        // can't find propono.properties, so use system props instead
+        if (cacheProps == null) cacheProps = System.getProperties();        
+        
+        String relativeURIsString = cacheProps.getProperty(
+                "propono.atomserver.filebased.relativeURIs");
+        boolean relativeURIs = "true".equals(relativeURIsString);
+        
+        String inlineCategoriesString = cacheProps.getProperty(
+                "propono.atomserver.filebased.inlineCategories");
+        boolean inlineCategories = "true".equals(inlineCategoriesString);
+        
+        String colnames = cacheProps.getProperty("propono.atomserver.filebased.collections");
+        if (colnames != null) {
+            
+            // collections specified in propono.properties, use those
+            
+            String[] colarray = Utilities.stringToStringArray(colnames,",");
+            for (int i=0; i<colarray.length; i++) {
+                String prefix = "propono.atomserver.filebased.collection." + colarray[i] + ".";
+                String collectionTitle = cacheProps.getProperty(prefix + "title");
+                String collectionSingular = cacheProps.getProperty(prefix + "singular");
+                String collectionPlural = cacheProps.getProperty(prefix + "plural");
+                String collectionAccept = cacheProps.getProperty(prefix + "accept");
+                
+                String catNamesString = cacheProps.getProperty(prefix + "categories");
+                String[] catNames = Utilities.stringToStringArray(catNamesString, ",");
+                
+                FileBasedCollection entries = new FileBasedCollection(
+                        collectionTitle, workspaceHandle, 
+                        collectionPlural, collectionSingular, collectionAccept,
+                        inlineCategories, catNames,
+                        relativeURIs, contextURI, contextPath, servletPath, baseDir);
+                workspace.addCollection(entries);
+                // want to be able to look up collection by singular and plural names
+                collectionMap.put(workspaceHandle + "|" + collectionSingular, entries);
+                collectionMap.put(workspaceHandle + "|" + collectionPlural, entries);
+            }
+        } else {
+            
+            // Fallback to two collections. One collection for accepting entries 
+            // and other collection for other ( resources/uploaded images etc.)
+            
+            String[] catNames = new String[] {"general", "category1", "category2"};
+        
+            FileBasedCollection entries = new FileBasedCollection(
+                "Entries", workspaceHandle, 
+                "entries", "entry", "application/atom+xml;type=entry",
+                inlineCategories, catNames, 
+                relativeURIs, contextURI, contextPath, servletPath, baseDir);
+            workspace.addCollection(entries);
+            // want to be able to look up collection by singular and plural names
+            collectionMap.put(workspaceHandle + "|entry", entries);
+            collectionMap.put(workspaceHandle + "|entries", entries);
+
+            FileBasedCollection resources = new FileBasedCollection(
+                "Resources", workspaceHandle, 
+                "resources", "resource", "*/*",
+                inlineCategories, catNames, 
+                relativeURIs, contextURI, contextPath, servletPath, baseDir);
+            // want to be able to look up collection by singular and plural names
+            workspace.addCollection(resources);
+            collectionMap.put(workspaceHandle + "|resource", resources);
+            collectionMap.put(workspaceHandle + "|resources", resources);   
+        }
+        
+        getWorkspaces().add(workspace);
+    }
+    
+    /**
+     * Find workspace by handle, returns null of not found.
+     */
+    FileBasedWorkspace findWorkspaceByHandle(String handle) {
+        return (FileBasedWorkspace)workspaceMap.get(handle);
+    }
+    
+    FileBasedCollection findCollectionByHandle(String handle, String collection) {
+        return (FileBasedCollection)collectionMap.get(handle+"|"+collection);
+    }  
+}
diff --git a/src/main/java/org/rometools/propono/atom/server/impl/FileBasedCollection.java b/src/main/java/org/rometools/propono/atom/server/impl/FileBasedCollection.java
new file mode 100644
index 0000000..9101e25
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/impl/FileBasedCollection.java
@@ -0,0 +1,837 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.server.impl;
+
+import java.util.Iterator;
+import org.jdom.Document;
+import org.jdom.output.XMLOutputter;
+
+
+import com.sun.syndication.feed.WireFeed;
+import com.sun.syndication.feed.atom.Category;
+import com.sun.syndication.feed.atom.Content;
+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 com.sun.syndication.io.impl.Atom10Generator;
+import com.sun.syndication.io.impl.Atom10Parser;
+import org.rometools.propono.atom.common.Categories;
+import org.rometools.propono.atom.common.Collection;
+import org.rometools.propono.atom.common.rome.AppModule;
+import org.rometools.propono.atom.common.rome.AppModuleImpl;
+
+import org.rometools.propono.atom.server.AtomException;
+import org.rometools.propono.atom.server.AtomMediaResource;
+import org.rometools.propono.atom.server.AtomNotFoundException;
+import org.rometools.propono.utils.Utilities;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.StringTokenizer;
+import javax.activation.FileTypeMap;
+import javax.activation.MimetypesFileTypeMap;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * File based Atom collection implementation. This is the heart of the 
+ * file-based Atom service implementation. It provides methods for adding, 
+ * getting updating and deleting Atom entries and media entries.
+ */
+public class FileBasedCollection extends Collection {
+    private static Log log =
+            LogFactory.getFactory().getInstance(FileBasedCollection.class);
+    
+    private String handle = null;
+    private String singular = null;
+    private String collection = null;
+    
+    private boolean inlineCats = false;
+    private String[] catNames = null;
+
+    private boolean relativeURIs = false;
+    private String contextURI = null; 
+    private String contextPath = null;
+    private String servletPath = null;
+    private String baseDir = null;
+    
+    private static final String FEED_TYPE = "atom_1.0";
+
+    /**
+     * Construct by providing title (plain text, no HTML), a workspace handle,
+     * a plural collection name (e.g. entries), a singular collection name
+     * (e.g. entry), the base directory for file storage, the content-type
+     * range accepted by the collection and the root Atom protocol URI for the
+     * service.
+     * @param title        Title of collection (plain text, no HTML)
+     * @param handle       Workspace handle
+     * @param collection   Collection handle, plural
+     * @param singular     Collection handle, singular
+     * @param accept       Content type range accepted by collection
+     * @param inlineCats   True for inline categories
+     * @param catNames     Category names for this workspace
+     * @param baseDir      Base directory for file storage
+     * @param relativeURIs True for relative URIs
+     * @param contextURI   Absolute URI of context that hosts APP service
+     * @param contextPath  Context path of APP service (e.g. "/sample-atomserver")
+     * @param servletPath  Servlet path of APP service (e.g. "/app")
+     */
+    public FileBasedCollection(
+        String title,
+        String handle, 
+        String collection, 
+        String singular, 
+        String accept,
+        boolean inlineCats, 
+        String[] catNames,
+        boolean relativeURIs,
+        String contextURI,
+        String contextPath,
+        String servletPath,
+        String baseDir) {       
+        super(title, "text", 
+                relativeURIs 
+                ? servletPath.substring(1) + "/" + handle + "/" + collection
+                : contextURI + servletPath + "/" + handle + "/" + collection);
+        
+        this.handle = handle;
+        this.collection = collection;
+        this.singular = singular;
+        this.inlineCats = inlineCats;
+        this.catNames = catNames;
+        this.baseDir = baseDir;
+        this.relativeURIs = relativeURIs;
+        this.contextURI = contextURI;
+        this.contextPath = contextPath;
+        this.servletPath = servletPath;
+        
+        addAccept(accept);
+    }
+    
+    /**
+     * Get feed document representing collection.
+     * @throws com.sun.syndication.propono.atom.server.AtomException On error retrieving feed file.
+     * @return Atom Feed representing collection.
+     */
+    public Feed getFeedDocument() throws AtomException {
+        InputStream in = null;        
+        synchronized(FileStore.getFileStore()) {
+            in = FileStore.getFileStore().getFileInputStream(getFeedPath());            
+            if (in == null) {
+                in = createDefaultFeedDocument(contextURI + servletPath + "/" + handle + "/" + collection);
+            }
+        }        
+        try {
+            WireFeedInput input = new WireFeedInput();
+            WireFeed wireFeed = input.build(new InputStreamReader(in, "UTF-8"));
+            return (Feed)wireFeed;
+        } catch (Exception ex) {
+            throw new AtomException(ex);
+        } 
+    }   
+
+    /**
+     * Get list of one Categories object containing categories allowed by collection.
+     * @param inline True if Categories object should contain collection of 
+     *                in-line Categories objects or false if it should set the 
+     *                Href for out-of-line categories.
+     */
+    public List getCategories(boolean inline) {
+        Categories cats = new Categories();
+        cats.setFixed(true);
+        cats.setScheme(contextURI + "/" + handle + "/" + singular);
+        if (inline) {
+            for (int i=0; i<catNames.length; i++) {
+                Category cat = new Category();
+                cat.setTerm(catNames[i]);
+                cats.addCategory(cat);
+            }
+        } else {
+            cats.setHref(getCategoriesURI());
+        }
+        return Collections.singletonList(cats);
+    }
+    
+    /**
+     * Get list of one Categories object containing categories allowed by collection,
+     * returns in-line categories if collection set to use in-line categories.
+     */
+    public List getCategories() {
+        return getCategories(inlineCats);
+    }
+    
+    /**
+     * Add entry to collection.
+     * @param entry Entry to be added to collection. Entry will be saved to disk in a 
+     * directory under the collection's directory and the path will follow the 
+     * pattern [collection-plural]/[entryid]/entry.xml. The entry will be added 
+     * to the collection's feed in [collection-plural]/feed.xml.
+     * @throws java.lang.Exception On error.
+     * @return Entry as it exists on the server.
+     */
+    public Entry addEntry(Entry entry) throws Exception {
+        synchronized (FileStore.getFileStore()) {            
+            Feed f = getFeedDocument();
+
+            String fsid = FileStore.getFileStore().getNextId();            
+            updateTimestamps(entry);
+
+            // Save entry to file
+            String entryPath = getEntryPath(fsid);  
+
+            OutputStream os = FileStore.getFileStore().getFileOutputStream(entryPath);
+            updateEntryAppLinks(entry, fsid, true);
+            Atom10Generator.serializeEntry(entry, new OutputStreamWriter(os, "UTF-8"));
+            os.flush();
+            os.close();
+        
+            // Update feed file
+            updateEntryAppLinks(entry, fsid, false);
+            updateFeedDocumentWithNewEntry(f, entry);
+
+            return entry;
+        }
+    }
+    
+    /**
+     * Add media entry to collection. Accepts a media file  to be added to collection. 
+     * The file will be saved to disk in a directory under the collection's directory 
+     * and the path will follow the pattern <code>[collection-plural]/[entryid]/media/[entryid]</code>.
+     * An Atom entry will be created to store metadata for the entry and it will exist 
+     * at the path <code>[collection-plural]/[entryid]/entry.xml</code>.
+     * The entry will be added to the collection's feed in [collection-plural]/feed.xml.
+     * @param entry Entry object
+     * @param slug String to be used in file-name
+     * @param is Source of media data
+     * @throws java.lang.Exception On Error
+     * @return Location URI of entry
+     */
+    public String addMediaEntry(Entry entry, String slug, InputStream is) throws Exception {
+        synchronized (FileStore.getFileStore()) {            
+
+            // Save media file temp file
+            Content content = (Content)entry.getContents().get(0);
+            if (entry.getTitle() == null) {
+                entry.setTitle(slug);
+            }
+            String fileName = createFileName((slug != null) ? slug : entry.getTitle(), content.getType());                 
+            File tempFile = File.createTempFile(fileName, "tmp");
+            FileOutputStream fos = new FileOutputStream(tempFile);
+            Utilities.copyInputToOutput(is, fos);
+            fos.close();
+
+            // Save media file
+            FileInputStream fis = new FileInputStream(tempFile);
+            saveMediaFile(fileName, content.getType(), tempFile.length(), fis);
+            fis.close();
+            File resourceFile = new File(getEntryMediaPath(fileName));
+
+            // Create media-link entry
+            updateTimestamps(entry); 
+
+            // Save media-link entry
+            String entryPath = getEntryPath(fileName);  
+            OutputStream os = FileStore.getFileStore().getFileOutputStream(entryPath);
+            updateMediaEntryAppLinks(entry, resourceFile.getName(), true);
+            Atom10Generator.serializeEntry(entry, new OutputStreamWriter(os, "UTF-8"));
+            os.flush();
+            os.close();
+
+            // Update feed with new entry
+            Feed f = getFeedDocument();
+            updateMediaEntryAppLinks(entry, resourceFile.getName(), false);
+            updateFeedDocumentWithNewEntry(f, entry);
+
+            return getEntryEditURI(fileName, false, true);
+        }
+    }
+    
+    /**
+     * Get an entry from the collection.
+     * @param fsid Internal ID of entry to be returned
+     * @throws java.lang.Exception On error
+     * @return Entry specified by fileName/ID
+     */
+    public Entry getEntry(String fsid) throws Exception {
+        if (fsid.endsWith(".media-link")) {
+            fsid = fsid.substring(0, fsid.length() - ".media-link".length());
+        }
+
+        String entryPath = getEntryPath(fsid); 
+
+        checkExistence(entryPath);
+        InputStream in = FileStore.getFileStore().getFileInputStream(entryPath);
+
+        final Entry entry;
+        String filePath = getEntryMediaPath(fsid);                    
+        File resource = new File(fsid);
+        if (resource.exists()) {
+            entry = loadAtomResourceEntry(in, resource);
+            updateMediaEntryAppLinks(entry, fsid, true);
+        } else {
+            entry = loadAtomEntry(in);
+            updateEntryAppLinks(entry, fsid, true);
+        }
+        return entry;
+    }
+    
+    /**
+     * Get media resource wrapping a file.
+     */
+    public AtomMediaResource getMediaResource(String fileName) throws Exception {
+        String filePath = getEntryMediaPath(fileName);                    
+        File resource = new File(filePath);
+        return new AtomMediaResource(resource);
+    }
+    
+    /**
+     * Update an entry in the collection.
+     * @param entry Updated entry to be stored
+     * @param fsid Internal ID of entry
+     * @throws java.lang.Exception On error
+     */
+    public void updateEntry(Entry entry, String fsid) throws Exception {
+        synchronized (FileStore.getFileStore()) {            
+
+            Feed f = getFeedDocument();
+
+            if (fsid.endsWith(".media-link")) {
+                fsid = fsid.substring(0, fsid.length() - ".media-link".length());
+            }
+
+            updateTimestamps(entry);
+
+            updateEntryAppLinks(entry, fsid, false);
+            updateFeedDocumentWithExistingEntry(f, entry);
+
+            String entryPath = getEntryPath(fsid);
+            OutputStream os = FileStore.getFileStore().getFileOutputStream(entryPath);
+            updateEntryAppLinks(entry, fsid, true);
+            Atom10Generator.serializeEntry(entry, new OutputStreamWriter(os, "UTF-8"));
+            os.flush();
+            os.close();
+        }
+    }
+    
+    /**
+     * Update media associated with a media-link entry.
+     * @param fileName Internal ID of entry being updated
+     * @param contentType Content type of data
+     * @param is Source of updated data
+     * @throws java.lang.Exception On error
+     * @return Updated Entry as it exists on server
+     */
+    public Entry updateMediaEntry(String fileName, String contentType, InputStream is) throws Exception {
+        synchronized (FileStore.getFileStore()) {            
+
+            File tempFile = File.createTempFile(fileName, "tmp");
+            FileOutputStream fos = new FileOutputStream(tempFile);
+            Utilities.copyInputToOutput(is, fos);
+            fos.close();
+
+            // Update media file
+            FileInputStream fis = new FileInputStream(tempFile);
+            saveMediaFile(fileName, contentType, tempFile.length(), fis);
+            fis.close();
+            File resourceFile = new File(getEntryMediaPath(fileName));
+
+            // Load media-link entry to return
+            String entryPath = getEntryPath(fileName);
+            InputStream in = FileStore.getFileStore().getFileInputStream(entryPath);
+            Entry atomEntry = loadAtomResourceEntry(in, resourceFile);
+
+            updateTimestamps(atomEntry);
+            updateMediaEntryAppLinks(atomEntry, fileName, false);
+
+            // Update feed with new entry
+            Feed f = getFeedDocument();
+            updateFeedDocumentWithExistingEntry(f, atomEntry);
+
+            // Save updated media-link entry
+            OutputStream os = FileStore.getFileStore().getFileOutputStream(entryPath);
+            updateMediaEntryAppLinks(atomEntry, fileName, true);
+            Atom10Generator.serializeEntry(atomEntry, new OutputStreamWriter(os, "UTF-8"));
+            os.flush();
+            os.close();  
+
+            return atomEntry;
+        }
+    }
+    
+    /**
+     * Delete an entry and any associated media file.
+     * @param fsid Internal ID of entry
+     * @throws java.lang.Exception On error
+     */
+    public void deleteEntry(String fsid) throws Exception {
+        synchronized (FileStore.getFileStore()) {
+
+            // Remove entry from Feed
+            Feed feed = getFeedDocument();
+            updateFeedDocumentRemovingEntry(feed, fsid);
+
+            String entryFilePath = this.getEntryPath(fsid);
+            FileStore.getFileStore().deleteFile(entryFilePath);
+            
+            String entryMediaPath = this.getEntryMediaPath(fsid);
+            if (entryMediaPath != null) {
+                FileStore.getFileStore().deleteFile(entryMediaPath);
+            }
+            
+            String entryDirPath = getEntryDirPath(fsid);
+            FileStore.getFileStore().deleteDirectory(entryDirPath); 
+            
+            try {Thread.sleep(500L);}catch(Exception ignored){}
+        }
+    }
+    
+    private void updateFeedDocumentWithNewEntry(Feed f, Entry e) throws AtomException {
+        boolean inserted = false;
+        for (int i=0; i<f.getEntries().size(); i++) {
+            Entry entry = (Entry)f.getEntries().get(i);
+            AppModule mod = (AppModule)entry.getModule(AppModule.URI);
+            AppModule newMod = (AppModule)e.getModule(AppModule.URI);
+            if (newMod.getEdited().before(mod.getEdited())) {
+                f.getEntries().add(i, e);
+                inserted = true;
+                break;
+            }
+        }
+        if (!inserted) {
+            f.getEntries().add(0, e);
+        }
+        updateFeedDocument(f);
+    }
+    
+    private void updateFeedDocumentRemovingEntry(Feed f, String id) throws AtomException {
+        Entry e = findEntry("urn:uuid:" + id, f);
+        f.getEntries().remove(e);
+        updateFeedDocument(f);
+    }
+    
+    private void updateFeedDocumentWithExistingEntry(Feed f, Entry e) throws AtomException {
+        Entry old = findEntry(e.getId(), f);
+        f.getEntries().remove(old);
+        
+        boolean inserted = false;
+        for (int i=0; i<f.getEntries().size(); i++) {
+            Entry entry = (Entry)f.getEntries().get(i);
+            AppModule entryAppModule = (AppModule)entry.getModule(AppModule.URI);
+            AppModule eAppModule = (AppModule)entry.getModule(AppModule.URI);
+            if (eAppModule.getEdited().before(entryAppModule.getEdited())) {
+                f.getEntries().add(i, e);
+                inserted = true;
+                break;
+            }
+        }
+        if (!inserted) {
+            f.getEntries().add(0, e);
+        }
+        updateFeedDocument(f);
+    }
+        
+    private Entry findEntry(String id, Feed f) {
+        List l = f.getEntries();
+        for (Iterator it = l.iterator(); it.hasNext();) {
+            Entry e = (Entry)it.next();
+            if (id.equals(e.getId()))
+                return e;
+        }
+        
+        return null;
+    }
+       
+    private void updateFeedDocument(Feed f) throws AtomException{
+        try {
+            synchronized(FileStore.getFileStore()) {                
+                WireFeedOutput wireFeedOutput = new WireFeedOutput();
+                Document feedDoc = wireFeedOutput.outputJDom( f );
+                XMLOutputter outputter = new XMLOutputter();                
+                //outputter.setFormat(Format.getPrettyFormat());
+                OutputStream fos = FileStore.getFileStore().getFileOutputStream(getFeedPath());
+                outputter.output(feedDoc, new OutputStreamWriter(fos, "UTF-8"));
+            }
+        } catch (FeedException fex) {
+            throw new AtomException(fex);
+        }catch (IOException ex) {
+            throw new AtomException(ex);
+        }
+    }
+          
+    private InputStream createDefaultFeedDocument(String uri) throws AtomException {
+        
+        Feed f = new Feed();
+        f.setTitle("Feed");
+        f.setId(uri);
+        f.setFeedType( FEED_TYPE);
+        
+        Link selfLink = new Link();
+        selfLink.setRel("self");
+        selfLink.setHref(uri);
+        f.getOtherLinks().add(selfLink);
+                
+        try {
+            WireFeedOutput wireFeedOutput = new WireFeedOutput();
+            Document feedDoc = wireFeedOutput.outputJDom( f );
+            XMLOutputter outputter = new XMLOutputter();
+            //outputter.setFormat(Format.getCompactFormat());
+            OutputStream fos = FileStore.getFileStore().getFileOutputStream(getFeedPath());
+            outputter.output(feedDoc, new OutputStreamWriter(fos, "UTF-8")); 
+            
+        } catch (FeedException ex) {
+            throw new AtomException(ex);
+        } catch (IOException ex) {
+            throw new AtomException(ex);
+        } catch ( Exception e ) {
+            
+            e.printStackTrace();
+        }
+        return FileStore.getFileStore().getFileInputStream(getFeedPath());
+    }
+    
+    
+    private Entry loadAtomResourceEntry(InputStream in, File file) {
+        try {
+            Entry entry = Atom10Parser.parseEntry(
+                new BufferedReader(new InputStreamReader(in)), null);
+            updateMediaEntryAppLinks(entry, file.getName(), true);
+            return entry;
+            
+        } catch ( Exception e ) {
+            e.printStackTrace();
+            return null;
+        }    
+    }
+    
+    private void updateEntryAppLinks(Entry entry, String fsid, boolean singleEntry) {
+                             
+        entry.setId("urn:uuid:" + fsid);
+        
+        // Look for existing alt links and the alt link
+        Link altLink = null;
+        List altLinks = entry.getAlternateLinks();
+        if (altLinks != null) {
+            for (Iterator it = altLinks.iterator(); it.hasNext();) {
+                Link link = (Link)it.next();
+                if (link.getRel() == null || "alternate".equals(link.getRel())) {
+                    altLink = link;
+                    break;
+                }
+            }
+        } else {
+            // No alt links found, so add them now
+            altLinks = new ArrayList();
+            entry.setAlternateLinks(altLinks);
+        }
+        // The alt link not found, so add it now
+        if (altLink == null) {
+            altLink = new Link();
+            altLinks.add(altLink);
+        }       
+        // Set correct value for the alt link
+        altLink.setRel("alternate");
+        altLink.setHref(getEntryViewURI(fsid));        
+        
+        // Look for existing other links and the edit link
+        Link editLink = null;
+        List otherLinks = entry.getOtherLinks();
+        if (otherLinks != null) {
+            for (Iterator it = otherLinks.iterator(); it.hasNext();) {
+                Link link = (Link)it.next();
+                if ("edit".equals(link.getRel())) {
+                    editLink = link;
+                    break;
+                }
+            }
+        } else {
+            // No other links found, so add them now
+            otherLinks = new ArrayList();
+            entry.setOtherLinks(otherLinks);
+        }
+        // The edit link not found, so add it now
+        if (editLink == null) {
+            editLink = new Link();
+            otherLinks.add(editLink);
+        }       
+        // Set correct value for the edit link
+        editLink.setRel("edit");
+        editLink.setHref(getEntryEditURI(fsid, relativeURIs, singleEntry));
+    }    
+    
+    private void updateMediaEntryAppLinks(Entry entry, String fileName, boolean singleEntry) {
+                        
+        // TODO: figure out why PNG is missing from Java MIME types
+        FileTypeMap map = FileTypeMap.getDefaultFileTypeMap();
+        if (map instanceof MimetypesFileTypeMap) {
+            try {
+                ((MimetypesFileTypeMap)map).addMimeTypes("image/png png PNG");
+            } catch (Exception ignored) {}
+        }
+        String contentType = map.getContentType(fileName);
+        
+        entry.setId(getEntryMediaViewURI(fileName));
+        entry.setTitle(fileName);
+        entry.setUpdated(new Date());
+        
+        List otherlinks = new ArrayList();
+        entry.setOtherLinks(otherlinks);
+        
+        Link editlink = new Link();
+        editlink.setRel("edit");
+        editlink.setHref(getEntryEditURI(fileName, relativeURIs, singleEntry));
+        otherlinks.add(editlink);
+        
+        Link editMedialink = new Link();
+        editMedialink.setRel("edit-media");
+        editMedialink.setHref(getEntryMediaEditURI(fileName, relativeURIs, singleEntry));
+        otherlinks.add(editMedialink);
+        
+        Content content = (Content)entry.getContents().get(0);
+        content.setSrc(getEntryMediaViewURI(fileName));
+        List contents = new ArrayList();
+        contents.add(content);
+        entry.setContents(contents);
+    }
+    
+    /**
+     * Create a Rome Atom entry based on a Roller entry.
+     * Content is escaped.
+     * Link is stored as rel=alternate link.
+     */    
+    private Entry loadAtomEntry(InputStream in)   {
+        try {
+            return Atom10Parser.parseEntry(
+                new BufferedReader(new InputStreamReader(in, "UTF-8")), null);
+        } catch ( Exception e ) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+        
+    /** 
+     * Update existing or add new app:edited.
+     */
+    private void updateTimestamps(Entry entry) {
+         // We're not differenting between an update and an edit (yet)
+        entry.setUpdated(new Date());
+        
+        AppModule appModule = (AppModule)entry.getModule(AppModule.URI);
+        if (appModule == null) {
+            appModule = new AppModuleImpl();
+            List modules = entry.getModules()==null ? new ArrayList() : entry.getModules();
+            modules.add(appModule);
+            entry.setModules(modules);           
+        }
+        appModule.setEdited(entry.getUpdated());
+    }
+    
+    
+    /**
+     * Save file to website's resource directory.
+     * @param handle Weblog handle to save to
+     * @param name Name of file to save
+     * @param size Size of file to be saved
+     * @param is Read file from input stream
+     */
+    private void saveMediaFile(
+        String name, String contentType, long size, InputStream is) throws AtomException {
+                
+        byte[] buffer = new byte[8192];
+        int bytesRead = 0;
+        
+        File dirPath = new File(getEntryMediaPath(name));
+        if (!dirPath.getParentFile().exists()) {
+            dirPath.getParentFile().mkdirs();
+        }
+        OutputStream bos = null;
+        try {
+            bos = new FileOutputStream(dirPath.getAbsolutePath());
+            while ((bytesRead = is.read(buffer, 0, 8192)) != -1) {
+                bos.write(buffer, 0, bytesRead);
+            }
+        } catch (Exception e) {
+            throw new AtomException("ERROR uploading file", e);
+        } finally {
+            try {
+                bos.flush();
+                bos.close();
+            } catch (Exception ignored) {}
+        }
+        
+    }  
+    
+    /**
+     * Creates a file name for a file based on a weblog handle, title string and a
+     * content-type.
+     *
+     * @param handle      Weblog handle
+     * @param title       Title to be used as basis for file name (or null)
+     * @param contentType Content type of file (must not be null)
+     *
+     * If a title is specified, the method will apply the same create-anchor
+     * logic we use for weblog entries to create a file name based on the title.
+     *
+     * If title is null, the base file name will be the weblog handle plus a
+     * YYYYMMDDHHSS timestamp.
+     *
+     * The extension will be formed by using the part of content type that
+     * comes after he slash.
+     *
+     * For example:
+     *    weblog.handle = "daveblog"
+     *    title         = "Port Antonio"
+     *    content-type  = "image/jpg"
+     * Would result in port_antonio.jpg
+     *
+     * Another example:
+     *    weblog.handle = "daveblog"
+     *    title         = null
+     *    content-type  = "image/jpg"
+     * Might result in daveblog-200608201034.jpg
+     */
+    private String createFileName(String title, String contentType) {
+        
+        if (handle == null) throw new IllegalArgumentException("weblog handle cannot be null");
+        if (contentType == null) throw new IllegalArgumentException("contentType cannot be null");
+        
+        String fileName = null;
+        
+        SimpleDateFormat sdf = new SimpleDateFormat();
+        sdf.applyPattern("yyyyMMddHHssSSS");
+        
+        // Determine the extension based on the contentType. This is a hack.
+        // The info we need to map from contentType to file extension is in
+        // JRE/lib/content-type.properties, but Java Activation doesn't provide
+        // a way to do a reverse mapping or to get at the data.
+        String[] typeTokens = contentType.split("/"); 
+        String ext = typeTokens[1];
+        
+        if (title != null && !title.trim().equals("")) {
+            // We've got a title, so use it to build file name
+            String base = Utilities.replaceNonAlphanumeric(title, ' ');
+            StringTokenizer toker = new StringTokenizer(base);
+            String tmp = null;
+            int count = 0;
+            while (toker.hasMoreTokens() && count < 5) {
+                String s = toker.nextToken();
+                s = s.toLowerCase();
+                tmp = (tmp == null) ? s : tmp + "_" + s;
+                count++;
+            }
+            fileName = tmp + "-" + sdf.format(new Date()) + "." + ext;
+            
+        } else {
+            // No title or text, so instead we'll use the item's date
+            // in YYYYMMDD format to form the file name
+            fileName = handle + "-" + sdf.format(new Date()) + "." + ext;
+        }
+        
+        return fileName;
+    }
+    
+    //------------------------------------------------------------ URI methods
+    
+    private String getEntryEditURI(String fsid, boolean relative, boolean singleEntry) {
+        String entryURI = null;
+        if (relative) {
+            if (singleEntry) {
+                entryURI = fsid;
+            } else {
+                entryURI = singular + "/" + fsid;
+            }
+        } else {
+            entryURI = contextURI + servletPath + "/" + handle + "/" + singular + "/" + fsid;
+        }
+        return entryURI;
+    }
+    
+    private String getEntryViewURI(String fsid) {
+        return contextURI + "/" + handle + "/" + collection + "/" + fsid + "/entry.xml";
+    }
+        
+    private String getEntryMediaEditURI(String fsid, boolean relative, boolean singleEntry) {
+        String entryURI = null;
+        if (relative) {
+            if (singleEntry) {
+                entryURI = "media/" + fsid;
+            } else {
+                entryURI = singular + "/media/" + fsid;
+            }
+        } else {
+            entryURI = contextURI + servletPath + "/" + handle + "/" + singular + "/media/" + fsid; 
+        }
+        return entryURI;
+              
+    }
+    
+    private String getEntryMediaViewURI(String fsid) {
+        return contextURI + "/" + handle + "/" + collection + "/" + fsid + "/media/" + fsid;        
+    }
+    
+    private String getCategoriesURI() {
+        if (relativeURIs) {
+            return contextURI + servletPath + "/" + handle + "/" + singular + "/categories";
+        } else {
+            return servletPath + "/" + handle + "/" + singular + "/categories";
+        }
+    }
+
+    //------------------------------------------------------- File path methods
+    
+    private String getBaseDir() {
+        return baseDir;
+    }
+        
+    private String getFeedPath() {
+        return getBaseDir() + handle  
+            +  File.separator + collection + File.separator+ "feed.xml";
+    }
+        
+    private String getEntryDirPath(String id) {
+        return  getBaseDir() + handle 
+            + File.separator + collection + File.separator + id;
+    }
+    
+    private String getEntryPath(String id) {
+        return getEntryDirPath(id) + File.separator + "entry.xml";
+    }
+    
+    private String getEntryMediaPath(String id) {
+        return getEntryDirPath(id) + File.separator + "media" + File.separator + id;
+    }
+    
+    private static void checkExistence(String path) throws AtomNotFoundException{
+        if (!FileStore.getFileStore().exists(path)) {
+            throw new AtomNotFoundException("Entry does not exist");
+        }
+    }
+                       
+}
diff --git a/src/main/java/org/rometools/propono/atom/server/impl/FileBasedWorkspace.java b/src/main/java/org/rometools/propono/atom/server/impl/FileBasedWorkspace.java
new file mode 100644
index 0000000..767d490
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/impl/FileBasedWorkspace.java
@@ -0,0 +1,34 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.server.impl;
+
+import org.rometools.propono.atom.common.Workspace;
+
+/**
+ * File based Atom service Workspace.
+ */
+public class FileBasedWorkspace extends Workspace {
+    private String baseDir = null;
+    private String handle = null;
+    
+    /** Creates a new instance of FileBasedWorkspace */
+    public FileBasedWorkspace(String handle, String baseDir) {
+        super(handle, "text");
+        this.handle = handle;
+        this.baseDir = baseDir;
+    }
+    
+}
diff --git a/src/main/java/org/rometools/propono/atom/server/impl/FileStore.java b/src/main/java/org/rometools/propono/atom/server/impl/FileStore.java
new file mode 100644
index 0000000..4f559dc
--- /dev/null
+++ b/src/main/java/org/rometools/propono/atom/server/impl/FileStore.java
@@ -0,0 +1,116 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.server.impl;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.commons.lang.RandomStringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Class which helps in handling File persistence related operations.
+ */
+class FileStore {
+    
+    private static Log log =
+        LogFactory.getFactory().getInstance(FileStore.class);
+    
+    private static final FileStore fileStore = new FileStore();
+    
+    public String getNextId() {
+        //return UUID.randomUUID().toString(); // requires JDK 1.5
+        return RandomStringUtils.randomAlphanumeric(20);
+    }
+    
+    public boolean exists(String path) {
+        File f = new File(path);
+        return f.exists();
+    }
+    
+    public InputStream getFileInputStream(String path) {
+        log.debug("getFileContents path: " + path);
+        try {
+            return new BufferedInputStream(new FileInputStream(path));
+        } catch (FileNotFoundException e) {
+            log.debug("   File not found: " + path);
+            return null;
+        }
+    }
+    
+    public OutputStream getFileOutputStream(String path) {
+        log.debug("getFileOutputStream path: " + path);
+        try {
+            File f = new File(path);
+            f.getParentFile().mkdirs();
+            return new BufferedOutputStream(new FileOutputStream(f));
+        } catch (FileNotFoundException e) {
+            log.debug("   File not found: " + path);
+            return null;
+        }
+    }
+    
+    public void createOrUpdateFile(String path, InputStream content) throws FileNotFoundException, IOException {
+        log.debug("createOrUpdateFile path: " + path);
+        File f = new File(path);
+        f.mkdirs();
+        FileOutputStream out = new FileOutputStream(f);
+        
+        byte[] buffer = new byte[2048];
+        int read;
+        while ((read = content.read(buffer)) != -1) {
+            out.write(buffer, 0, read);
+        }
+    }
+    
+    public void deleteFile(String path) {
+        log.debug("deleteFile path: " + path);
+        File f = new File(path);
+        if (!f.delete()) {
+            log.debug("   Failed to delete: " + f.getAbsolutePath());
+        }
+    }
+
+    public static FileStore getFileStore() {
+        return fileStore;
+    }
+    
+    public boolean deleteDirectory(String path) {
+        return deleteDirectory(new File(path));
+    }
+    
+    public boolean deleteDirectory(File path) {
+        log.debug("deleteDirectory path: " + path);
+        if( path.exists() ) {
+            File[] files = path.listFiles();
+            for(int i=0; i<files.length; i++) {
+                if(files[i].isDirectory()) {
+                    deleteDirectory(files[i]);
+                } else {
+                    files[i].delete();
+                }
+            }
+        }
+        return( path.delete() );
+    }
+}
diff --git a/src/main/java/org/rometools/propono/blogclient/BaseBlogEntry.java b/src/main/java/org/rometools/propono/blogclient/BaseBlogEntry.java
new file mode 100644
index 0000000..61f89ac
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/BaseBlogEntry.java
@@ -0,0 +1,193 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/** 
+ * Base implementation of a blog entry.
+ */
+public abstract class BaseBlogEntry implements BlogEntry {
+    protected String  id = null;
+    protected Person  author = null;
+    protected Content content = null;
+    protected String  title = null;
+    protected String  permalink = null;
+    protected String  summary = null;
+    protected Date    modificationDate = null;
+    protected Date    publicationDate = null;
+    protected List    categories = new ArrayList();
+    protected boolean draft = false;
+    private Blog      blog = null;
+    
+    /**
+     * Contruct abstract blog entry.
+     */
+    public BaseBlogEntry(Blog blog) {
+        this.blog = blog;
+    }   
+
+    
+    /**
+     * {@inheritDoc}
+     */
+    public String getId() {
+        return id;
+    }    
+    
+    /**
+     * {@inheritDoc}
+     */
+    public String getPermalink() {
+        return permalink;
+    }      
+    
+    void setPermalink(String permalink) {
+        this.permalink = permalink;
+    }      
+    
+    /**
+     * {@inheritDoc}
+     */
+    public Person getAuthor() {
+        return author;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public void setAuthor(Person author) {
+        this.author = author;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public Content getContent() {
+        return content;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public void setContent(Content content) {
+        this.content = content;
+    }    
+    
+    /**
+     * {@inheritDoc}
+     */
+    public boolean getDraft() {
+        return draft;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public void setDraft(boolean draft) {
+        this.draft = draft;
+    }    
+    
+    /**
+     * {@inheritDoc}
+     */
+    public Date getPublicationDate() {
+        return publicationDate;
+    }    
+    
+    /**
+     * {@inheritDoc}
+     */
+    public void setPublicationDate(Date pubDate) {
+        this.publicationDate = pubDate;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public Date getModificationDate() {
+        return modificationDate;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public void setModificationDate(Date date) {
+        modificationDate = date;
+    }        
+    
+    /**
+     * {@inheritDoc}
+     */
+    public String getTitle() {
+        return title;
+    }    
+    
+    /**
+     * {@inheritDoc}
+     */
+    public void setTitle(String title) {
+        this.title = title;
+    }   
+    
+    /**
+     * {@inheritDoc}
+     */
+    public String getSummary() {
+        return summary;
+    }    
+    
+    /**
+     * {@inheritDoc}
+     */
+    public void setSummary(String summary) {
+        this.summary = summary;
+    }    
+    
+    /**
+     * {@inheritDoc}
+     */
+    public List getCategories() {
+        return categories;
+    }   
+    
+    /**
+     * {@inheritDoc}
+     */
+    public void setCategories(List categories) {
+        this.categories = categories;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public Blog getBlog() {
+        return blog;
+    }
+    
+    void setBlog(Blog blog) {
+        this.blog = blog;
+    }
+    
+    /**
+     * String representation, returns id. 
+     */
+    public String toString() {
+        return id;
+    }
+}
diff --git a/src/main/java/org/rometools/propono/blogclient/Blog.java b/src/main/java/org/rometools/propono/blogclient/Blog.java
new file mode 100644
index 0000000..2512ccb
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/Blog.java
@@ -0,0 +1,213 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient;
+
+import java.util.List;
+import java.util.Iterator;
+
+/**
+ * <p>Represents a blog, which has collections of entries and resources.
+ * You can access the collections using the getCollections() and 
+ * getCollection(String name) methods, which return Blog.Collection objects,
+ * which you can use to create, retrieve update or delete entries within 
+ * a collection.</p>
+ */
+public interface Blog {
+    
+    /**
+     * Token can be used to fetch this blog again from getBlog() method.
+     * @return Blog object specified by token.
+     */
+    public String getToken();
+    
+    /**
+     * Name of this blog.
+     * @return Display name of this blog.
+     */
+    public String getName();
+        
+    /**
+     * Get a single BlogEntry (or BlogResource) by token.
+     * @param token Token from blog entry's getToken() method.
+     * @throws com.sun.syndication.propono.blogclient.BlogClientException On error fetching the blog entry.
+     * @return Blog entry specified by token.
+     */
+    public BlogEntry getEntry(String token) throws BlogClientException;
+    
+    /**
+     * Gets listing of entry and resource collections available in the blog,
+     * including the primary collections.
+     * @throws BlogClientException On error fetching collections.
+     * @return List of Blog.Collection objects.
+     */
+    public List getCollections() throws BlogClientException;
+    
+    /**
+     * Get collection by token.
+     * @param token Token from a collection's getToken() method.
+     * @throws BlogClientException On error fetching collection.
+     * @return Blog.Collection object.
+     */
+    public Collection getCollection(String token) throws BlogClientException;
+            
+    /**
+     * Represents an entry or resource collection on a blog server.
+     */
+    public interface Collection {
+        
+        /**
+         * Get blog that contains this collection.
+         * @return Blog that contains this collection.
+         */
+        public Blog getBlog();
+        
+        /**
+         * Title of collection.
+         * @return Title of collecton.
+         */
+        public String getTitle();
+        
+        /**
+         * Token that can be used to fetch collection.
+         * @return Token that can be used to fetch collection.
+         */
+        public String getToken();
+        
+        /**
+         * Content-types accepted by collection.
+         * @return Comma-separated list of content-types accepted.
+         */
+        public List getAccepts();
+        
+        /**
+         * Determines if collection will accept a content-type.
+         * @param contentType Content-type to be considered.
+         * @return True of content type will be accepted, false otherwise.
+         */
+        public boolean accepts(String contentType);
+        
+        /**
+         * Return categories allowed by colletion.
+         * @throws BlogClientException On error fetching categories.
+         * @return List of BlogEntry.Category objects for this collection.
+         */
+        public List getCategories() throws BlogClientException;
+        
+        /**
+         * Create but do not save new entry in collection. 
+         * To save entry, call its save() method.
+         * @throws BlogClientException On error creating entry.
+         * @return New BlogEntry object.
+         */
+        public BlogEntry newEntry() throws BlogClientException;
+        
+        /**
+         * Create but do not save new resource in collection. 
+         * To save resource, call its save() method.
+         * @param name        Name of new resource.
+         * @param contentType MIME content-type of new resource.
+         * @param bytes       Data for new resource.
+         * @throws BlogClientException On error creating entry.
+         * @return New BlogResource object,
+         */
+        public BlogResource newResource(String name, String contentType, byte[] bytes) throws BlogClientException;
+        
+        /**
+         * Get iterator over entries/resources in this collection.
+         * @return List of BlogEntry objects, some may be BlogResources.
+         * @throws BlogClientException On error fetching entries/resources.
+         */
+        public Iterator getEntries() throws BlogClientException;
+        
+        /**
+         * Save or update a BlogEntry in this collection by adding it to this
+         * collection and then calling it's entry.save() method.
+         * @param entry BlogEntry to be saved.
+         * @throws BlogClientException On error saving entry.
+         * @return URI of entry.
+         */
+        public String saveEntry(BlogEntry entry) throws BlogClientException;
+
+        /**
+         * Save or update resource in this collection
+         * @param resource BlogResource to be saved.
+         * @throws BlogClientException On error saving resource.
+         * @return URI of resource.
+         */
+        public String saveResource(BlogResource resource) throws BlogClientException;
+    }
+    
+    
+    // Deprecated primary collection methods, instead use collections directly.
+    
+    /**
+     * Get iterator over entries in primary entries collection (the first 
+     * collection that accepts entries). Note that entries may be partial, 
+     * so don't try to update and save them: to update and entry, first fetch
+     * it with getEntry(), change fields, then call entry.save();
+     * @return To iterate over all entries in collection.
+     * @throws BlogClientException On failure or if there is no primary entries collection.
+     *
+     * @deprecated Instead use collections directly.
+     */
+    public Iterator getEntries() throws BlogClientException;
+
+    /**
+     * Get entries in primary resources collection (the first 
+     * collection that accepts anything other than entries).
+     * @throws BlogClientException On failure or if there is no primary resources collection.
+     * @return To iterate over all resojrces in collection.
+     *
+     * @deprecated Instead use collections directly.
+     */
+    public Iterator getResources() throws BlogClientException;
+
+    
+    /**
+     * Create but do not save it to server new BlogEntry in primary entries collection
+     * (the first collection found that accepts entries). To save the entry to the 
+     * server to a collection, use the entry's save() method.
+     * @throws BlogClientException On error or if there is no primary entries collection.
+     * @return Unsaved BlogEntry in primary entries collection.
+     *
+     * @deprecated Instead use collections directly.
+     */
+    public BlogEntry newEntry() throws BlogClientException;
+
+    /**
+     * Create but do not save it to server new BlogResource in primary resources collection
+     * (the first collection found that accepts resources). To save the resource to the 
+     * server to a collection, use the resource's save() method.
+     * @param name  Name of resource to be saved.
+     * @param type  MIME content type of resource data.
+     * @param bytes Bytes of resource data.
+     * @throws BlogClientException On error or if there is no primary respurces collection.
+     * @return Unsaved BlogEntry in primary resources collection.
+     *
+     * @deprecated Instead use collections directly.
+     */
+    public BlogResource newResource(String name, String type, byte[] bytes) 
+        throws BlogClientException;
+
+    /**
+     * Returns list of available BlogEntry.Category in primary entries collection.
+     * @throws BlogClientException On error or if there is no primary entries collection.
+     * @return List of BlogEntry.Category objects.
+     *
+     * @deprecated Instead use collections directly.
+     */
+    public List getCategories() throws BlogClientException;    
+}
diff --git a/src/main/java/org/rometools/propono/blogclient/BlogClientException.java b/src/main/java/org/rometools/propono/blogclient/BlogClientException.java
new file mode 100644
index 0000000..d0b304a
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/BlogClientException.java
@@ -0,0 +1,40 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient;
+
+/**
+ * Represents a Blog Client exception, the library throws these instead of 
+ * implementation specific exceptions.
+ */
+public class BlogClientException extends Exception {
+    
+    /** 
+     * Construct a new exception 
+     * @param msg Text message that explains exception
+     */
+    public BlogClientException(String msg) {
+        super(msg);
+    }
+    
+    /** 
+     * Construct a new exception which wraps a throwable.
+     * @param msg Text message that explains exception
+     * @param t   Throwable to be wrapped by exception
+     */
+    public BlogClientException(String msg, Throwable t) {
+        super(msg, t);
+    }
+}
diff --git a/src/main/java/org/rometools/propono/blogclient/BlogConnection.java b/src/main/java/org/rometools/propono/blogclient/BlogConnection.java
new file mode 100644
index 0000000..817ebc6
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/BlogConnection.java
@@ -0,0 +1,34 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient;
+
+import java.util.List;
+
+/**
+ * A BlogConnection is a single-user connection to a blog server where the user 
+ * has access to multiple blogs, which are each represented by a Blog interface.
+ */
+public interface BlogConnection {
+    
+    /** Returns collection of blogs available from this connection */
+    public abstract List getBlogs();
+    
+    /** Get blog by token */
+    public abstract Blog getBlog(String token);
+    
+    /** Set appkey (optional, needed by some blog servers) */
+    public void setAppkey(String appkey);
+}
\ No newline at end of file
diff --git a/src/main/java/org/rometools/propono/blogclient/BlogConnectionFactory.java b/src/main/java/org/rometools/propono/blogclient/BlogConnectionFactory.java
new file mode 100644
index 0000000..1e8e5b7
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/BlogConnectionFactory.java
@@ -0,0 +1,80 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient;
+
+import java.lang.reflect.Constructor;
+
+/**
+ * Entry point to the Blogapps blog client library.
+ */
+public class BlogConnectionFactory { 
+    
+    // BlogConnection implementations must:
+    // 1) implement BlogConnection
+    // 2) privide contructor that accepts three strings args: url, username and password.
+    
+    // TODO: make implementations configurable
+    private static String ATOMPROTOCOL_IMPL_CLASS = 
+        "com.sun.syndication.propono.blogclient.atomprotocol.AtomConnection";
+    
+    private static String METAWEBLOG_IMPL_CLASS = 
+        "com.sun.syndication.propono.blogclient.metaweblog.MetaWeblogConnection";
+            
+   /**
+    * Create a connection to a blog server.
+    * @param type     Connection type, must be "atom" or "metaweblog"
+    * @param url      End-point URL to connect to
+    * @param username Username for login to blog server
+    * @param password Password for login to blog server
+    */
+   public static BlogConnection getBlogConnection(
+      String type, String url, String username, String password) 
+      throws BlogClientException {
+      BlogConnection blogConnection = null;
+      if (type == null || type.equals("metaweblog")) {
+         blogConnection = createBlogConnection(
+                 METAWEBLOG_IMPL_CLASS, url, username, password);
+      } else if (type.equals("atom")) {
+         blogConnection = createBlogConnection(
+                 ATOMPROTOCOL_IMPL_CLASS, url, username, password);
+      } else {
+          throw new BlogClientException("Type must be 'atom' or 'metaweblog'");
+      }
+      return blogConnection;
+   }
+   
+   private static BlogConnection createBlogConnection(
+           String className, String url, String username, String password)
+           throws BlogClientException {
+       Class conClass;
+       try {
+           conClass = Class.forName(className);
+       } catch (ClassNotFoundException ex) {
+           throw new BlogClientException(
+                   "BlogConnection impl. class not found: "+className, ex);
+       }
+       Class[] args = new Class[] {String.class, String.class, String.class};
+       Constructor ctor;
+       try {
+           ctor = conClass.getConstructor(args);
+           return (BlogConnection)
+           ctor.newInstance(new Object[] {url, username, password});
+       } catch (Throwable t) {
+           throw new BlogClientException(
+                   "ERROR instantiating BlogConnection impl.", t);
+       }
+   }
+}
diff --git a/src/main/java/org/rometools/propono/blogclient/BlogEntry.java b/src/main/java/org/rometools/propono/blogclient/BlogEntry.java
new file mode 100644
index 0000000..7bf2c1d
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/BlogEntry.java
@@ -0,0 +1,231 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient;
+
+import java.util.Date;
+import java.util.List;
+import java.io.InputStream;
+        
+/** 
+ * Represents a single blog entry.
+ */
+public interface BlogEntry {
+    
+    /** Get token, which can be used to fetch the blog entry */
+    public String getToken();
+    
+    /** 
+     * Save this entry to it's collection. If this is a new entry and does not
+     * have a collection yet, then save() will save it to the primary collection.
+     */
+    public void save() throws BlogClientException;
+    
+    /** Delete this entry from blog server */
+    public void delete() throws BlogClientException;
+
+    /** Permanent link to this entry (assigned by server) */
+    public String getPermalink(); 
+    
+    /** Blog is associated with a blog */
+    public Blog getBlog();
+    
+    /** Get categories, a list of BlogEntry.Category objects */
+    public List getCategories();
+    
+    /** Set categories, a list of BlogEntry.Category objects */
+    public void setCategories(List categories);
+
+    /** Get globally unique ID of this blog entry */
+    public String getId();
+
+    /** Get title of this blog entry */
+    public String getTitle();
+
+    /** Set title of this blog entry */
+    public void setTitle(String title);
+    
+    /** Get summary of this blog entry */
+    public String getSummary();
+    
+    /** Set summary of this blog entry */
+    public void setSummary(String summary);
+    
+    /** Get content of this blog entry */
+    public Content getContent();
+
+    /** Set content of this blog entry */
+    public void setContent(Content content);
+        
+    /** Get draft status of this entry */
+    public boolean getDraft();
+
+    /** Set draft status of this entry */
+    public void setDraft(boolean draft);
+    
+    /** Get author of this entry */
+    public Person getAuthor();
+    
+    /** Set author of this entry */
+    public void setAuthor(Person author);
+ 
+    /** Set publish date of this entry */
+    public Date getPublicationDate();
+    
+    /** Get publish date of this entry */
+    public void setPublicationDate(Date date);   
+    
+    /** Get update date of this entry */
+    public Date getModificationDate();
+    
+    /** Set update date of this entry */
+    public void setModificationDate(Date date);
+    
+    /** Represents blog entry content */
+    public class Content {
+        String type = "html";
+        String value = null;
+        String src = null;
+        
+        /** Construct content */
+        public Content() {}
+        
+        /** Construct content with value (and type="html") */
+        public Content(String value) {
+            this.value = value;
+        }
+        /** Get value of content if in-line */
+        public String getValue() {
+            return value;
+        }
+        /** Set value of content if in-line */
+        public void setValue(String value) {
+            this.value = value;
+        }
+        /**
+         * Get type of content, either "text", "html", "xhtml" or a MIME content-type.
+         * Defaults to HTML.
+         */
+        public String getType() {
+            return type;
+        }
+        /**
+         * Set type of content, either "text", "html", "xhtml" or a MIME content-type.
+         * Defaults to HTML.
+         */
+        public void setType(String type) {
+            this.type = type;
+        }
+        /** Get URI of content if out-of-line */
+        public String getSrc() {
+            return src;
+        }
+        /** Set URI of content if out-of-line */
+        public void setSrc(String src) {
+            this.src = src;
+        }
+    }
+
+    /** Represents a blog author or contributor */
+    public class Person {
+        String name;
+        String email;
+        String url;
+        /** Get person's email */
+        public String getEmail() {
+            return email;
+        }
+        /** Set person's email  */
+        public void setEmail(String email) {
+            this.email = email;
+        }
+        /** Get person's name */
+        public String getName() {
+            return name;
+        }
+        /** Set person's name */
+        public void setName(String name) {
+            this.name = name;
+        }
+        /** Get person's URL */
+        public String getUrl() {
+            return url;
+        }
+        /** Set person's URL */
+        public void setUrl(String url) {
+            this.url = url;
+        }
+        /** Returns person's name */
+        public String toString() {
+            return name;
+        }
+    }
+    
+    /** Represents a weblog category */
+    public class Category {
+        String id;
+        String name;
+        String url;
+        /**
+         * Create new Catetory
+         */
+        public Category() {}
+        /**
+         * Create new category with name.
+         */
+        public Category(String id) {
+            this.id = id;
+            this.name = id;
+        }
+        /**
+         * Determines if categories are equal based on id.
+         */
+        public boolean equals(Object obj) {
+            Category other = (Category)obj;
+            if (obj == null) return false;
+            if (getId() != null && other.getId() != null 
+                    && getId().equals(other.getId())) return true;
+            return false;
+        }
+        /** Get category id */
+        public String getId() {
+            return id;
+        }
+        /** Set category id */
+        public void setId(String id) {
+            this.id = id;
+        }
+        /** Get category display name */
+        public String getName() {
+            return name;
+        }
+        /** Set category display name */
+        public void setName(String name) {
+            this.name = name;
+        }
+        /** Get URL of category domain */
+        public String getUrl() {
+            return url;
+        }
+        /** Set URL of category domain */
+        public void setUrl(String url) {
+            this.url = url;
+        }
+        /** Return category's name or id for display */
+        public String toString() {
+            return name!=null ? name : id;
+        }
+    }    
+}
diff --git a/src/main/java/org/rometools/propono/blogclient/BlogResource.java b/src/main/java/org/rometools/propono/blogclient/BlogResource.java
new file mode 100644
index 0000000..80a5478
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/BlogResource.java
@@ -0,0 +1,39 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient;
+
+import java.io.InputStream;
+
+/** 
+ * Represents a file that has been uploaded to a blog. 
+ * <p />
+ * Resources are modeled as a type of BlogEntry, but be aware: not all servers 
+ * can save resource metadata (i.e. title, category, author, etc.). MetaWeblog
+ * based servers can't save metadata at all and Atom protocol servers are not
+ * required to preserve uploaded file metadata.
+ */
+public interface BlogResource extends BlogEntry {
+
+    /** Get resource name (name is required) */
+    public String getName();
+        
+    /** Get resource as stream, using content.src as URL */
+    public InputStream getAsStream() throws BlogClientException;
+       
+    /** Update resource by immediately uploading new bytes to server */
+    public void update(byte[] newBytes) throws BlogClientException;
+}
+
diff --git a/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomBlog.java b/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomBlog.java
new file mode 100644
index 0000000..3565775
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomBlog.java
@@ -0,0 +1,206 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient.atomprotocol;
+
+import org.rometools.propono.utils.ProponoException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.rometools.propono.blogclient.Blog;
+import org.rometools.propono.blogclient.BlogClientException;
+import org.rometools.propono.blogclient.BlogEntry;
+import org.rometools.propono.blogclient.BlogResource;
+import org.rometools.propono.atom.client.ClientAtomService;
+import org.rometools.propono.atom.client.ClientCollection;
+import org.rometools.propono.atom.client.ClientEntry;
+import org.rometools.propono.atom.client.ClientMediaEntry;
+import org.rometools.propono.atom.client.ClientWorkspace;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Atom protocol implementation of the BlogClient Blog interface.
+ */
+public class AtomBlog implements Blog {
+    static final Log logger = LogFactory.getLog(AtomBlog.class);
+    private HttpClient        httpClient = null;
+    private String            name = null;
+    private ClientAtomService service;
+    private ClientWorkspace   workspace = null;
+    private AtomCollection    entriesCollection = null;
+    private AtomCollection    resourcesCollection = null;
+    private Map               collections = new TreeMap();
+    
+    /**
+     * Create AtomBlog using specified HTTPClient, user account and workspace, 
+     * called by AtomConnection. Fetches Atom Service document and creates 
+     * an AtomCollection object for each collection found. The first entry 
+     * collection is considered the primary entry collection. And the first 
+     * resource collection is considered the primary resource collection.
+     */
+    AtomBlog(ClientAtomService service, ClientWorkspace workspace) {
+        this.setService(service);
+        this.setWorkspace(workspace);
+        this.name = workspace.getTitle();
+        Iterator members = workspace.getCollections().iterator();
+        
+        while (members.hasNext()) {
+            ClientCollection col = (ClientCollection) members.next();
+            if (col.accepts("entry") && entriesCollection == null) {
+                // first entry collection is primary entry collection 
+                entriesCollection = new AtomCollection(this, col);
+            }
+            else if (!col.accepts("entry") && resourcesCollection == null) {
+                // first non-entry collection is primary resource collection
+                resourcesCollection = new AtomCollection(this, col);
+            } 
+            collections.put(col.getHrefResolved(), new AtomCollection(this, col));
+        }    
+    }  
+    
+    /**
+     * {@inheritDoc} 
+     */
+    public String getName() { return name; }
+
+    /**
+     * String display of blog, returns name.
+     */
+    public String toString() { return getName(); }
+    
+    /**
+     * {@inheritDoc} 
+     */
+    public String getToken() { return entriesCollection.getToken(); }
+
+    /**
+     * {@inheritDoc} 
+     */
+    public BlogEntry newEntry() throws BlogClientException { 
+        if (entriesCollection == null) throw new BlogClientException("No entry collection");
+        return entriesCollection.newEntry(); 
+    }  
+    
+    /**
+     * {@inheritDoc}
+     */
+    public BlogEntry getEntry(String token) throws BlogClientException {      
+        ClientEntry clientEntry = null;
+        AtomEntry atomEntry = null;
+        try {
+            clientEntry = getService().getEntry(token);         
+        } catch (ProponoException ex) {
+            throw new BlogClientException("ERROR: fetching entry", ex);
+        }
+        if (clientEntry != null && clientEntry instanceof ClientMediaEntry) {
+            return new AtomResource(this, (ClientMediaEntry)clientEntry);
+        } else if (clientEntry != null && clientEntry instanceof ClientEntry) {
+            return new AtomEntry(this, clientEntry);
+        } else {
+            throw new BlogClientException("ERROR: unknown object type returned");
+        }
+    }
+    
+    /**
+     * {@inheritDoc} 
+     */
+    public Iterator getEntries() throws BlogClientException {
+        if (entriesCollection == null) throw new BlogClientException("No primary entry collection");
+        return new AtomEntryIterator(entriesCollection);
+    }   
+    
+    /**
+     * {@inheritDoc} 
+     */
+    public Iterator getResources() throws BlogClientException {
+        if (resourcesCollection == null) throw new BlogClientException("No primary entry collection");
+        return new AtomEntryIterator(resourcesCollection);
+    }   
+    
+    String saveEntry(BlogEntry entry) throws BlogClientException {
+        if (entriesCollection == null) throw new BlogClientException("No primary entry collection");
+        return entriesCollection.saveEntry(entry);
+    } 
+    
+    void deleteEntry(BlogEntry entry) throws BlogClientException {
+        if (entriesCollection == null) throw new BlogClientException("No primary entry collection");
+        entriesCollection.deleteEntry(entry);        
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public List getCategories() throws BlogClientException {
+        if (entriesCollection == null) throw new BlogClientException("No primary entry collection");
+        return entriesCollection.getCategories();
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public BlogResource newResource(
+        String name, String contentType, byte[] bytes) throws BlogClientException {
+        if (resourcesCollection == null) { 
+            throw new BlogClientException("No resource collection");
+        }
+        return resourcesCollection.newResource(name, contentType, bytes);
+    }
+
+
+    String saveResource(BlogResource res) throws BlogClientException {
+        if (resourcesCollection == null) throw new BlogClientException("No primary resource collection");
+        return resourcesCollection.saveResource(res);
+    }
+         
+    void deleteResource(BlogResource resource) throws BlogClientException {
+        deleteEntry((BlogEntry)resource);
+    }   
+    
+    /**
+     * {@inheritDoc}
+     */
+    public List getCollections() throws BlogClientException {
+        return new ArrayList(collections.values());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Blog.Collection getCollection(String token) throws BlogClientException {
+        return (Blog.Collection)collections.get(token);
+    } 
+    
+    ClientAtomService getService() {
+        return service;
+    }
+
+    void setService(ClientAtomService service) {
+        this.service = service;
+    }
+
+    ClientWorkspace getWorkspace() {
+        return workspace;
+    }
+
+    void setWorkspace(ClientWorkspace workspace) {
+        this.workspace = workspace;
+    }
+
+}
diff --git a/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomCollection.java b/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomCollection.java
new file mode 100644
index 0000000..3758bc5
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomCollection.java
@@ -0,0 +1,165 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient.atomprotocol;
+
+import com.sun.syndication.feed.atom.Category;
+import org.rometools.propono.atom.client.ClientAtomService;
+import org.rometools.propono.atom.common.Categories;
+import org.rometools.propono.atom.client.ClientCollection;
+import org.rometools.propono.atom.client.ClientEntry;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List; 
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.rometools.propono.blogclient.Blog;
+import org.rometools.propono.blogclient.BlogClientException;
+import org.rometools.propono.blogclient.BlogEntry;
+import org.rometools.propono.blogclient.BlogResource;
+
+/**
+ * Atom protocol implementation of BlogClient Blog.Collection.
+ */
+public class AtomCollection implements Blog.Collection {
+    static final Log logger = LogFactory.getLog(AtomCollection.class);
+    
+    private Blog blog = null;
+    private List categories = new ArrayList();
+
+    private ClientCollection clientCollection = null;
+
+
+    AtomCollection(AtomBlog blog, ClientCollection col) {
+        this.blog = blog; 
+        this.clientCollection = col;
+        for (Iterator catsIter = col.getCategories().iterator(); catsIter.hasNext();) {
+            Categories cats = (Categories)catsIter.next();
+            for (Iterator catIter = cats.getCategories().iterator(); catIter.hasNext();) {
+                Category cat = (Category)catIter.next();
+                BlogEntry.Category blogCat = new BlogEntry.Category(cat.getTerm());
+                blogCat.setName(cat.getLabel());
+                blogCat.setUrl(cat.getScheme());
+                getCategories().add(blogCat);
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String getTitle() {
+        return getClientCollection().getTitle();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String getToken() {
+        return getClientCollection().getHrefResolved();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public List getAccepts() {
+        return getClientCollection().getAccepts();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean accepts(String ct) {
+        return getClientCollection().accepts(ct);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Iterator getEntries() throws BlogClientException {
+        return new AtomEntryIterator(this); 
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public BlogEntry newEntry() throws BlogClientException {
+        AtomBlog atomBlog = (AtomBlog)getBlog();
+        BlogEntry entry = new AtomEntry(atomBlog, this);
+        return entry;        
+    }
+        
+    /**
+     * {@inheritDoc}
+     */
+    public BlogResource newResource(String name, String contentType, byte[] bytes) throws BlogClientException {
+        return new AtomResource(this, name, contentType, bytes);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String saveResource(BlogResource res) throws BlogClientException {
+        ((AtomResource)res).setCollection(this);
+        res.save();
+        return res.getContent().getSrc();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String saveEntry(BlogEntry entry) throws BlogClientException {
+        ((AtomEntry)entry).setCollection(this);
+        entry.save();
+        return entry.getPermalink();
+    }
+
+    void deleteEntry(BlogEntry entry) throws BlogClientException {
+        try {       
+            ClientAtomService service = ((AtomBlog)getBlog()).getService();
+            ClientEntry clientEntry = service.getEntry(entry.getToken());
+            clientEntry.remove();
+
+        } catch (Exception e) {
+            throw new BlogClientException("ERROR deleting entry", e);
+        }
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public Blog getBlog() {
+        return blog;
+    }
+
+    void setBlog(AtomBlog blog) {
+        this.blog = blog;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public List getCategories() {
+        return categories;
+    }
+
+    void setCategories(List categories) {
+        this.categories = categories;
+    }
+
+    ClientCollection getClientCollection() {
+        return clientCollection;
+    }
+}
diff --git a/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomConnection.java b/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomConnection.java
new file mode 100644
index 0000000..3545a67
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomConnection.java
@@ -0,0 +1,92 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient.atomprotocol;
+
+import org.rometools.propono.atom.client.AtomClientFactory;
+import org.rometools.propono.atom.client.BasicAuthStrategy;
+import org.rometools.propono.atom.client.ClientAtomService;
+import org.rometools.propono.atom.client.ClientWorkspace;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.jdom.Document;
+
+import org.rometools.propono.blogclient.BlogConnection;
+import org.rometools.propono.blogclient.Blog;
+import org.rometools.propono.blogclient.BlogClientException;
+
+
+/**
+ * Atom protocol of BlogConnection. Connects to Atom server, creates AtomBlog
+ * object for each Atom workspace found and within each blog a collection for each
+ * Atom collection found. 
+ */
+public class AtomConnection implements BlogConnection {
+    private static Log logger = LogFactory.getLog(AtomConnection.class);
+    private HttpClient httpClient = null;
+    private Map blogs = new HashMap();
+    
+    /**
+     * Create Atom blog client instance for specified URL and user account.
+     * @param uri      End-point URL of Atom service
+     * @param username Username of account 
+     * @param password Password of account
+     */
+    public AtomConnection(String uri, String username, String password) 
+        throws BlogClientException {
+        
+        Document doc = null;
+        try {
+            ClientAtomService service = (ClientAtomService)
+                AtomClientFactory.getAtomService(uri, new BasicAuthStrategy(username, password));
+            Iterator iter = service.getWorkspaces().iterator();
+            int count = 0;
+            while (iter.hasNext()) {
+                ClientWorkspace workspace = (ClientWorkspace)iter.next();
+                Blog blog = new AtomBlog(service, workspace);
+                blogs.put(blog.getToken(), blog);
+            }            
+        } catch (Throwable t) {
+            throw new BlogClientException("Error connecting to blog server", t);
+        }
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public List getBlogs() {
+        return new ArrayList(blogs.values());
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public Blog getBlog(String token) {
+        return (AtomBlog)blogs.get(token);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void setAppkey(String appkey) {
+    }
+}
diff --git a/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomEntry.java b/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomEntry.java
new file mode 100644
index 0000000..29242a5
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomEntry.java
@@ -0,0 +1,240 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient.atomprotocol;
+
+import org.rometools.propono.utils.ProponoException;
+import org.rometools.propono.atom.common.rome.AppModule;
+import org.rometools.propono.atom.common.rome.AppModuleImpl;
+import org.rometools.propono.blogclient.BlogClientException;
+import org.rometools.propono.blogclient.BlogEntry;
+import org.rometools.propono.blogclient.BaseBlogEntry;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.feed.atom.Link;
+import org.rometools.propono.atom.client.ClientEntry;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.rometools.propono.blogclient.BlogEntry.Person;
+
+/**
+ * Atom protocol implementation of BlogEntry.
+ */
+public class AtomEntry extends BaseBlogEntry implements BlogEntry {
+    static final Log logger = LogFactory.getLog(AtomCollection.class);
+    
+    String editURI = null;
+    AtomCollection collection = null;
+            
+    AtomEntry(AtomBlog blog, AtomCollection collection) throws BlogClientException {
+        super(blog);
+        this.collection = collection;
+    }
+    
+    AtomEntry(AtomCollection collection, ClientEntry entry)  throws BlogClientException {   
+        this((AtomBlog)collection.getBlog(), collection);
+        //clientEntry = entry;
+        copyFromRomeEntry(entry);
+    }
+    
+    AtomEntry(AtomBlog blog, ClientEntry entry)  throws BlogClientException {   
+        super(blog);
+        //clientEntry = entry;
+        copyFromRomeEntry(entry);
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public String getToken() {
+        return editURI;
+    }
+    
+    AtomCollection getCollection() {
+        return collection;
+    }
+
+    void setCollection(AtomCollection collection) {
+        this.collection = collection;
+    }
+
+    /**
+     * True if entry's token's are equal.
+     */
+    public boolean equals(Object o) {
+        if (o instanceof AtomEntry) {
+            AtomEntry other = (AtomEntry)o;
+            if (other.getToken() != null && getToken() != null) {
+                return other.getToken().equals(getToken());
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public void save() throws BlogClientException {
+        boolean create = (getToken() == null);
+        if (create && getCollection() == null) {
+            throw new BlogClientException("Cannot save entry, no collection");
+        } else if (create) {
+            try {
+                ClientEntry clientEntry = collection.getClientCollection().createEntry();
+                copyToRomeEntry(clientEntry);
+                collection.getClientCollection().addEntry(clientEntry);
+                copyFromRomeEntry(clientEntry);
+            } catch (ProponoException ex) {
+                 throw new BlogClientException("Error saving entry", ex);
+            }
+        } else {
+            try {
+                ClientEntry clientEntry = ((AtomBlog)getBlog()).getService().getEntry(getToken());
+                copyToRomeEntry(clientEntry);
+                clientEntry.update();
+                copyFromRomeEntry(clientEntry);
+            } catch (ProponoException ex) {
+                 throw new BlogClientException("Error updating entry", ex);
+            }
+        }   
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public void delete() throws BlogClientException {
+        if (getToken() == null) {
+            throw new BlogClientException("Cannot delete unsaved entry");
+        }
+        try {
+            ClientEntry clientEntry = ((AtomBlog)getBlog()).getService().getEntry(editURI);
+            clientEntry.remove();
+        } catch (ProponoException ex) {
+             throw new BlogClientException("Error removing entry", ex);
+        }
+    }
+        
+    void copyFromRomeEntry(ClientEntry entry) {
+        id = entry.getId();
+        title = entry.getTitle();   
+        editURI = entry.getEditURI();
+        List altlinks = entry.getAlternateLinks();
+        if (altlinks != null) {
+            for (Iterator iter = altlinks.iterator(); iter.hasNext();) {
+                Link link = (Link)iter.next();
+                if ("alternate".equals(link.getRel()) || link.getRel()==null) {
+                    permalink = link.getHrefResolved();
+                    break;
+                }
+            }
+        }
+        List contents = entry.getContents();
+        com.sun.syndication.feed.atom.Content romeContent = null;
+        if (contents != null && contents.size() > 0) {
+            romeContent = (com.sun.syndication.feed.atom.Content)contents.get(0);
+        }
+        if (romeContent != null) {
+            content = new BlogEntry.Content(romeContent.getValue());
+            content.setType(romeContent.getType());
+            content.setSrc(romeContent.getSrc());
+        }  
+        if (entry.getCategories() != null) {
+            List cats = new ArrayList();
+            List romeCats = entry.getCategories();
+            for (Iterator iter=romeCats.iterator(); iter.hasNext();) {
+                com.sun.syndication.feed.atom.Category romeCat = 
+                    (com.sun.syndication.feed.atom.Category)iter.next();
+                BlogEntry.Category cat = new BlogEntry.Category();
+                cat.setId(romeCat.getTerm());
+                cat.setUrl(romeCat.getScheme());
+                cat.setName(romeCat.getLabel());
+                cats.add(cat);
+            }
+            categories = cats;
+        }
+        List authors = entry.getAuthors();
+        if (authors!=null && authors.size() > 0) {
+            com.sun.syndication.feed.atom.Person romeAuthor = 
+                (com.sun.syndication.feed.atom.Person)authors.get(0);
+            if (romeAuthor != null) {
+                author = new Person();
+                author.setName(romeAuthor.getName());
+                author.setEmail(romeAuthor.getEmail());
+                author.setUrl(romeAuthor.getUrl());
+            }    
+        }    
+        publicationDate = entry.getPublished();
+        modificationDate = entry.getModified();
+        
+        AppModule control = (AppModule)entry.getModule(AppModule.URI);
+        if (control != null && control.getDraft() != null) { 
+            draft = control.getDraft().booleanValue(); 
+        } else {
+            draft = false;
+        }
+    }
+    Entry copyToRomeEntry(ClientEntry entry) {
+        if (id != null) {
+            entry.setId(id);
+        }
+        entry.setTitle(title);        
+        if (author != null) {
+            com.sun.syndication.feed.atom.Person person = 
+                new com.sun.syndication.feed.atom.Person();
+            person.setName(author.getName());
+            person.setEmail(author.getEmail());
+            person.setUrl(author.getUrl());
+            List authors = new ArrayList();
+            authors.add(person);
+            entry.setAuthors(authors);
+        }
+        if (content != null) {
+            com.sun.syndication.feed.atom.Content romeContent = 
+                new com.sun.syndication.feed.atom.Content();
+            romeContent.setValue(content.getValue());
+            romeContent.setType(content.getType());
+            List contents = new ArrayList();
+            contents.add(romeContent);
+            entry.setContents(contents);
+        }
+        if (categories != null) {
+            List romeCats = new ArrayList();
+            for (Iterator iter=categories.iterator(); iter.hasNext();) {
+                BlogEntry.Category cat = (BlogEntry.Category)iter.next();
+                com.sun.syndication.feed.atom.Category romeCategory =
+                    new com.sun.syndication.feed.atom.Category();
+                romeCategory.setTerm(cat.getId());
+                romeCategory.setScheme(cat.getUrl());
+                romeCategory.setLabel(cat.getName());
+                romeCats.add(romeCategory);
+            }
+            entry.setCategories(romeCats);
+        }
+        entry.setPublished((publicationDate == null) ? new Date() : publicationDate);
+        entry.setModified((modificationDate == null) ? new Date() : modificationDate);
+        
+        List modules = new ArrayList();
+        AppModule control = new AppModuleImpl();
+        control.setDraft(new Boolean(draft));
+        modules.add(control);
+        entry.setModules(modules);
+        
+        return entry;
+    }
+
+}
diff --git a/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomEntryIterator.java b/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomEntryIterator.java
new file mode 100644
index 0000000..20c30aa
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomEntryIterator.java
@@ -0,0 +1,72 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient.atomprotocol;
+
+import org.rometools.propono.atom.client.ClientEntry;
+import org.rometools.propono.atom.client.ClientMediaEntry;
+import java.util.Iterator;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.rometools.propono.blogclient.BlogClientException;
+
+/**
+ * Atom protocol implementation of BlogClient entry iterator.
+ */
+public class AtomEntryIterator implements Iterator {
+    static final Log logger = LogFactory.getLog(AtomEntryIterator.class);
+    private Iterator iterator = null;
+    private AtomCollection collection = null;
+    
+    AtomEntryIterator(AtomCollection collection) throws BlogClientException {
+        try {
+            this.collection = collection;
+            iterator = collection.getClientCollection().getEntries();
+        } catch (Exception e) {
+            throw new BlogClientException("ERROR fetching collection", e);
+        }
+    }
+    
+    /**
+     * True if more entries are available.
+     */
+    public boolean hasNext() {
+        return iterator.hasNext();
+    }
+    
+    /**
+     * Get next entry.
+     */
+    public Object next() {
+        try {
+            ClientEntry entry = (ClientEntry)iterator.next();
+            if (entry instanceof ClientMediaEntry) {
+                return new AtomResource(collection, (ClientMediaEntry)entry);
+            } else {
+                return new AtomEntry(collection, entry);
+            }
+        } catch (Exception e) {
+            logger.error("ERROR fetching entry", e);
+        }
+        return null;
+    }
+    
+    /**
+     * Remove is not supported.
+     */
+    public void remove() {
+        // optional method, not implemented
+    }
+}
diff --git a/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomResource.java b/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomResource.java
new file mode 100644
index 0000000..e20cee8
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/atomprotocol/AtomResource.java
@@ -0,0 +1,134 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient.atomprotocol;
+
+import java.io.InputStream;
+
+import org.rometools.propono.blogclient.BlogClientException;
+import org.rometools.propono.blogclient.BlogEntry;
+import org.rometools.propono.blogclient.BlogResource;
+import com.sun.syndication.feed.atom.Link;
+import org.rometools.propono.atom.client.ClientAtomService;
+import org.rometools.propono.atom.client.ClientCollection;
+import org.rometools.propono.atom.client.ClientEntry;
+import org.rometools.propono.atom.client.ClientMediaEntry;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Atom protocol implementation of BlogResource.
+ */
+public class AtomResource extends AtomEntry implements BlogResource {
+    private AtomCollection collection;
+    private byte[] bytes;
+
+    AtomResource(AtomCollection collection, String name, String contentType, byte[] bytes) 
+            throws BlogClientException {
+        super((AtomBlog)collection.getBlog(), collection);             
+        this.collection = collection;
+        this.bytes = bytes;
+        BlogEntry.Content rcontent = new BlogEntry.Content();
+        rcontent.setType(contentType);
+        setContent(rcontent);
+    } 
+    
+    AtomResource(AtomCollection collection, ClientMediaEntry entry) 
+            throws BlogClientException {
+        super(collection, entry);
+    }  
+    
+    AtomResource(AtomBlog blog, ClientMediaEntry entry)  throws BlogClientException {   
+        super(blog, entry);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String getName() {
+        return getTitle();
+    }
+    
+    byte[] getBytes() {
+        return bytes;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public InputStream getAsStream() throws BlogClientException {
+        try {
+            return null; //((ClientMediaEntry)clientEntry).getAsStream();
+        } catch (Exception e) {
+            throw new BlogClientException("Error creating entry", e);
+        }
+    }
+        
+    /**
+     * {@inheritDoc}
+     */
+    public void save() throws BlogClientException {
+        try {
+            if (getToken() == null) {
+                ClientAtomService clientService = ((AtomBlog)getBlog()).getService();
+                ClientCollection clientCollection = collection.getClientCollection();
+                
+                ClientMediaEntry clientEntry = 
+                    new ClientMediaEntry(clientService, clientCollection, getTitle(), 
+                        "", getContent().getType(), getBytes());
+                
+                copyToRomeEntry(clientEntry);
+                collection.getClientCollection().addEntry(clientEntry); 
+                this.editURI = clientEntry.getEditURI();
+                
+            } else {
+                ClientAtomService clientService = ((AtomBlog)getBlog()).getService();
+                ClientMediaEntry clientEntry = (ClientMediaEntry)clientService.getEntry(editURI);
+                clientEntry.update(); 
+            }
+        } catch (Exception e) {
+            throw new BlogClientException("Error creating entry", e);
+        }
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public void update(byte[] newBytes) throws BlogClientException {
+        try {
+            //((ClientMediaEntry)clientEntry).setBytes(newBytes);
+            //clientEntry.update();
+        } catch (Exception e) {
+            throw new BlogClientException("Error creating entry", e);
+        }
+    }
+        
+    void copyFromRomeEntry(ClientEntry entry) {
+        super.copyFromRomeEntry(entry);
+        
+        List links = entry.getOtherLinks();
+        if (links != null) {
+            for (Iterator iter = links.iterator(); iter.hasNext();) {
+                Link link = (Link)iter.next();
+                if ("edit-media".equals(link.getRel())) {
+                    id = link.getHrefResolved();
+                    break;
+                }
+            }
+        }
+        
+        
+    }
+}
diff --git a/src/main/java/org/rometools/propono/blogclient/blogclient-diagram.gif b/src/main/java/org/rometools/propono/blogclient/blogclient-diagram.gif
new file mode 100644
index 0000000000000000000000000000000000000000..35dac526bed67592e2a4feec37d8976085639158
GIT binary patch
literal 18568
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%0D!jTf<Ez`)vJZav6(GP
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
XQbb8sAT=&92?75s006xx0000xKC_ZD

literal 0
HcmV?d00001

diff --git a/src/main/java/org/rometools/propono/blogclient/metaweblog/MetaWeblogBlog.java b/src/main/java/org/rometools/propono/blogclient/metaweblog/MetaWeblogBlog.java
new file mode 100644
index 0000000..5c70d40
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/metaweblog/MetaWeblogBlog.java
@@ -0,0 +1,436 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient.metaweblog;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Iterator;
+import org.rometools.propono.blogclient.BlogEntry;
+import org.rometools.propono.blogclient.Blog;
+import org.rometools.propono.blogclient.BlogClientException;
+import org.rometools.propono.blogclient.BlogResource;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+import org.apache.xmlrpc.client.XmlRpcClient;
+import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
+
+/**
+ * Blog implementation that uses a mix of Blogger and MetaWeblog API methods.
+ */
+public class MetaWeblogBlog implements Blog {
+    private String blogid;
+    private String name;
+    private URL    url;
+    private String userName;
+    private String password;
+    private String appkey = "dummy";
+    private Map    collections;
+    
+    private XmlRpcClient xmlRpcClient = null;
+
+    /**
+     * {@inheritDoc}
+     */
+    public String getName() { return name; }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public String getToken() { return blogid; }
+    
+    /**
+     * String representation of blog, returns the name.
+     */
+    public String toString() { return getName(); }
+    
+    private XmlRpcClient getXmlRpcClient() { 
+        
+        if (xmlRpcClient == null) {
+            XmlRpcClientConfigImpl xmlrpcConfig = new XmlRpcClientConfigImpl();
+            xmlrpcConfig.setServerURL(url);
+            xmlRpcClient = new XmlRpcClient();
+            xmlRpcClient.setConfig(xmlrpcConfig);
+        }
+        return xmlRpcClient; 
+    }
+    
+    MetaWeblogBlog(String blogid, String name, 
+            URL url, String userName, String password) {
+        this.blogid = blogid;
+        this.name = name;
+        this.url = url;
+        this.userName = userName;
+        this.password = password;
+        this.collections = new TreeMap();
+        collections.put("entries", 
+            new MetaWeblogBlogCollection(this, "entries", "Entries", "entry"));
+        collections.put("resources", 
+            new MetaWeblogBlogCollection(this, "resources", "Resources", "*"));
+    }
+
+    MetaWeblogBlog(String blogId, String name, 
+            URL url, String userName, String password, String appkey) {
+        this(blogId, name, url, userName, password);
+        this.appkey = appkey;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public BlogEntry newEntry() {
+        return new MetaWeblogEntry(this, new HashMap());
+    }
+
+    String saveEntry(BlogEntry entry) throws BlogClientException {
+        Blog.Collection col = (Blog.Collection)collections.get("entries");
+        return col.saveEntry(entry);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public BlogEntry getEntry(String id) throws BlogClientException {
+        try {
+            Map result = (Map)
+                getXmlRpcClient().execute("metaWeblog.getPost", new Object[] {id, userName, password});
+            return new MetaWeblogEntry(this, result);
+        } catch (Exception e) {
+            throw new BlogClientException("ERROR: XML-RPC error getting entry", e);
+        }
+    }
+
+    void deleteEntry(String id) throws BlogClientException {
+        try {
+            getXmlRpcClient().execute("blogger.deletePost", 
+                new Object[] {appkey, id, userName, password, Boolean.FALSE});
+        } catch (Exception e) {
+            throw new BlogClientException("ERROR: XML-RPC error getting entry", e);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Iterator getEntries() throws BlogClientException {
+        return new EntryIterator();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public BlogResource newResource(String name, String contentType, byte[] bytes) throws BlogClientException {
+        return new MetaWeblogResource(this, name, contentType, bytes);
+    }
+    
+    String saveResource(MetaWeblogResource resource) throws BlogClientException {  
+        Blog.Collection col = (Blog.Collection)collections.get("resources");
+        return col.saveResource(resource);
+    }
+    
+    BlogResource getResource(String token) throws BlogClientException {
+        return null;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public Iterator getResources() throws BlogClientException {
+        return new NoOpIterator();
+    }
+     
+    void deleteResource(BlogResource resource) throws BlogClientException {
+        // no-op
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public List getCategories() throws BlogClientException {
+ 
+        ArrayList ret = new ArrayList();        
+        try {
+            Object result = 
+                getXmlRpcClient().execute ("metaWeblog.getCategories", 
+                new Object[] {blogid, userName, password});
+            if (result != null && result instanceof HashMap) {
+                // Standard MetaWeblog API style: struct of struts
+                Map catsmap = (Map)result;
+                Iterator keys = catsmap.keySet().iterator();
+                while (keys.hasNext()) {
+                    String key = (String)keys.next();
+                    Map catmap = (Map)catsmap.get(key);
+                    BlogEntry.Category category = new BlogEntry.Category(key);
+                    category.setName((String)catmap.get("description"));
+                    // catmap.get("htmlUrl"); 
+                    // catmap.get("rssUrl");
+                    ret.add(category);
+                } 
+            } else if (result != null && result instanceof Object[]) {
+                // Wordpress style: array of structs
+                Object[] resultArray = (Object[])result;
+                for (int i=0; i<resultArray.length; i++) {
+                    Map catmap = (Map)resultArray[i];
+                    String categoryId = (String)catmap.get("categoryId");
+                    String categoryName = (String)catmap.get("categoryName");
+                    BlogEntry.Category category = new BlogEntry.Category(categoryId);
+                    category.setName(categoryName);
+                    ret.add(category);   
+                }
+            } 
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return ret;
+    }
+
+    private HashMap createPostStructure(BlogEntry entry) {
+        return ((MetaWeblogEntry)entry).toPostStructure();
+    }
+        
+    /**
+     * {@inheritDoc}
+     */
+    public List getCollections() throws BlogClientException {
+        return new ArrayList(collections.values());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Blog.Collection getCollection(String token) throws BlogClientException {
+        return (Blog.Collection)collections.get(token);
+    }
+    
+    //-------------------------------------------------------------------------
+    
+    /** MetaWeblog API impplementation of Blog.Collection */
+    public class MetaWeblogBlogCollection implements Blog.Collection {
+        private String accept = null;
+        private String title = null;
+        private String token = null;
+        private Blog blog = null;
+        
+        /**
+         * @param token  Identifier for collection, unique within blog
+         * @param title  Title of collection
+         * @param accept Content types accepted, either "entry" or "*"
+         */
+        public MetaWeblogBlogCollection(Blog blog, String token, String title, String accept) {
+            this.blog = blog; 
+            this.accept = accept;
+            this.title = title;
+            this.token = token;
+        }
+        
+        /**
+         * {@inheritDoc}
+         */
+        public String getTitle() {
+            return title;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public String getToken() {
+            return token;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public List getAccepts() {
+            return Collections.singletonList(accept);
+        }
+        
+        /**
+         * {@inheritDoc}
+         */
+        public BlogResource newResource(String name, String contentType, byte[] bytes) throws BlogClientException {
+            return blog.newResource(name, contentType, bytes);
+        }
+    
+        /**
+         * {@inheritDoc}
+         */
+        public BlogEntry newEntry() throws BlogClientException {
+            return blog.newEntry();
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public boolean accepts(String ct) {
+            if (accept.equals("*")) {
+                // everything accepted
+                return true;
+            } else if (accept.equals("entry") && ct.equals("application/metaweblog+xml")) {
+                // entries only accepted and "application/metaweblog+xml" means entry
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public Iterator getEntries() throws BlogClientException {
+            Iterator ret = null;
+            if (accept.equals("entry")) {
+                ret = MetaWeblogBlog.this.getEntries();
+            } else {
+                ret = MetaWeblogBlog.this.getResources();
+            }
+            return ret;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public String saveEntry(BlogEntry entry) throws BlogClientException {
+            String ret = entry.getId();
+            if (entry.getId() == null) {
+                try {
+                    ret = (String)getXmlRpcClient().execute("metaWeblog.newPost", 
+                        new Object[] {blogid, userName, password, createPostStructure(entry), new Boolean(!entry.getDraft()) });
+                } catch (Exception e) {
+                    throw new BlogClientException("ERROR: XML-RPC error saving new entry", e);
+                }
+            } else {
+                try {
+                    getXmlRpcClient().execute("metaWeblog.editPost", 
+                        new Object[] {entry.getId(),userName,password,createPostStructure(entry),new Boolean(!entry.getDraft())});            
+                } catch (Exception e) {
+                    throw new BlogClientException("ERROR: XML-RPC error updating entry", e);
+                }
+            }
+            return ret;
+        }
+        
+        /**
+         * {@inheritDoc}
+         */
+        public String saveResource(BlogResource res) throws BlogClientException {
+            MetaWeblogResource resource = (MetaWeblogResource)res;
+            try {
+                HashMap resmap = new HashMap();
+                resmap.put("name", resource.getName());
+                resmap.put("type", resource.getContent().getType());
+                resmap.put("bits", resource.getBytes());                           
+                Map result = (Map)
+                    getXmlRpcClient().execute("metaWeblog.newMediaObject", 
+                    new Object[] {blogid, userName, password, resmap});
+                String url = (String)result.get("url");
+                res.getContent().setSrc(url);
+                return url;
+            } catch (Exception e) {
+                throw new BlogClientException("ERROR: loading or uploading file", e); 
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public List getCategories() throws BlogClientException {
+            return MetaWeblogBlog.this.getCategories();
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public Blog getBlog() {
+            return blog;
+        }
+    }  
+    
+    //-------------------------------------------------------------------------
+    /**
+     * Iterates over MetaWeblog API entries.
+     */
+    public class EntryIterator implements Iterator {
+        private int pos = 0;
+        private boolean eod = false;
+        private static final int BUFSIZE = 30;
+        private List results = null;
+        /**
+         * Iterator for looping over MetaWeblog API entries.
+         */
+        public EntryIterator() throws BlogClientException {
+            getNextEntries();
+        }
+        /**
+         * Returns true if more entries are avialable.
+         */
+        public boolean hasNext() {
+            if (pos == results.size() && !eod) {
+                try { getNextEntries(); } catch (Exception ignored) {}
+            }
+            return (pos < results.size());
+        }
+        /**
+         * Get next entry.
+         */
+        public Object next() {
+            Map entryHash = (Map)results.get(pos++);
+            return new MetaWeblogEntry(MetaWeblogBlog.this, entryHash);
+        }
+        /**
+         * Remove is not implemented.
+         */
+        public void remove() {
+        }
+        private void getNextEntries() throws BlogClientException {
+            int requestSize = pos + BUFSIZE;
+            try {
+                Object[] resultsArray = (Object[])
+                    getXmlRpcClient().execute("metaWeblog.getRecentPosts", 
+                        new Object[] {blogid, userName, password, new Integer(requestSize)} );
+                results = Arrays.asList(resultsArray);
+            } catch (Exception e) {
+                throw new BlogClientException("ERROR: XML-RPC error getting entry", e);
+            }
+            if (results.size() < requestSize) eod = true;
+        }
+    }
+    
+    //-------------------------------------------------------------------------
+    /**
+     * No-op iterator.
+     */
+    public class NoOpIterator implements Iterator {
+        /**
+         * No-op
+         */
+        public boolean hasNext() {
+            return false;
+        }
+        /**
+         * No-op
+         */
+        public Object next() {
+            return null;
+        }        
+        /**
+         * No-op
+         */
+        public void remove() {}
+    }
+
+}
diff --git a/src/main/java/org/rometools/propono/blogclient/metaweblog/MetaWeblogConnection.java b/src/main/java/org/rometools/propono/blogclient/metaweblog/MetaWeblogConnection.java
new file mode 100644
index 0000000..ff637a9
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/metaweblog/MetaWeblogConnection.java
@@ -0,0 +1,105 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient.metaweblog;
+
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.HashMap;
+import java.io.IOException;
+
+import org.rometools.propono.blogclient.BlogConnection;
+import org.rometools.propono.blogclient.Blog;
+import org.rometools.propono.blogclient.BlogClientException;
+import org.apache.xmlrpc.XmlRpcException;
+import org.apache.xmlrpc.client.XmlRpcClient;
+import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
+
+/**
+ * BlogClient implementation that uses a mix of Blogger and MetaWeblog API methods.
+ */
+public class MetaWeblogConnection implements BlogConnection {
+    private URL url = null;
+    private String userName = null;
+    private String password = null;
+    private String appkey = "null";
+    private Map blogs = null;
+    
+    private XmlRpcClient xmlRpcClient = null;
+        
+    public MetaWeblogConnection(String url, String userName, String password) 
+            throws BlogClientException {
+        this.userName = userName;
+        this.password = password;
+        try {
+            this.url = new URL(url);
+            blogs = createBlogMap();
+        } catch (Throwable t) {
+            throw new BlogClientException("ERROR connecting to server", t);
+        }
+    }
+    
+    private XmlRpcClient getXmlRpcClient() {         
+        if (xmlRpcClient == null) {
+            XmlRpcClientConfigImpl xmlrpcConfig = new XmlRpcClientConfigImpl();
+            xmlrpcConfig.setServerURL(url);
+            xmlRpcClient = new XmlRpcClient();
+            xmlRpcClient.setConfig(xmlrpcConfig);
+        }
+        return xmlRpcClient; 
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public List getBlogs() {
+        return new ArrayList(blogs.values());
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    private Map createBlogMap() throws XmlRpcException, IOException {
+        Map blogMap = new HashMap();
+        Object[] results = (Object[])getXmlRpcClient().execute("blogger.getUsersBlogs", 
+            new Object[] {appkey, userName, password});
+        for (int i = 0; i < results.length; i++) {
+            Map blog = (Map)results[i];
+            String blogid = (String)blog.get("blogid");
+            String name = (String)blog.get("blogName");
+            blogMap.put(blogid, new MetaWeblogBlog(blogid, name, url, userName, password));
+        }
+        return blogMap;
+    }
+    
+     /**
+     * {@inheritDoc}
+     */
+    public Blog getBlog(String token) {
+        return (Blog)blogs.get(token);
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public void setAppkey(String appkey) {
+        this.appkey = appkey;
+    }
+}
+
diff --git a/src/main/java/org/rometools/propono/blogclient/metaweblog/MetaWeblogEntry.java b/src/main/java/org/rometools/propono/blogclient/metaweblog/MetaWeblogEntry.java
new file mode 100644
index 0000000..5c00935
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/metaweblog/MetaWeblogEntry.java
@@ -0,0 +1,122 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient.metaweblog;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import org.rometools.propono.blogclient.BlogClientException;
+import org.rometools.propono.blogclient.BaseBlogEntry;
+import org.rometools.propono.blogclient.BlogEntry;
+import java.util.Map;
+
+
+/** 
+ * MetaWeblog API implementation of an entry.
+ */
+public class MetaWeblogEntry extends BaseBlogEntry {
+    
+    MetaWeblogEntry(MetaWeblogBlog blog, Map entryMap) {
+        super(blog);
+        id = (String)entryMap.get("postid");
+        
+        content = new Content((String)entryMap.get("description"));
+        
+        // let's pretend MetaWeblog API has a content-type
+        content.setType("application/metaweblog+xml");
+        
+        // no way to tell if entry is draft or not
+        draft = false;
+
+        title = (String)entryMap.get("title");
+        publicationDate = (Date)entryMap.get("dateCreated");        
+        permalink = (String)entryMap.get("permaLink");
+
+        // AlexisMP: fix to get the author value populated.
+        author.setName( (String)entryMap.get("userid") );
+        author.setEmail( (String)entryMap.get("author") );
+
+        categories = new ArrayList();
+        Object[] catArray = (Object[])entryMap.get("categories");
+        if (catArray != null) {
+            for (int i=0; i<catArray.length; i++) {
+                Category cat = new Category((String)catArray[i]);
+                categories.add(cat);
+            }
+        }
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public String getToken() {
+        return id;
+    }
+    
+    /**
+     * True if tokens are equal
+     */
+    public boolean equals(Object o) {
+        if (o instanceof MetaWeblogEntry) {
+            MetaWeblogEntry other = (MetaWeblogEntry)o;
+            if (other.id != null && id != null) {
+                return other.id.equals(id);
+            }
+        }
+        return false;
+    }
+        
+    /**
+     * {@inheritDoc}
+     */
+    public void save() throws BlogClientException {
+        id = ((MetaWeblogBlog)getBlog()).saveEntry(this);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void delete() throws BlogClientException {
+        ((MetaWeblogBlog)getBlog()).deleteEntry(id);
+    }
+
+    HashMap toPostStructure() {
+        HashMap struct = new HashMap();       
+        if (getTitle() != null) {
+            struct.put("title", getTitle());
+        }
+        if (getContent() != null && getContent().getValue() != null) {
+            struct.put("description", getContent().getValue());
+        }
+        if (getCategories() != null && getCategories().size() > 0) {
+            List catArray = new ArrayList();
+            List cats = getCategories();
+            for (int i=0; i<cats.size(); i++) {
+                BlogEntry.Category cat = (BlogEntry.Category)cats.get(i);
+                catArray.add(cat.getName());
+            }
+            struct.put("categories", catArray);
+        }
+        if (getPublicationDate() != null) {
+            struct.put("dateCreated", getPublicationDate());
+        }
+        if (getId() != null) {
+            struct.put("postid", getId());                        
+        }
+        return struct;
+    }
+}
diff --git a/src/main/java/org/rometools/propono/blogclient/metaweblog/MetaWeblogResource.java b/src/main/java/org/rometools/propono/blogclient/metaweblog/MetaWeblogResource.java
new file mode 100644
index 0000000..72189d6
--- /dev/null
+++ b/src/main/java/org/rometools/propono/blogclient/metaweblog/MetaWeblogResource.java
@@ -0,0 +1,112 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.blogclient.metaweblog;
+
+import org.rometools.propono.blogclient.BlogClientException;
+import java.io.InputStream; 
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.rometools.propono.blogclient.BlogResource;
+import java.util.HashMap;
+
+/** 
+ * MetaWeblog API implementation of an resource entry.
+ */
+public class MetaWeblogResource extends MetaWeblogEntry implements BlogResource {
+    private MetaWeblogBlog blog;
+    private String name;
+    private String contentType;
+    private byte[] bytes;
+
+    MetaWeblogResource(MetaWeblogBlog blog, 
+        String name, String contentType, byte[] bytes) {
+        super(blog, new HashMap());
+        this.blog = blog;
+        this.name = name;
+        this.contentType = contentType;
+        this.bytes = bytes;
+        this.content = new Content();
+        this.content.setType(contentType);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String getName() {
+        return name;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public String getToken() {
+        return null;
+    }
+    /**
+     * Get content-type of associated media resource.
+     */
+    public String getContentType() {
+        return contentType;
+    }
+    /**
+     * Get media resource as input stream.
+     */
+    public InputStream getAsStream() throws BlogClientException {
+        HttpClient httpClient = new HttpClient();
+        GetMethod method = new GetMethod(permalink);
+        try {
+            httpClient.executeMethod(method);
+        } catch (Exception e) {
+            throw new BlogClientException("ERROR: error reading file", e);
+        }
+        if (method.getStatusCode() != 200) {
+            throw new BlogClientException("ERROR HTTP status=" + method.getStatusCode());
+        }
+        try {
+            return method.getResponseBodyAsStream();
+        } catch (Exception e) {
+            throw new BlogClientException("ERROR: error reading file", e);
+        }
+    }  
+    
+    /**
+     * {@inheritDoc}
+     */
+    public void save() throws BlogClientException {
+        blog.saveResource(this);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void update(byte[] bytes) throws BlogClientException {
+        this.bytes = bytes;
+        save();
+    }
+
+    /**
+     * Get resource data as byte array.
+     */
+    public byte[] getBytes() {
+        return bytes;
+    }
+    
+    /**
+     * Not supported by MetaWeblog API
+     */
+    public void delete() throws BlogClientException {
+    }
+}
diff --git a/src/main/java/org/rometools/propono/utils/ProponoException.java b/src/main/java/org/rometools/propono/utils/ProponoException.java
new file mode 100644
index 0000000..0143448
--- /dev/null
+++ b/src/main/java/org/rometools/propono/utils/ProponoException.java
@@ -0,0 +1,153 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.utils;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+
+
+/**
+ * Base Propono exception class.
+ */
+public class ProponoException extends Exception {
+    
+    private Throwable mRootCause = null;
+    private String longMessage = null;
+    
+    
+    /**
+     * Construct emtpy exception object.
+     */
+    public ProponoException() {
+        super();
+    }
+    
+    
+    /**
+     * Construct ProponoException with message string.
+     * @param s Error message string.
+     */
+    public ProponoException(String s) {
+        super(s);
+    }
+    
+    /**
+     * Construct ProponoException with message string.
+     * @param s Error message string.
+     */
+    public ProponoException(String s, String longMessage) {
+        super(s);
+        this.longMessage = longMessage;
+    }
+    
+    
+    /**
+     * Construct ProponoException, wrapping existing throwable.
+     * @param s Error message
+     * @param t Existing connection to wrap.
+     */
+    public ProponoException(String s, Throwable t) {
+        super(s);
+        mRootCause = t;
+    }
+    
+    /**
+     * Construct ProponoException, wrapping existing throwable.
+     * @param s Error message
+     * @param t Existing connection to wrap.
+     */
+    public ProponoException(String s, String longMessge, Throwable t) {
+        super(s);
+        mRootCause = t;
+        this.longMessage = longMessage;
+    }
+    
+    
+    /**
+     * Construct ProponoException, wrapping existing throwable.
+     * @param t Existing exception to be wrapped.
+     */
+    public ProponoException(Throwable t) {
+        mRootCause = t;
+    }
+    
+    
+    /**
+     * Get root cause object, or null if none.
+     * @return Root cause or null if none.
+     */
+    public Throwable getRootCause() {
+        return mRootCause;
+    }
+    
+    
+    /**
+     * Get root cause message.
+     * @return Root cause message.
+     */
+    public String getRootCauseMessage() {
+        String rcmessage = null;
+        if (getRootCause()!=null) {
+            if (getRootCause().getCause()!=null) {
+                rcmessage = getRootCause().getCause().getMessage();
+            }
+            rcmessage = (rcmessage == null) ? getRootCause().getMessage() : rcmessage;
+            rcmessage = (rcmessage == null) ? super.getMessage() : rcmessage;
+            rcmessage = (rcmessage == null) ? "NONE" : rcmessage;
+        }
+        return rcmessage;
+    }
+    
+    
+    /**
+     * Print stack trace for exception and for root cause exception if htere is one.
+     * @see java.lang.Throwable#printStackTrace()
+     */
+    public void printStackTrace() {
+        super.printStackTrace();
+        if (mRootCause != null) {
+            System.out.println("--- ROOT CAUSE ---");
+            mRootCause.printStackTrace();
+        }
+    }
+    
+    
+    /**
+     * Print stack trace for exception and for root cause exception if htere is one.
+     * @param s Stream to print to.
+     */
+    public void printStackTrace(PrintStream s) {
+        super.printStackTrace(s);
+        if (mRootCause != null) {
+            s.println("--- ROOT CAUSE ---");
+            mRootCause.printStackTrace(s);
+        }
+    }
+    
+    
+    /**
+     * Print stack trace for exception and for root cause exception if htere is one.
+     * @param s Writer to write to.
+     */
+    public void printStackTrace(PrintWriter s) {
+        super.printStackTrace(s);
+        if (null != mRootCause) {
+            s.println("--- ROOT CAUSE ---");
+            mRootCause.printStackTrace(s);
+        }
+    }
+    
+}
diff --git a/src/main/java/org/rometools/propono/utils/Utilities.java b/src/main/java/org/rometools/propono/utils/Utilities.java
new file mode 100644
index 0000000..67bcf89
--- /dev/null
+++ b/src/main/java/org/rometools/propono/utils/Utilities.java
@@ -0,0 +1,268 @@
+/*   
+ *  Copyright 2007 Dave Johnson (Blogapps project)
+ * 
+ * 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.rometools.propono.utils;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.util.NoSuchElementException;
+import java.util.StringTokenizer;
+import java.util.regex.Pattern;
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.Namespace;
+import org.jdom.Parent;
+
+/**
+ * Utilities for file I/O and string manipulation.
+ */
+public class Utilities {
+
+    private static final String LS = System.getProperty("line.separator");
+
+    /** 
+     * Returns the contents of the file in a byte array (from JavaAlmanac).
+     */
+    public static byte[] getBytesFromFile(File file) throws IOException {
+        InputStream is = new FileInputStream(file);
+
+        // Get the size of the file
+        long length = file.length();
+
+        // You cannot create an array using a long type.
+        // It needs to be an int type.
+        // Before converting to an int type, check
+        // to ensure that file is not larger than Integer.MAX_VALUE.
+        if (length > Integer.MAX_VALUE) {
+            // File is too large
+        }
+
+        // Create the byte array to hold the data
+        byte[] bytes = new byte[(int)length];
+
+        // Read in the bytes
+        int offset = 0;
+        int numRead = 0;
+        while (offset < bytes.length
+               && (numRead=is.read(bytes, offset, bytes.length-offset)) >= 0) {
+            offset += numRead;
+        }
+
+        // Ensure all the bytes have been read in
+        if (offset < bytes.length) {
+            throw new IOException("Could not completely read file "+file.getName());
+        }
+
+        // Close the input stream and return bytes
+        is.close();
+        return bytes;
+    }
+
+    /**
+     * Read input from stream and into string.
+     */
+    public static String streamToString(InputStream is) throws IOException {
+        StringBuffer sb = new StringBuffer();
+        BufferedReader in = new BufferedReader(new InputStreamReader(is));
+        String line;
+        while ((line = in.readLine()) != null) {
+            sb.append(line);
+            sb.append(LS);
+        }
+        return sb.toString();
+    }
+        
+    /**
+     * Copy input stream to output stream using 8K buffer.
+     */
+    public static void copyInputToOutput(
+            InputStream input,
+            OutputStream output)
+            throws IOException {
+        BufferedInputStream in = new BufferedInputStream(input);
+        BufferedOutputStream out = new BufferedOutputStream(output);
+        byte buffer[] = new byte[8192];
+        for (int count = 0; count != -1;) {
+            count = in.read(buffer, 0, 8192);
+            if (count != -1)
+                out.write(buffer, 0, count);
+        }
+        
+        try {
+            in.close();
+            out.close();
+        } catch (IOException ex) {
+            throw new IOException("Closing file streams, " + ex.getMessage());
+        }
+    }
+    
+        
+    /**
+     * Replaces occurences of non-alphanumeric characters with a supplied char.
+     */
+    public static String replaceNonAlphanumeric(String str, char subst) {
+        StringBuffer ret = new StringBuffer(str.length());
+        char[] testChars = str.toCharArray();
+        for (int i = 0; i < testChars.length; i++) {
+            if (Character.isLetterOrDigit(testChars[i])) {
+                ret.append(testChars[i]);
+            } else {
+                ret.append( subst );
+            }
+        }
+        return ret.toString();
+    }
+   
+    /**
+     * Convert string to string array.
+     */
+    public static String[] stringToStringArray(String instr, String delim)
+    throws NoSuchElementException, NumberFormatException {
+        StringTokenizer toker = new StringTokenizer(instr, delim);
+        String stringArray[] = new String[toker.countTokens()];
+        int i = 0;        
+        while (toker.hasMoreTokens()) {
+            stringArray[i++] = toker.nextToken();
+        }
+        return stringArray;
+    }
+    
+    /** 
+     * Convert string array to string.
+     */
+    public static String stringArrayToString(String[] stringArray, String delim) {
+        String ret = "";
+        for (int i = 0; i < stringArray.length; i++) {
+            if (ret.length() > 0)
+                ret = ret + delim + stringArray[i];
+            else
+                ret = stringArray[i];
+        }
+        return ret;
+    }
+    
+    
+        static Pattern absoluteURIPattern = Pattern.compile("^[a-z0-9]*:.*$");
+    
+    private static boolean isAbsoluteURI(String uri) {
+        return absoluteURIPattern.matcher(uri).find();
+    }
+    
+    private static boolean isRelativeURI(String uri) {
+        return !isAbsoluteURI(uri);
+    }
+        
+    /**
+     * } 
+     * Resolve URI based considering xml:base and baseURI.
+     * @param baseURI Base URI of feed
+     * @param parent  Parent from which to consider xml:base
+     * @param url     URL to be resolved
+     */
+    private static String resolveURI(String baseURI, Parent parent, String url) {
+        if (isRelativeURI(url)) {
+            url = (!".".equals(url) && !"./".equals(url)) ? url : "";
+
+            // Relative URI with parent
+            if (parent != null && parent instanceof Element) {
+
+                // Do we have an xml:base?         
+                String xmlbase = ((Element)parent).getAttributeValue(
+                    "base", Namespace.XML_NAMESPACE);
+                if (xmlbase != null && xmlbase.trim().length() > 0) {
+                    if (isAbsoluteURI(xmlbase)) {
+                        // Absolute xml:base, so form URI right now 
+                        if (url.startsWith("/")) { 
+                            // Host relative URI
+                            int slashslash = xmlbase.indexOf("//");
+                            int nextslash = xmlbase.indexOf("/", slashslash + 2);
+                            if (nextslash != -1) xmlbase = xmlbase.substring(0, nextslash);
+                            return formURI(xmlbase, url); 
+                        }
+                        if (!xmlbase.endsWith("/")) {
+                            // Base URI is filename, strip it off 
+                            xmlbase = xmlbase.substring(0, xmlbase.lastIndexOf("/"));
+                        }
+                        return formURI(xmlbase, url);
+                    } else {
+                        // Relative xml:base, so walk up tree
+                        return resolveURI(baseURI, parent.getParent(), 
+                            stripTrailingSlash(xmlbase) + "/"+ stripStartingSlash(url));
+                    }
+                }
+                // No xml:base so walk up tree
+                return resolveURI(baseURI, parent.getParent(), url);
+
+            // Relative URI with no parent (i.e. top of tree), so form URI right now
+            } else if (parent == null || parent instanceof Document) {
+                return formURI(baseURI, url);        
+            } 
+        }                
+        return url;
+    }
+        
+    /** 
+     * Form URI by combining base with append portion and giving 
+     * special consideration to append portions that begin with ".."
+     * @param base   Base of URI, may end with trailing slash
+     * @param append String to append, may begin with slash or ".."
+     */
+    private static String formURI(String base, String append) {
+        base = stripTrailingSlash(base);
+        append = stripStartingSlash(append);
+        if (append.startsWith("..")) {
+            String ret = null;
+            String[] parts = append.split("/");
+            for (int i=0; i<parts.length; i++) {
+                if ("..".equals(parts[i])) {
+                    int last = base.lastIndexOf("/");
+                    if (last != -1) {
+                        base = base.substring(0, last);
+                        append = append.substring(3, append.length());
+                    }
+                    else break;
+                }
+            }
+        }
+        return base + "/" + append;
+    }
+    
+    /** 
+     * Strip starting slash from beginning of string.
+     */
+    private static String stripStartingSlash(String s) {
+        if (s != null && s.startsWith("/")) {
+            s = s.substring(1, s.length());
+        }
+        return s;
+    }
+    
+    /** 
+     * Strip trailing slash from end of string.
+     */
+    private static String stripTrailingSlash(String s) {
+        if (s != null && s.endsWith("/")) {
+            s = s.substring(0, s.length() - 1);
+        }
+        return s;
+    }
+}
diff --git a/src/main/java/propono-version.properties b/src/main/resources/propono-version.properties
similarity index 100%
rename from src/main/java/propono-version.properties
rename to src/main/resources/propono-version.properties
diff --git a/src/main/java/rome.properties b/src/main/resources/rome.properties
similarity index 100%
rename from src/main/java/rome.properties
rename to src/main/resources/rome.properties
diff --git a/src/test/java/org/rometools/propono/atom/client/AtomClientTest.java b/src/test/java/org/rometools/propono/atom/client/AtomClientTest.java
new file mode 100644
index 0000000..23c791c
--- /dev/null
+++ b/src/test/java/org/rometools/propono/atom/client/AtomClientTest.java
@@ -0,0 +1,459 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.client;
+
+import org.rometools.propono.atom.client.BasicAuthStrategy;
+import org.rometools.propono.atom.client.ClientWorkspace;
+import org.rometools.propono.atom.client.ClientAtomService;
+import org.rometools.propono.atom.client.ClientCollection;
+import org.rometools.propono.atom.client.AtomClientFactory;
+import org.rometools.propono.atom.client.ClientEntry;
+import org.rometools.propono.atom.client.ClientMediaEntry;
+import com.sun.syndication.feed.atom.Category;
+import com.sun.syndication.feed.atom.Content;
+import org.rometools.propono.utils.ProponoException;
+import org.rometools.propono.atom.common.Categories;
+import org.rometools.propono.atom.common.Collection;
+import java.io.FileInputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Simple APP test designed to run against a live Atom server.
+ */
+public class AtomClientTest extends TestCase {
+    
+    private static Log log =
+            LogFactory.getFactory().getInstance(AtomClientTest.class);
+    
+    private static ClientAtomService service = null;
+
+    // Basic Auth example
+    private static String endpoint = "http://localhost:8080/sample-atomserver/app";
+    private static String username = "admin";
+    private static String password = "admin";
+    static {
+        try {
+            service = AtomClientFactory.getAtomService(endpoint,
+                new BasicAuthStrategy(username, password));
+        } catch (Exception e) {
+            log.error("ERROR creating service", e);
+        }
+    }
+
+    /*
+    // Roller OAuth example
+    private static String endpoint = "http://macsnoopdave:8080/roller-services/app";
+    private static String consumerKey    = "55132608a2fb68816bcd3d1caeafc933";
+    private static String consumerSecret = "bb420783-fdea-4270-ab83-36445c18c307";
+    private static String requestUri     = "http://macsnoopdave:8080/roller-services/oauth/requestToken";
+    private static String authorizeUri   = "http://macsnoopdave:8080/roller-services/oauth/authorize?userId=roller&oauth_callback=none";
+    private static String accessUri      = "http://macsnoopdave:8080/roller-services/oauth/accessToken";
+    private static String username       = "roller";
+    private static String password       = "n/a";
+    static {
+        try {
+            service = AtomClientFactory.getAtomService(endpoint,
+                new OAuthStrategy(
+                    username, consumerKey, consumerSecret, "HMAC-SHA1",
+                    requestUri, authorizeUri, accessUri));
+        } catch (Exception e) {
+            log.error("ERROR creating service", e);
+        }
+    }
+    */
+
+    // GData Blogger API
+    /*
+    private static String endpoint = "http://www.blogger.com/feeds/default/blogs?alt=atom-service";
+    private static String email = "EMAIL";
+    private static String password = "PASSWORD";
+    private static String serviceName = "blogger";
+    static {
+        try {
+            service = AtomClientFactory.getAtomService(endpoint,
+                new GDataAuthStrategy(email, password, serviceName));
+        } catch (Exception e) {
+            log.error("ERROR creating service", e);
+        }
+    }  
+    */
+
+
+    private int maxPagingEntries = 10;
+    
+    
+    public AtomClientTest(String testName) {
+        super(testName);
+    }
+
+    public String getEndpoint() {
+        return endpoint;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+    
+    protected void setUp() throws Exception {
+    }
+
+    protected void tearDown() throws Exception {
+    }
+
+    public static Test suite() {
+        TestSuite suite = new TestSuite(AtomClientTest.class);        
+        return suite;
+    }
+
+    /**
+     * Tests that server has introspection doc with at least one workspace.
+     */
+    public void testGetAtomService() throws Exception {        
+        assertNotNull(service);
+        assertTrue(service.getWorkspaces().size() > 0); 
+        for (Iterator it = service.getWorkspaces().iterator(); it.hasNext();) {
+            ClientWorkspace space = (ClientWorkspace) it.next();
+            assertNotNull(space.getTitle());            
+            log.debug("Workspace: " + space.getTitle());
+            for (Iterator colit = space.getCollections().iterator(); colit.hasNext();) {
+                ClientCollection col = (ClientCollection) colit.next();
+                log.debug("   Collection: " + col.getTitle() + " Accepts: " + col.getAccepts());
+                log.debug("      href: " + col.getHrefResolved());
+                assertNotNull(col.getTitle());
+            }
+        }
+    }
+    
+    /**
+     * Tests that entries can be posted and removed in all collections that 
+     * accept entries. Fails if no collections found that accept entries.
+     */
+    public void testSimpleEntryPostAndRemove() throws Exception {
+        assertNotNull(service);
+        assertTrue(service.getWorkspaces().size() > 0);    
+        int count = 0;
+        for (Iterator it = service.getWorkspaces().iterator(); it.hasNext();) {
+            ClientWorkspace space = (ClientWorkspace) it.next();
+            assertNotNull(space.getTitle());
+            
+            for (Iterator colit = space.getCollections().iterator(); colit.hasNext();) {
+                ClientCollection col = (ClientCollection) colit.next();
+                if (col.accepts(Collection.ENTRY_TYPE)) {
+                    
+                    // we found a collection that accepts entries, so post one
+                    ClientEntry m1 = col.createEntry();
+                    m1.setTitle("Test post");
+                    Content c = new Content();
+                    c.setValue("This is a test post");
+                    c.setType("html");
+                    m1.setContent(c);
+                    
+                    col.addEntry(m1);
+                    
+                    // entry should now exist on server
+                    ClientEntry m2 = col.getEntry(m1.getEditURI());
+                    assertNotNull(m2);
+                                        
+                    // remove entry
+                    m2.remove();
+                    
+                    // fetching entry now should result in exception
+                    boolean failed = false;
+                    try {
+                        col.getEntry(m1.getEditURI());                    
+                    } catch (ProponoException e) {
+                        failed = true;
+                    }
+                    assertTrue(failed);                    
+                    count++;
+                }
+            }
+        }  
+        assertTrue(count > 0);
+    } 
+    
+    /**
+     * Tests that entries can be posted, updated and removed in all collections that 
+     * accept entries. Fails if no collections found that accept entries.
+     */
+    public void testSimpleEntryPostUpdateAndRemove() throws Exception {
+        assertNotNull(service);
+        assertTrue(service.getWorkspaces().size() > 0);    
+        int count = 0;
+        for (Iterator it = service.getWorkspaces().iterator(); it.hasNext();) {
+            ClientWorkspace space = (ClientWorkspace) it.next();
+            assertNotNull(space.getTitle());
+            
+            for (Iterator colit = space.getCollections().iterator(); colit.hasNext();) {
+                ClientCollection col = (ClientCollection) colit.next();
+                if (col.accepts(Collection.ENTRY_TYPE)) {
+                    
+                    // we found a collection that accepts entries, so post one
+                    ClientEntry m1 = col.createEntry();
+                    m1.setTitle(col.getTitle() + ": Test post");
+                    Content c = new Content();
+                    c.setValue("This is a test post");
+                    c.setType("html");
+                    m1.setContent(c);
+                    
+                    col.addEntry(m1);
+                    
+                    // entry should now exist on server
+                    ClientEntry m2 = (ClientEntry)col.getEntry(m1.getEditURI());
+                    assertNotNull(m2);
+                    
+                    m2.setTitle(col.getTitle() + ": Updated title");
+                    m2.update();
+                                        
+                    // entry should now be updated on server
+                    ClientEntry m3 = (ClientEntry)col.getEntry(m1.getEditURI());
+                    assertEquals(col.getTitle() + ": Updated title", m3.getTitle());
+
+                    // remove entry
+                    m3.remove();
+                    
+                    // fetching entry now should result in exception
+                    boolean failed = false;
+                    try {
+                        col.getEntry(m1.getEditURI());                    
+                    } catch (ProponoException e) {
+                        failed = true;
+                    }
+                    assertTrue(failed);                   
+                    count++;
+                }
+            }
+        }  
+        assertTrue(count > 0);
+    } 
+    
+    public void testFindWorkspace() throws Exception {
+        assertNotNull(service);
+        ClientWorkspace ws = (ClientWorkspace)service.findWorkspace("adminblog");
+        if (ws != null) {
+            ClientCollection col = (ClientCollection)ws.findCollection(null, "entry");            
+            ClientEntry entry = col.createEntry();
+            entry.setTitle("NPE on submitting order query");
+            entry.setContent("This is a <b>bad</b> one!", Content.HTML);
+            col.addEntry(entry);
+            
+            // entry should now exist on server
+            ClientEntry saved = (ClientEntry)col.getEntry(entry.getEditURI());
+            assertNotNull(saved);
+
+            // remove entry
+            saved.remove();
+            
+            // fetching entry now should result in exception
+            boolean failed = false;
+            try {
+                col.getEntry(saved.getEditURI());                    
+            } catch (ProponoException e) {
+                failed = true;
+            }
+            assertTrue(failed);   
+        }
+    }
+    
+    /**
+     * Test posting an entry to every available collection with a fixed and 
+     * an unfixed category if server support allows, then cleanup.
+     */
+    public void testEntryPostWithCategories() throws Exception {
+        assertNotNull(service);
+        assertTrue(service.getWorkspaces().size() > 0);    
+        int count = 0;
+        for (Iterator it = service.getWorkspaces().iterator(); it.hasNext();) {
+            ClientWorkspace space = (ClientWorkspace) it.next();
+            assertNotNull(space.getTitle());
+            
+            for (Iterator colit = space.getCollections().iterator(); colit.hasNext();) {
+                ClientCollection col = (ClientCollection) colit.next();
+                if (col.accepts(Collection.ENTRY_TYPE)) {
+                    
+                    // we found a collection that accepts GIF, so post one
+                    ClientEntry m1 = col.createEntry();
+                    m1.setTitle("Test post");
+                    Content c = new Content();
+                    c.setValue("This is a test post");
+                    c.setType("html");
+                    m1.setContent(c);
+                    
+                    // if possible, pick one fixed an un unfixed category
+                    Category fixedCat = null;
+                    Category unfixedCat = null;
+                    List entryCats = new ArrayList();
+                    for (int i=0; i<col.getCategories().size(); i++) {
+                        Categories cats = (Categories)col.getCategories().get(i);
+                        if (cats.isFixed() && fixedCat == null) {
+                            String scheme = cats.getScheme();
+                            fixedCat = (Category)cats.getCategories().get(0);
+                            if (fixedCat.getScheme() == null) fixedCat.setScheme(scheme);
+                            entryCats.add(fixedCat);
+                        } else if (!cats.isFixed() && unfixedCat == null) {
+                            String scheme = cats.getScheme();
+                            unfixedCat = new Category();
+                            unfixedCat.setScheme(scheme);
+                            unfixedCat.setTerm("tagster");
+                            entryCats.add(unfixedCat);
+                        }
+                    }
+                    m1.setCategories(entryCats);
+                    col.addEntry(m1);
+                    
+                    // entry should now exist on server
+                    ClientEntry m2 = (ClientEntry)col.getEntry(m1.getEditURI());
+                    assertNotNull(m2);
+                    
+                    if (fixedCat != null) {
+                        // we added a fixed category, let's make sure it's there
+                        boolean foundCat = false;
+                        for (Iterator catit = m2.getCategories().iterator(); catit.hasNext();) {
+                            Category cat = (Category) catit.next();
+                            if (   cat.getTerm().equals(  fixedCat.getTerm())) {
+                                foundCat = true;
+                            }
+                        }
+                        assertTrue(foundCat);
+                    }
+                    
+                    if (unfixedCat != null) {
+                        // we added an unfixed category, let's make sure it's there
+                        boolean foundCat = false;
+                        for (Iterator catit = m2.getCategories().iterator(); catit.hasNext();) {
+                            Category cat = (Category) catit.next();
+                            if (cat.getTerm().equals(  unfixedCat.getTerm())) {
+                                foundCat = true;
+                            }
+                        }
+                        assertTrue(foundCat);
+                    }
+                    
+                    // remove entry
+                    m2.remove();
+                    
+                    // fetching entry now should result in exception
+                    boolean failed = false;
+                    try {
+                        col.getEntry(m1.getEditURI());                    
+                    } catch (ProponoException e) {
+                        failed = true;
+                    }
+                    assertTrue(failed);                    
+                    count++;
+                }
+            }
+        }  
+        assertTrue(count > 0);
+    }
+    
+    /**
+     * Post media entry to every media colletion avialable on server, then cleanup.
+     */
+    public void testMediaPost() throws Exception {
+        assertNotNull(service);
+        assertTrue(service.getWorkspaces().size() > 0);        
+        int count = 0;
+        for (Iterator it = service.getWorkspaces().iterator(); it.hasNext();) {
+            ClientWorkspace space = (ClientWorkspace) it.next();
+            assertNotNull(space.getTitle());
+            
+            for (Iterator colit = space.getCollections().iterator(); colit.hasNext();) {
+                ClientCollection col = (ClientCollection) colit.next();
+                if (col.accepts("image/gif")) {
+                    
+                    // we found a collection that accepts GIF, so post one
+                    ClientMediaEntry m1 = col.createMediaEntry("duke"+count, "duke"+count, "image/gif", 
+                        new FileInputStream("test/testdata/duke-wave-shadow.gif"));
+                    col.addEntry(m1);
+                    
+                    // entry should now exist on server
+                    ClientMediaEntry m2 = (ClientMediaEntry)col.getEntry(m1.getEditURI());
+                    assertNotNull(m2);
+                    
+                    // remove entry
+                    m2.remove();
+                    
+                    // fetching entry now should result in exception
+                    boolean failed = false;
+                    try {
+                        col.getEntry(m1.getEditURI());                    
+                    } catch (ProponoException e) {
+                        failed = true;
+                    }
+                    assertTrue(failed);
+                    count++;
+                }
+            }
+        }   
+        assertTrue(count > 0);
+    }
+    
+    /**
+     * Post X media entries each media collection found, test paging, then cleanup.
+     *
+    public void testMediaPaging() throws Exception {
+        ClientAtomService service = getClientAtomService();
+        assertNotNull(service);
+        assertTrue(service.getWorkspaces().size() > 0);        
+        int count = 0;
+        for (Iterator it = service.getWorkspaces().iterator(); it.hasNext();) {
+            ClientWorkspace space = (ClientWorkspace) it.next();
+            assertNotNull(space.getTitle());
+            
+            for (Iterator colit = space.getCollections().iterator(); colit.hasNext();) {
+                ClientCollection col = (ClientCollection) colit.next();
+                if (col.accepts("image/gif")) {
+                    
+                    // we found a collection that accepts GIF, so post 100 of them
+                    List posted = new ArrayList();
+                    for (int i=0; i<maxPagingEntries; i++) {
+                        ClientMediaEntry m1 = col.createMediaEntry("duke"+count, "duke"+count, "image/gif", 
+                            new FileInputStream("test/testdata/duke-wave-shadow.gif"));
+                        col.addEntry(m1);
+                        posted.add(m1);
+                    }
+                    int entryCount = 0;
+                    for (Iterator iter = col.getEntries(); iter.hasNext();) {
+                        ClientMediaEntry entry = (ClientMediaEntry) iter.next();
+                        entryCount++;
+                    }
+                    for (Iterator delit = posted.iterator(); delit.hasNext();) {
+                        ClientEntry entry = (ClientEntry) delit.next();
+                        entry.remove();
+                    }
+                    assertTrue(entryCount >= maxPagingEntries);
+                    count++;
+                    break;
+                }
+            }
+        }   
+        assertTrue(count > 0);
+    }*/  
+}
+
+
diff --git a/src/test/java/org/rometools/propono/atom/client/BloggerDotComTest.java b/src/test/java/org/rometools/propono/atom/client/BloggerDotComTest.java
new file mode 100644
index 0000000..7309751
--- /dev/null
+++ b/src/test/java/org/rometools/propono/atom/client/BloggerDotComTest.java
@@ -0,0 +1,91 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.client;
+
+import org.rometools.propono.atom.client.ClientCollection;
+import org.rometools.propono.atom.client.ClientAtomService;
+import org.rometools.propono.atom.client.AtomClientFactory;
+import org.rometools.propono.atom.client.GDataAuthStrategy;
+import org.rometools.propono.atom.client.ClientEntry;
+import com.sun.syndication.feed.atom.Content;
+import java.util.Iterator;
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+/**
+ * Simple APP test designed to run against Blogger.com.
+ */
+public class BloggerDotComTest extends TestCase {
+    
+    private String collectionURI = "http://www.blogger.com/feeds/BLOGID/posts/default";
+    private String atomServiceURI= "http://www.blogger.com/feeds/default/blogs?alt=atom-service";
+    private String email         = "EMAIL";
+    private String password      = "PASSWORD";
+    
+    public BloggerDotComTest(String testName) {
+        super(testName);
+    }
+
+    protected void setUp() throws Exception {
+    }
+
+    protected void tearDown() throws Exception {
+    }
+
+    public static Test suite() {
+        TestSuite suite = new TestSuite(BloggerDotComTest.class);        
+        return suite;
+    }
+
+    /**
+     * Verify that server returns service document containing workspaces containing collections.
+     */
+    public void testGetEntries() throws Exception {  
+                
+        // no auth necessary for iterating through entries
+        ClientCollection col = AtomClientFactory.getCollection(collectionURI, 
+            new GDataAuthStrategy(email, password, "blogger"));
+        assertNotNull(col);
+        int count = 0;
+        for (Iterator it = col.getEntries(); it.hasNext();) {
+            ClientEntry entry = (ClientEntry) it.next();
+            assertNotNull(entry);
+            count++;
+        }
+        assertTrue(count > 0);
+
+        col = AtomClientFactory.getCollection(collectionURI, 
+            new GDataAuthStrategy(email, password, "blogger"));
+        ClientEntry p1 = col.createEntry();
+        p1.setTitle("Propono post");
+        Content c = new Content();
+        c.setValue("This is content from ROME Propono");
+        p1.setContent(c);
+        col.addEntry(p1);
+        
+        ClientEntry p2 = col.getEntry(p1.getEditURI());
+        assertNotNull(p2);
+        
+        
+        ClientAtomService atomService = AtomClientFactory.getAtomService(
+            collectionURI, new GDataAuthStrategy(email, password, "blogger"));
+        assertNotNull(atomService);
+        
+    }
+}
+
+
diff --git a/src/test/java/org/rometools/propono/atom/common/AtomServiceTest.java b/src/test/java/org/rometools/propono/atom/common/AtomServiceTest.java
new file mode 100644
index 0000000..49afc72
--- /dev/null
+++ b/src/test/java/org/rometools/propono/atom/common/AtomServiceTest.java
@@ -0,0 +1,160 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.common;
+
+import org.rometools.propono.atom.common.Workspace;
+import org.rometools.propono.atom.common.AtomService;
+import org.rometools.propono.atom.common.Categories;
+import org.rometools.propono.atom.common.Collection;
+import com.sun.syndication.feed.atom.Category;
+import java.io.FileInputStream;
+import java.util.Iterator;
+import junit.framework.*;
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.input.SAXBuilder;
+
+/**
+ * Tests reading and writing of service document, no server needed.
+ */
+public class AtomServiceTest extends TestCase {
+    
+    public AtomServiceTest(String testName) {
+        super(testName);
+    }
+
+    protected void setUp() throws Exception {
+    }
+
+    protected void tearDown() throws Exception {
+    }
+
+    public static Test suite() {
+        TestSuite suite = new TestSuite(AtomServiceTest.class);
+        
+        return suite;
+    }
+
+    /**
+     * Test of documentToService method, of class AtomService.
+     */
+    public void testDocumentToService() {
+        try {
+            // Load service document from disk
+            SAXBuilder builder = new SAXBuilder();
+            Document document = builder.build(new FileInputStream("test/testdata/servicedoc1.xml"));
+            assertNotNull(document);
+            AtomService service = AtomService.documentToService(document); 
+            
+            int workspaceCount = 0;
+            
+            // Verify that service contains expected workspaces, collections and categories
+            for (Iterator it = service.getWorkspaces().iterator(); it.hasNext();) {
+                Workspace space = (Workspace)it.next();
+                assertNotNull(space.getTitle());
+                workspaceCount++;
+                int collectionCount = 0;
+
+                for (Iterator colit = space.getCollections().iterator(); colit.hasNext();) {
+                    Collection col = (Collection)colit.next();
+                    assertNotNull(col.getTitle());
+                    assertNotNull(col.getHrefResolved());
+                    collectionCount++;
+                    int catCount = 0;
+                    if (col.getCategories().size() > 0) {
+                        for (Iterator catsit = col.getCategories().iterator(); catsit.hasNext();) {
+                            Categories cats = (Categories) catsit.next();
+                            for (Iterator catit = cats.getCategories().iterator(); catit.hasNext();) {
+                                Category cat = (Category) catit.next();
+                                catCount++;
+                            }
+                            assertTrue(catCount > 0);
+                        }
+                    }
+                }
+            }
+            
+            assertTrue(workspaceCount > 0);
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+            fail();
+        }
+    }  
+    
+    /**
+     * Test of documentToService method, of class AtomService.
+     */
+    public void testServiceToDocument() {
+        try {
+            // Create service with workspace and collections
+            AtomService service = new AtomService();
+            
+            Workspace workspace1 = new Workspace("workspace1", null);
+            Workspace workspace2 = new Workspace("workspace1", null);
+            service.addWorkspace(workspace1);
+            service.addWorkspace(workspace2);
+            
+            Collection collection11 = 
+                new Collection("collection11", null, "http://example.com/app/col11");            
+            Collection collection12 = 
+                new Collection("collection12", null, "http://example.com/app/col12");
+            workspace1.addCollection(collection11);
+            workspace1.addCollection(collection12);
+            
+            Collection collection21 = 
+                new Collection("collection21", null, "http://example.com/app/col21");            
+            Collection collection22 = 
+                new Collection("collection22", null, "http://example.com/app/col22");
+            workspace2.addCollection(collection21);
+            workspace2.addCollection(collection22);
+            
+            // TODO: add categories at collection level
+            
+            // Convert to JDOM document
+            Document document = service.serviceToDocument();
+
+            // verify that JDOM document contains service, workspace and collection
+            assertEquals("service", document.getRootElement().getName());
+            int workspaceCount = 0;
+            for (Iterator spaceit = document.getRootElement().getChildren().iterator(); spaceit.hasNext();) {
+                Element elem = (Element) spaceit.next();            
+                if ("workspace".equals(elem.getName())) { 
+                    workspaceCount++;
+                }
+                boolean workspaceTitle = false;
+                int collectionCount = 0;
+                for (Iterator colit = elem.getChildren().iterator(); colit.hasNext();) {
+                    Element colelem = (Element) colit.next();
+                    if ("title".equals(colelem.getName())) { 
+                        workspaceTitle = true;
+                    } else if ("collection".equals(colelem.getName())){
+                        collectionCount++;             
+                    }
+                    
+                    // TODO: test for categories at the collection level
+                }
+                assertTrue(workspaceTitle);
+                assertTrue(collectionCount > 0);
+            }
+            assertTrue(workspaceCount > 0);
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+            fail();
+        }
+    }  
+}
diff --git a/src/test/java/org/rometools/propono/atom/common/CollectionTest.java b/src/test/java/org/rometools/propono/atom/common/CollectionTest.java
new file mode 100644
index 0000000..57696c9
--- /dev/null
+++ b/src/test/java/org/rometools/propono/atom/common/CollectionTest.java
@@ -0,0 +1,68 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.atom.common;
+
+import org.rometools.propono.atom.common.Collection;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import junit.framework.*;
+
+/**
+ * Tests Collection class, no server needed.
+ */
+public class CollectionTest extends TestCase {
+    
+    public CollectionTest(String testName) {
+        super(testName);
+    }
+
+    protected void setUp() throws Exception {
+    }
+
+    protected void tearDown() throws Exception {
+    }
+
+    /**
+     * Test of accepts method, of class com.sun.syndication.propono.atom.common.Collection.
+     */
+    public void testAccepts() {   
+        
+        Collection col = 
+            new Collection("dummy_title","dummy_titletype","dummy_href");
+        
+        col.setAccepts(Collections.singletonList("image/*"));
+        assertTrue(col.accepts("image/gif"));
+        assertTrue(col.accepts("image/jpg"));
+        assertTrue(col.accepts("image/png"));
+        assertFalse(col.accepts("test/html"));
+        
+        List accepts = new ArrayList();
+        accepts.add("image/*");
+        accepts.add("text/*");
+        col.setAccepts(accepts);
+        assertTrue(col.accepts("image/gif"));
+        assertTrue(col.accepts("image/jpg"));
+        assertTrue(col.accepts("image/png"));
+        assertTrue(col.accepts("text/html"));
+        
+        col.setAccepts(Collections.singletonList("*/*"));
+        assertTrue(col.accepts("image/gif"));
+        assertTrue(col.accepts("image/jpg"));
+        assertTrue(col.accepts("image/png"));
+        assertTrue(col.accepts("text/html"));
+    }  
+}
diff --git a/src/test/java/org/rometools/propono/atom/server/AtomClientServerTest.java b/src/test/java/org/rometools/propono/atom/server/AtomClientServerTest.java
new file mode 100644
index 0000000..cf599da
--- /dev/null
+++ b/src/test/java/org/rometools/propono/atom/server/AtomClientServerTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * 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.rometools.propono.atom.server;
+
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Level; 
+import java.util.logging.Logger;
+import junit.framework.Test;
+import junit.framework.TestSuite;
+import org.mortbay.http.HttpContext;
+import org.mortbay.http.HttpServer;
+import org.mortbay.http.SocketListener;
+import org.mortbay.jetty.servlet.ServletHandler;
+ 
+
+/**
+ * Test Propono Atom Client against Atom Server via Jetty. Extends 
+ * <code>AtomClientTest</code> to start Jetty server, run tests and then stop
+ * the Jetty server.
+ */
+public class AtomClientServerTest { // extends AtomClientTest {
+
+    private HttpServer server;
+    public static final int TESTPORT = 8283;
+    public static final String ENDPOINT = "http://localhost:" + TESTPORT + "/rome/app";
+    public static final String USERNAME = "admin";
+    public static final String PASSWORD = "admin";
+
+    public AtomClientServerTest(String s) {
+        //super(s);
+    }
+
+    public String getEndpoint() {
+        return ENDPOINT;
+    }
+
+    public String getUsername() {
+        return USERNAME;
+    }
+
+    public String getPassword() {
+        return PASSWORD;
+    }
+
+    public static Test suite() {
+        TestSuite suite = new TestSuite(AtomClientServerTest.class);
+        return suite;
+    }
+
+    protected HttpServer getServer() {
+        return server;
+    }
+
+    protected void setUp() throws Exception {
+        ConsoleHandler handler = new ConsoleHandler();
+        Logger logger = Logger.getLogger("com.sun.syndication.propono");
+        logger.setLevel(Level.FINEST);
+        logger.addHandler(handler);
+
+        setupServer();
+        HttpContext context = createContext();
+        ServletHandler servlets = createServletHandler();
+        context.addHandler(servlets);
+        server.addContext(context);
+        server.start();
+    }
+
+    private void setupServer() throws InterruptedException {
+        // Create the server
+        if (server != null) {
+            server.stop();
+            server = null;
+        }
+        server = new HttpServer();
+
+        // Create a port listener
+        SocketListener listener = new SocketListener();
+        listener.setPort(TESTPORT);
+        server.addListener(listener);
+    }
+
+    private ServletHandler createServletHandler() {
+        System.setProperty(
+                "com.sun.syndication.propono.atom.server.AtomHandlerFactory",
+                "com.sun.syndication.propono.atom.server.TestAtomHandlerFactory");
+        ServletHandler servlets = new ServletHandler();
+        servlets.addServlet(
+                "app", "/app/*",
+                "com.sun.syndication.propono.atom.server.AtomServlet");
+        return servlets;
+    }
+
+    private HttpContext createContext() {
+        HttpContext context = new HttpContext();
+        context.setContextPath("/rome/*");
+        return context;
+    }
+
+    protected void tearDown() throws Exception {
+        if (server != null) {
+            server.stop();
+            server.destroy();
+            server = null;
+        }
+    }
+}
+
+
diff --git a/src/test/java/org/rometools/propono/atom/server/TestAtomHandlerFactory.java b/src/test/java/org/rometools/propono/atom/server/TestAtomHandlerFactory.java
new file mode 100644
index 0000000..d4d5c52
--- /dev/null
+++ b/src/test/java/org/rometools/propono/atom/server/TestAtomHandlerFactory.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * 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.rometools.propono.atom.server;
+
+import org.rometools.propono.atom.server.AtomHandlerFactory;
+import org.rometools.propono.atom.server.AtomHandler;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class TestAtomHandlerFactory extends AtomHandlerFactory {
+    
+    public AtomHandler newAtomHandler(HttpServletRequest req, HttpServletResponse res) {
+        return new TestAtomHandlerImpl(req, "build/testuploaddir");
+    }
+}
diff --git a/src/test/java/org/rometools/propono/atom/server/TestAtomHandlerImpl.java b/src/test/java/org/rometools/propono/atom/server/TestAtomHandlerImpl.java
new file mode 100644
index 0000000..83b047d
--- /dev/null
+++ b/src/test/java/org/rometools/propono/atom/server/TestAtomHandlerImpl.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * 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.rometools.propono.atom.server;
+
+import org.rometools.propono.atom.server.impl.FileBasedAtomHandler;
+import javax.servlet.http.HttpServletRequest;
+
+public class TestAtomHandlerImpl extends FileBasedAtomHandler {
+       
+    public TestAtomHandlerImpl(HttpServletRequest req, String uploaddir) {
+        super(req, uploaddir);
+    }
+    public boolean validateUser( String loginId, String password ) {        
+        return AtomClientServerTest.USERNAME.equals(loginId) 
+            && AtomClientServerTest.PASSWORD.equals(password); 
+    }    
+}
\ No newline at end of file
diff --git a/src/test/java/org/rometools/propono/blogclient/SimpleBlogClientTest.java b/src/test/java/org/rometools/propono/blogclient/SimpleBlogClientTest.java
new file mode 100644
index 0000000..bdb1d34
--- /dev/null
+++ b/src/test/java/org/rometools/propono/blogclient/SimpleBlogClientTest.java
@@ -0,0 +1,220 @@
+/*   
+ * Copyright 2007 Sun Microsystems, Inc.
+ * 
+ * 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.rometools.propono.blogclient;
+
+import org.rometools.propono.blogclient.BlogConnection;
+import org.rometools.propono.blogclient.BlogResource;
+import org.rometools.propono.blogclient.Blog;
+import org.rometools.propono.blogclient.BlogConnectionFactory;
+import org.rometools.propono.blogclient.BlogEntry;
+import com.sun.syndication.io.impl.Atom10Parser;
+import org.rometools.propono.utils.Utilities;
+import java.io.File;
+import java.util.Iterator;
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+
+
+/**
+ * Tests Atom and MetaWeblog API CRUD via BlogClient.
+ * Exclude this from automated tests because it requires a live blog server.
+ */
+public class SimpleBlogClientTest extends TestCase {
+    
+    private String metaweblogEndpoint = "http://localhost:8080/roller/roller-services/xmlrpc";
+    //private String atomEndpoint       = "http://localhost:8080/roller/roller-services/app";
+    private String atomEndpoint       = "http://localhost:8080/sample-atomserver/app";
+    
+    private String endpoint = "http://localhost:8080/atom-fileserver/app";
+    private String username = "admin";
+    private String password = "admin";
+    
+    public SimpleBlogClientTest(String testName) {
+        super(testName);
+    }
+
+    protected void setUp() throws Exception {
+    }
+
+    protected void tearDown() throws Exception {
+    }
+    
+    public void testBlogClientAtom() throws Exception {
+        testBlogClient("atom", atomEndpoint);
+    }
+    
+    public void testBlogClientMetaWeblog() throws Exception{
+        testBlogClient("metaweblog", metaweblogEndpoint);
+    }
+    
+    public void testBlogClient(String type, String endpoint) throws Exception {
+        BlogConnection conn = BlogConnectionFactory
+                .getBlogConnection(type, endpoint, username, password);
+        
+        int blogCount = 0;
+        for (Iterator it = conn.getBlogs().iterator(); it.hasNext();) {
+            Blog blog = (Blog) it.next();
+            System.out.println(blog.getName());
+            blogCount++;
+        }
+        assertTrue(blogCount > 0); 
+    }
+
+    public void testPostAndDeleteAtom() throws Exception {
+        testPostAndDelete("atom", atomEndpoint);
+    }
+    
+    public void testPostAndDeleteMetaWeblog() throws Exception {
+        testPostAndDelete("metaweblog", metaweblogEndpoint);
+    }
+    
+    public void testMediaPostAtom() throws Exception {
+        testMediaPost("atom", atomEndpoint);
+    }
+    
+    public void testMediaPostMetaWeblog() throws Exception {
+        testMediaPost("metaweblog", metaweblogEndpoint);
+    }
+    
+    public void testPostAndDelete(String type, String endpoint) throws Exception {
+        BlogConnection conn = BlogConnectionFactory
+                .getBlogConnection(type, endpoint, username, password);
+        assertNotNull(conn);
+        
+        String title1 = "Test content";
+        String content1 = "Test content";
+        
+        Blog blog = (Blog)conn.getBlogs().get(0);
+        BlogEntry entry = blog.newEntry();
+        entry.setTitle(title1);
+        entry.setContent(new BlogEntry.Content(content1));
+        entry.save();
+        String token = entry.getToken();
+        assertNotNull(token);
+        
+        entry = blog.getEntry(token);
+        
+        assertEquals(title1, entry.getTitle());
+        assertEquals(content1, entry.getContent().getValue());
+        
+        assertNotNull(entry);
+        entry.delete();
+        entry = null;
+        
+        boolean notFound = false;
+        try {
+            entry = blog.getEntry(token);
+        } catch (Exception e) {
+            notFound = true;
+        }
+        assertTrue(notFound);
+    }
+
+    /**
+     * Post media entry to every media colletion avialable on server, then cleanup.
+     */
+    public void testMediaPost(String type, String endpoint) throws Exception {
+        BlogConnection conn = BlogConnectionFactory
+                .getBlogConnection(type, endpoint, username, password);
+        assertNotNull(conn);
+        
+        assertTrue(conn.getBlogs().size() > 0);        
+        int count = 0;
+        for (Iterator it = conn.getBlogs().iterator(); it.hasNext();) {
+            Blog blog = (Blog) it.next();
+            assertNotNull(blog.getName());
+            
+            for (Iterator colit = blog.getCollections().iterator(); colit.hasNext();) {
+                Blog.Collection col = (Blog.Collection) colit.next();
+                if (col.accepts("image/gif")) {
+                    
+                    // we found a collection that accepts GIF, so post one
+                    BlogResource m1 = col.newResource("duke"+count, "image/gif", 
+                        Utilities.getBytesFromFile(new File("test/testdata/duke-wave-shadow.gif")));
+                    col.saveResource(m1);
+                    
+                    if ("atom".equals(type)) { // additional tests for Atom
+
+                        // entry should now exist on server
+                        BlogResource m2 = (BlogResource)blog.getEntry(m1.getToken());
+                        assertNotNull(m2);
+
+                        // remove entry
+                        m2.delete();
+
+                        // fetching entry now should result in exception
+                        boolean failed = false;
+                        try {
+                            blog.getEntry(m1.getToken());                    
+                        } catch (Exception e) {
+                            failed = true;
+                        }
+                        assertTrue(failed);
+                    }
+                    count++;
+                }
+            }
+        }   
+        assertTrue(count > 0);
+    }
+
+    
+    public void testEntryIterationAtom() throws Exception {
+        testEntryIteration("atom", atomEndpoint);
+    }
+    
+    public void testEntryIterationMetaWeblog() throws Exception {
+        testEntryIteration("metaweblog", metaweblogEndpoint);
+    }
+
+    public void testEntryIteration(String type, String endpoint) throws Exception {
+        BlogConnection conn = BlogConnectionFactory
+                .getBlogConnection(type, endpoint, username, password);
+        assertNotNull(conn);
+        
+        String title1 = "Test content";
+        String content1 = "Test content";
+        
+        Blog blog = (Blog)conn.getBlogs().get(0);
+        
+        for (int i=0; i<10; i++) {
+            BlogEntry entry = blog.newEntry();
+            entry.setTitle(title1);
+            entry.setContent(new BlogEntry.Content(content1));
+            entry.save();
+            String token = entry.getToken();
+            assertTrue(Atom10Parser.isAbsoluteURI(token));
+            assertNotNull(token);
+        }
+
+        for (Iterator it = blog.getEntries(); it.hasNext();) {
+            BlogEntry blogEntry = (BlogEntry)it.next();
+            assertTrue(Atom10Parser.isAbsoluteURI(blogEntry.getToken()));
+            blogEntry.delete();
+        }       
+    }
+    
+    
+    public static Test suite() {
+        TestSuite suite = new TestSuite(SimpleBlogClientTest.class);        
+        return suite;
+    }
+}
+
+
+