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; + } +} + + +