diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2e975ff
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/.classpath
+/.project
+/.settings
+/target
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6b0b127
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,203 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
+
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..fe019d6
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,26 @@
+
+Portions of the Propono code are Copyright (C) 2007 Sun Microsystems, Inc.
+
+Sun Microsystems, Inc.
+4150 Network Circle, Santa Clara, California 95054, U.S.A.
+All rights reserved.
+
+U.S. Government Rights - Commercial software. Government users are subject to
+the Sun Microsystems, Inc. standard license agreement and applicable provisions
+of the FAR and its supplements.
+
+Use is subject to license terms.
+
+This distribution may include materials developed by third parties.
+
+Sun, Sun Microsystems, the Sun logo and Java are trademarks or registered
+trademarks of Sun Microsystems, Inc. in the U.S. and other countries.
+
+This product is covered and controlled by U.S. Export Control laws and may be
+subject to the export or import laws in other countries. Nuclear, missile,
+chemical biological weapons or nuclear maritime end uses or end users, whether
+direct or indirect, are strictly prohibited. Export or reexport to countries
+subject to U.S. embargo or to entities identified on U.S. export exclusion
+lists, including, but not limited to, the denied persons and specially
+designated nationals lists is strictly prohibited.
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8a9f1a5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,6 @@
+rome
+====
+
+ROME is a set of RSS and Atom Utilities for Java. It makes it easy to work in Java with most syndication formats: RSS 0.90, RSS 0.91 Netscape, RSS 0.91 Userland, RSS 0.92, RSS 0.93, RSS 0.94, RSS 1.0, RSS 2.0, Atom 0.3, Atom 1.0
+
+More Information: http://rometools.github.io/rome-propono/
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..f4768e9
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,122 @@
+
+
+ 4.0.0
+
+
+ com.rometools
+ rome-parent
+ 1.6.0-SNAPSHOT
+
+
+ rome-propono
+ 1.6.0-SNAPSHOT
+ jar
+
+ rome-propono
+
+ The ROME Propono subproject is a Java class library that
+ supports publishing protocols, specifically the Atom Publishing Protocol
+ and the legacy MetaWeblog API. Propono includes an Atom client library,
+ Atom server framework and a Blog client that supports both Atom protocol
+ and the MetaWeblog API.
+
+
+ http://rometools.github.io/rome-propono/
+
+
+ scm:git:ssh://github.com/rometools/rome-propono.git
+ scm:git:ssh://git@github.com/rometools/rome-propono.git
+ https://github.com/rometools/rome-propono
+
+
+
+
+ Dave Johnson
+ http://rollerweblogger.org/roller
+ -5
+
+
+ Robert Cooper
+ kebernet@gmail.com
+ http://www.screaming-penguin.com
+ -4
+
+
+
+
+
+ sonatype-nexus-snapshots
+ https://oss.sonatype.org/content/repositories/snapshots
+
+ false
+
+
+ true
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-scm-publish-plugin
+
+ gh-pages
+ ${project.scm.developerConnection}
+ ${project.build.directory}/site
+
+
+
+
+
+
+
+ com.rometools
+ rome
+ 1.6.0-SNAPSHOT
+
+
+ commons-httpclient
+ commons-httpclient
+
+
+ org.apache.commons
+ commons-lang3
+
+
+ commons-beanutils
+ commons-beanutils
+
+
+ org.apache.xmlrpc
+ xmlrpc-client
+
+
+ net.oauth.core
+ oauth
+
+
+ javax.servlet
+ servlet-api
+ provided
+
+
+ ch.qos.logback
+ logback-classic
+ test
+
+
+ junit
+ junit
+ test
+
+
+ jetty
+ jetty
+ test
+
+
+
+
diff --git a/src/main/java/com/rometools/propono/atom/client/AtomClientFactory.java b/src/main/java/com/rometools/propono/atom/client/AtomClientFactory.java
new file mode 100644
index 0000000..281baaf
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/client/AtomClientFactory.java
@@ -0,0 +1,44 @@
+/*
+ * 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 com.rometools.propono.atom.client;
+
+import com.rometools.propono.utils.ProponoException;
+import com.rometools.rome.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(final String uri, final AuthStrategy authStrategy) throws ProponoException {
+ return new ClientAtomService(uri, authStrategy);
+ }
+
+ /**
+ * Create ClientCollection bound to URI.
+ */
+ public static ClientCollection getCollection(final String uri, final AuthStrategy authStrategy) throws ProponoException {
+ return new ClientCollection(uri, authStrategy);
+ }
+}
diff --git a/src/main/java/com/rometools/propono/atom/client/AuthStrategy.java b/src/main/java/com/rometools/propono/atom/client/AuthStrategy.java
new file mode 100644
index 0000000..c40ebd1
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/client/AuthStrategy.java
@@ -0,0 +1,29 @@
+/*
+ * 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 com.rometools.propono.atom.client;
+
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+
+import com.rometools.propono.utils.ProponoException;
+
+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/com/rometools/propono/atom/client/BasicAuthStrategy.java b/src/main/java/com/rometools/propono/atom/client/BasicAuthStrategy.java
new file mode 100644
index 0000000..8bf51e4
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/client/BasicAuthStrategy.java
@@ -0,0 +1,42 @@
+/*
+ * 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 com.rometools.propono.atom.client;
+
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+
+import com.rometools.propono.utils.ProponoException;
+import com.rometools.rome.io.impl.Base64;
+
+public class BasicAuthStrategy implements AuthStrategy {
+ private final String credentials;
+
+ public BasicAuthStrategy(final String username, final String password) {
+ new Base64();
+ credentials = new String(Base64.encode((username + ":" + password).getBytes()));
+ }
+
+ public void init() throws ProponoException {
+ // op-op
+ }
+
+ @Override
+ public void addAuthentication(final HttpClient httpClient, final HttpMethodBase method) throws ProponoException {
+ httpClient.getParams().setAuthenticationPreemptive(true);
+ final String header = "Basic " + credentials;
+ method.setRequestHeader("Authorization", header);
+ }
+}
diff --git a/src/main/java/com/rometools/propono/atom/client/ClientAtomService.java b/src/main/java/com/rometools/propono/atom/client/ClientAtomService.java
new file mode 100644
index 0000000..77ca1ca
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/client/ClientAtomService.java
@@ -0,0 +1,139 @@
+/*
+ * 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 com.rometools.propono.atom.client;
+
+import java.io.InputStreamReader;
+import java.util.List;
+import java.util.Locale;
+
+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.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.input.SAXBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.rometools.propono.atom.common.AtomService;
+import com.rometools.propono.utils.ProponoException;
+import com.rometools.rome.feed.atom.Entry;
+import com.rometools.rome.io.impl.Atom10Parser;
+
+/**
+ * This class models an Atom Publising Protocol Service Document. It extends the common
+ * {@link com.rometools.rome.propono.atom.common.Collection} class to add a getEntry()
+ * method and to return {@link com.rometools.rome.propono.atom.client.ClientWorkspace} objects
+ * instead of common {@link com.rometools.rome.propono.atom.common.Workspace}s.
+ */
+public class ClientAtomService extends AtomService {
+
+ private static final Logger LOG = LoggerFactory.getLogger(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(final String uri, final AuthStrategy authStrategy) throws ProponoException {
+ this.uri = uri;
+ this.authStrategy = authStrategy;
+ final Document doc = getAtomServiceDocument();
+ parseAtomServiceDocument(doc);
+ }
+
+ /**
+ * Get full entry from service by entry edit URI.
+ */
+ public ClientEntry getEntry(final String uri) throws ProponoException {
+ final 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());
+ }
+ final Entry romeEntry = Atom10Parser.parseEntry(new InputStreamReader(method.getResponseBodyAsStream()), uri, Locale.US);
+ if (!romeEntry.isMediaEntry()) {
+ return new ClientEntry(this, null, romeEntry, false);
+ } else {
+ return new ClientMediaEntry(this, null, romeEntry, false);
+ }
+ } catch (final Exception e) {
+ throw new ProponoException("ERROR: getting or parsing entry/media", e);
+ } finally {
+ method.releaseConnection();
+ }
+ }
+
+ void addAuthentication(final HttpMethodBase method) throws ProponoException {
+ authStrategy.addAuthentication(httpClient, method);
+ }
+
+ AuthStrategy getAuthStrategy() {
+ return authStrategy;
+ }
+
+ private Document getAtomServiceDocument() throws ProponoException {
+ GetMethod method = null;
+ final 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);
+
+ final SAXBuilder builder = new SAXBuilder();
+ final String doc = method.getResponseBodyAsString();
+ LOG.debug(doc);
+ return builder.build(method.getResponseBodyAsStream());
+
+ } catch (final Throwable t) {
+ final String msg = "ERROR retrieving Atom Service Document, code: " + code;
+ LOG.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(final Document document) throws ProponoException {
+ final Element root = document.getRootElement();
+ final List spaces = root.getChildren("workspace", AtomService.ATOM_PROTOCOL);
+ for (final Element e : spaces) {
+ addWorkspace(new ClientWorkspace(e, this, uri));
+ }
+ }
+
+ /**
+ * Package access to httpClient.
+ */
+ HttpClient getHttpClient() {
+ return httpClient;
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/client/ClientCategories.java b/src/main/java/com/rometools/propono/atom/client/ClientCategories.java
new file mode 100644
index 0000000..229430d
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/client/ClientCategories.java
@@ -0,0 +1,68 @@
+/*
+ * 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 com.rometools.propono.atom.client;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+import org.jdom2.input.SAXBuilder;
+
+import com.rometools.propono.atom.common.Categories;
+import com.rometools.propono.utils.ProponoException;
+
+/**
+ * Models an Atom protocol Categories element, which may contain ROME Atom
+ * {@link com.rometools.rome.feed.atom.Category} elements.
+ */
+public class ClientCategories extends Categories {
+ private ClientCollection clientCollection = null;
+
+ /** Load select from XML element */
+ public ClientCategories(final Element e, final ClientCollection clientCollection) throws ProponoException {
+ this.clientCollection = clientCollection;
+ parseCategoriesElement(e);
+ if (getHref() != null) {
+ fetchContents();
+ }
+ }
+
+ public void fetchContents() throws ProponoException {
+ final 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());
+ }
+ final SAXBuilder builder = new SAXBuilder();
+ final Document catsDoc = builder.build(new InputStreamReader(method.getResponseBodyAsStream()));
+ parseCategoriesElement(catsDoc.getRootElement());
+
+ } catch (final IOException ioe) {
+ throw new ProponoException("ERROR: reading out-of-line categories", ioe);
+ } catch (final JDOMException jde) {
+ throw new ProponoException("ERROR: parsing out-of-line categories", jde);
+ } finally {
+ method.releaseConnection();
+ }
+ }
+}
diff --git a/src/main/java/com/rometools/propono/atom/client/ClientCollection.java b/src/main/java/com/rometools/propono/atom/client/ClientCollection.java
new file mode 100644
index 0000000..a96f0b8
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/client/ClientCollection.java
@@ -0,0 +1,225 @@
+/*
+ * 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 com.rometools.propono.atom.client;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+
+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.jdom2.Element;
+
+import com.rometools.propono.atom.common.AtomService;
+import com.rometools.propono.atom.common.Categories;
+import com.rometools.propono.atom.common.Collection;
+import com.rometools.propono.atom.common.Workspace;
+import com.rometools.propono.utils.ProponoException;
+import com.rometools.rome.feed.atom.Entry;
+import com.rometools.rome.io.impl.Atom10Parser;
+
+/**
+ * Models an Atom collection, extends Collection and adds methods for adding, retrieving, updateing
+ * and deleting entries.
+ */
+public class ClientCollection extends Collection {
+
+ private final boolean writable = true;
+
+ private HttpClient httpClient = null;
+ private AuthStrategy authStrategy = null;
+ private ClientWorkspace workspace = null;
+ private ClientAtomService service = null;
+
+ ClientCollection(final Element e, final ClientWorkspace workspace, final String baseURI) throws ProponoException {
+ super(e, baseURI);
+ this.workspace = workspace;
+ service = workspace.getAtomService();
+ httpClient = workspace.getAtomService().getHttpClient();
+ authStrategy = workspace.getAtomService().getAuthStrategy();
+ parseCollectionElement(e);
+ }
+
+ ClientCollection(final String href, final 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 (final Throwable t) {
+ throw new ProponoException("ERROR creating HTTPClient", t);
+ }
+ }
+
+ void addAuthentication(final 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(final String uri) throws ProponoException {
+ final 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());
+ }
+ final Entry romeEntry = Atom10Parser.parseEntry(new InputStreamReader(method.getResponseBodyAsStream()), uri, Locale.US);
+ if (!romeEntry.isMediaEntry()) {
+ return new ClientEntry(service, this, romeEntry, false);
+ } else {
+ return new ClientMediaEntry(service, this, romeEntry, false);
+ }
+ } catch (final 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(final String title, final String slug, final String contentType, final 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(final String title, final String slug, final String contentType, final 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(final ClientEntry entry) throws ProponoException {
+ if (!isWritable()) {
+ throw new ProponoException("Collection is not writable");
+ }
+ entry.addToCollection(this);
+ }
+
+ @Override
+ protected void parseCollectionElement(final Element element) throws ProponoException {
+ if (workspace == null) {
+ return;
+ }
+
+ setHref(element.getAttribute("href").getValue());
+
+ final 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());
+ }
+ }
+
+ final List acceptElems = element.getChildren("accept", AtomService.ATOM_PROTOCOL);
+ if (acceptElems != null && !acceptElems.isEmpty()) {
+ for (final Element acceptElem : acceptElems) {
+ addAccept(acceptElem.getTextTrim());
+ }
+ }
+
+ // Loop to parse element to Categories objects
+ final List catsElems = element.getChildren("categories", AtomService.ATOM_PROTOCOL);
+ for (final Element catsElem : catsElems) {
+ final Categories cats = new ClientCategories(catsElem, this);
+ addCategories(cats);
+ }
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/client/ClientEntry.java b/src/main/java/com/rometools/propono/atom/client/ClientEntry.java
new file mode 100644
index 0000000..1af10cb
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/client/ClientEntry.java
@@ -0,0 +1,267 @@
+/*
+ * 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 com.rometools.propono.atom.client;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+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.DeleteMethod;
+import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.methods.PutMethod;
+import org.apache.commons.httpclient.methods.StringRequestEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.rometools.propono.utils.ProponoException;
+import com.rometools.propono.utils.Utilities;
+import com.rometools.rome.feed.atom.Content;
+import com.rometools.rome.feed.atom.Entry;
+import com.rometools.rome.feed.atom.Link;
+import com.rometools.rome.io.impl.Atom10Generator;
+import com.rometools.rome.io.impl.Atom10Parser;
+
+/**
+ * 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 long serialVersionUID = 1L;
+
+ private static final Logger LOG = LoggerFactory.getLogger(ClientEntry.class);
+
+ private ClientAtomService service = null;
+ private ClientCollection collection = null;
+ protected boolean partial = false;
+
+ public ClientEntry(final ClientAtomService service, final ClientCollection collection) {
+ this.service = service;
+ this.collection = collection;
+ }
+
+ public ClientEntry(final ClientAtomService service, final ClientCollection collection, final Entry entry, final boolean partial) throws ProponoException {
+ this.service = service;
+ this.collection = collection;
+ this.partial = partial;
+ try {
+ BeanUtils.copyProperties(this, entry);
+ } catch (final 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(final String contentString, final String type) {
+ final Content newContent = new Content();
+ newContent.setType(type == null ? Content.HTML : type);
+ newContent.setValue(contentString);
+ final 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(final Content c) {
+ final 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().isEmpty()) {
+ final Content c = getContents().get(0);
+ return c;
+ }
+ return null;
+ }
+
+ /**
+ * Determines if entries are equal based on edit URI.
+ */
+ @Override
+ public boolean equals(final Object o) {
+ if (o instanceof ClientEntry) {
+ final 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
+ * getEntry() methods in {@link com.rometools.rome.propono.atom.common.Collection}
+ * or {@link com.rometools.rome.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");
+ }
+ final EntityEnclosingMethod method = new PutMethod(getEditURI());
+ addAuthentication(method);
+ final StringWriter sw = new StringWriter();
+ final int code = -1;
+ try {
+ Atom10Generator.serializeEntry(this, sw);
+ method.setRequestEntity(new StringRequestEntity(sw.toString(), null, null));
+ method.setRequestHeader("Content-type", "application/atom+xml; charset=utf-8");
+ getHttpClient().executeMethod(method);
+ final InputStream is = method.getResponseBodyAsStream();
+ if (method.getStatusCode() != 200 && method.getStatusCode() != 201) {
+ throw new ProponoException("ERROR HTTP status=" + method.getStatusCode() + " : " + Utilities.streamToString(is));
+ }
+
+ } catch (final Exception e) {
+ final String msg = "ERROR: updating entry, HTTP code: " + code;
+ LOG.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");
+ }
+ final DeleteMethod method = new DeleteMethod(getEditURI());
+ addAuthentication(method);
+ try {
+ getHttpClient().executeMethod(method);
+ } catch (final IOException ex) {
+ throw new ProponoException("ERROR: removing entry, HTTP code", ex);
+ } finally {
+ method.releaseConnection();
+ }
+ }
+
+ void setCollection(final 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++) {
+ final Link link = getOtherLinks().get(i);
+ if (link.getRel() != null && link.getRel().equals("edit")) {
+ return link.getHrefResolved();
+ }
+ }
+ return null;
+ }
+
+ void addToCollection(final ClientCollection col) throws ProponoException {
+ setCollection(col);
+ final EntityEnclosingMethod method = new PostMethod(getCollection().getHrefResolved());
+ addAuthentication(method);
+ final StringWriter sw = new StringWriter();
+ int code = -1;
+ try {
+ Atom10Generator.serializeEntry(this, sw);
+ method.setRequestEntity(new StringRequestEntity(sw.toString(), null, null));
+ method.setRequestHeader("Content-type", "application/atom+xml; charset=utf-8");
+ getHttpClient().executeMethod(method);
+ final InputStream is = method.getResponseBodyAsStream();
+ code = method.getStatusCode();
+ if (code != 200 && code != 201) {
+ throw new ProponoException("ERROR HTTP status=" + code + " : " + Utilities.streamToString(is));
+ }
+ final Entry romeEntry = Atom10Parser.parseEntry(new InputStreamReader(is), getCollection().getHrefResolved(), Locale.US);
+ BeanUtils.copyProperties(this, romeEntry);
+
+ } catch (final Exception e) {
+ final String msg = "ERROR: saving entry, HTTP code: " + code;
+ LOG.debug(msg, e);
+ throw new ProponoException(msg, e);
+ } finally {
+ method.releaseConnection();
+ }
+ final Header locationHeader = method.getResponseHeader("Location");
+ if (locationHeader == null) {
+ LOG.warn("WARNING added entry, but no location header returned");
+ } else if (getEditURI() == null) {
+ final List links = getOtherLinks();
+ final Link link = new Link();
+ link.setHref(locationHeader.getValue());
+ link.setRel("edit");
+ links.add(link);
+ setOtherLinks(links);
+ }
+ }
+
+ void addAuthentication(final 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;
+ }
+
+ @Override
+ public void setCreated(final Date d) {
+ // protected against null created property (an old Atom 0.3 property)
+ if (d != null) {
+ super.setCreated(d);
+ }
+ }
+}
diff --git a/src/main/java/com/rometools/propono/atom/client/ClientMediaEntry.java b/src/main/java/com/rometools/propono/atom/client/ClientMediaEntry.java
new file mode 100644
index 0000000..049f039
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/client/ClientMediaEntry.java
@@ -0,0 +1,304 @@
+/*
+ * 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 com.rometools.propono.atom.client;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import org.apache.commons.beanutils.BeanUtils;
+import org.apache.commons.httpclient.Header;
+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.PostMethod;
+import org.apache.commons.httpclient.methods.PutMethod;
+import org.apache.commons.httpclient.methods.StringRequestEntity;
+import org.jdom2.JDOMException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.rometools.propono.utils.ProponoException;
+import com.rometools.propono.utils.Utilities;
+import com.rometools.rome.feed.atom.Content;
+import com.rometools.rome.feed.atom.Entry;
+import com.rometools.rome.feed.atom.Link;
+import com.rometools.rome.io.FeedException;
+import com.rometools.rome.io.impl.Atom10Generator;
+import com.rometools.rome.io.impl.Atom10Parser;
+
+/**
+ * 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 long serialVersionUID = 1L;
+
+ private static final Logger LOG = LoggerFactory.getLogger(ClientMediaEntry.class);
+
+ private String slug = null;
+ private byte[] bytes;
+ private InputStream inputStream;
+
+ /**
+ * Create ClientMedieEntry for service and collection.
+ */
+ public ClientMediaEntry(final ClientAtomService service, final ClientCollection collection) {
+ super(service, collection);
+ }
+
+ public ClientMediaEntry(final ClientAtomService service, final ClientCollection collection, final Entry entry, final boolean partial)
+ throws ProponoException {
+ super(service, collection, entry, partial);
+ }
+
+ public ClientMediaEntry(final ClientAtomService service, final ClientCollection collection, final String title, final String slug,
+ final String contentType, final InputStream is) {
+ this(service, collection);
+ inputStream = is;
+ setTitle(title);
+ setSlug(slug);
+ final Content content = new Content();
+ content.setType(contentType);
+ final List contents = new ArrayList();
+ contents.add(content);
+ setContents(contents);
+ }
+
+ public ClientMediaEntry(final ClientAtomService service, final ClientCollection collection, final String title, final String slug,
+ final String contentType, final byte[] bytes) {
+ this(service, collection);
+ this.bytes = bytes;
+ setTitle(title);
+ setSlug(slug);
+ final Content content = new Content();
+ content.setType(contentType);
+ final 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(final 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(final 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++) {
+ final 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().isEmpty()) {
+ final Content c = 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");
+ }
+ final 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 (final IOException e) {
+ throw new ProponoException("ERROR: getting media entry", e);
+ }
+ }
+
+ /**
+ * Update entry on server.
+ */
+ @Override
+ public void update() throws ProponoException {
+ if (partial) {
+ throw new ProponoException("ERROR: attempt to update partial entry");
+ }
+ EntityEnclosingMethod method = null;
+ final Content updateContent = 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());
+ final StringWriter sw = new StringWriter();
+ Atom10Generator.serializeEntry(this, sw);
+ method.setRequestEntity(new StringRequestEntity(sw.toString(), null, null));
+ method.setRequestHeader("Content-type", "application/atom+xml; charset=utf8");
+ } else {
+ throw new ProponoException("ERROR: media entry has no edit URI or media-link URI");
+ }
+ getCollection().addAuthentication(method);
+ method.addRequestHeader("Title", getTitle());
+ getCollection().getHttpClient().executeMethod(method);
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ final InputStream is = method.getResponseBodyAsStream();
+ if (method.getStatusCode() != 200 && method.getStatusCode() != 201) {
+ throw new ProponoException("ERROR HTTP status=" + method.getStatusCode() + " : " + Utilities.streamToString(is));
+ }
+
+ } catch (final 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 */
+ @Override
+ void addToCollection(final ClientCollection col) throws ProponoException {
+ setCollection(col);
+ final EntityEnclosingMethod method = new PostMethod(col.getHrefResolved());
+ getCollection().addAuthentication(method);
+ try {
+ final Content c = 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();
+ }
+ final InputStream is = method.getResponseBodyAsStream();
+ if (method.getStatusCode() == 200 || method.getStatusCode() == 201) {
+ final Entry romeEntry = Atom10Parser.parseEntry(new InputStreamReader(is), col.getHrefResolved(), Locale.US);
+ BeanUtils.copyProperties(this, romeEntry);
+
+ } else {
+ throw new ProponoException("ERROR HTTP status-code=" + method.getStatusCode() + " status-line: " + method.getStatusLine());
+ }
+ } catch (final IOException ie) {
+ throw new ProponoException("ERROR: saving media entry", ie);
+ } catch (final JDOMException je) {
+ throw new ProponoException("ERROR: saving media entry", je);
+ } catch (final FeedException fe) {
+ throw new ProponoException("ERROR: saving media entry", fe);
+ } catch (final IllegalAccessException ae) {
+ throw new ProponoException("ERROR: saving media entry", ae);
+ } catch (final InvocationTargetException te) {
+ throw new ProponoException("ERROR: saving media entry", te);
+ }
+ final Header locationHeader = method.getResponseHeader("Location");
+ if (locationHeader == null) {
+ LOG.warn("WARNING added entry, but no location header returned");
+ } else if (getEditURI() == null) {
+ final List links = getOtherLinks();
+ final 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(final String slug) {
+ this.slug = slug;
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/client/ClientWorkspace.java b/src/main/java/com/rometools/propono/atom/client/ClientWorkspace.java
new file mode 100644
index 0000000..e06b7e0
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/client/ClientWorkspace.java
@@ -0,0 +1,62 @@
+/*
+ * 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 com.rometools.propono.atom.client;
+
+import java.util.List;
+
+import org.jdom2.Element;
+
+import com.rometools.propono.atom.common.AtomService;
+import com.rometools.propono.atom.common.Workspace;
+import com.rometools.propono.utils.ProponoException;
+
+/**
+ * Represents Atom protocol workspace on client-side. It extends the common
+ * {@link com.rometools.rome.propono.atom.common.Workspace} to return
+ * {@link com.rometools.rome.propono.atom.client.ClientCollection} objects instead of common
+ * {@link com.rometools.rome.propono.atom.common.Collection}s.
+ */
+public class ClientWorkspace extends Workspace {
+
+ private ClientAtomService atomService = null;
+
+ ClientWorkspace(final Element e, final ClientAtomService atomService, final 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(final Element element, final String baseURI) throws ProponoException {
+ final 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());
+ }
+ final List collections = element.getChildren("collection", AtomService.ATOM_PROTOCOL);
+ for (final Element e : collections) {
+ addCollection(new ClientCollection(e, this, baseURI));
+ }
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/client/EntryIterator.java b/src/main/java/com/rometools/propono/atom/client/EntryIterator.java
new file mode 100644
index 0000000..a92d2ba
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/client/EntryIterator.java
@@ -0,0 +1,129 @@
+/*
+ * 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 com.rometools.propono.atom.client;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.jdom2.Document;
+import org.jdom2.input.SAXBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.rometools.propono.utils.ProponoException;
+import com.rometools.rome.feed.atom.Entry;
+import com.rometools.rome.feed.atom.Feed;
+import com.rometools.rome.feed.atom.Link;
+import com.rometools.rome.io.WireFeedInput;
+
+/**
+ * Enables iteration over entries in Atom protocol collection.
+ */
+public class EntryIterator implements Iterator {
+
+ private static final Logger LOG = LoggerFactory.getLogger(EntryIterator.class);
+
+ private final ClientCollection collection;
+
+ private Iterator members = null;
+ private Feed col = null;
+ private final String collectionURI;
+ private String nextURI;
+
+ EntryIterator(final ClientCollection collection) throws ProponoException {
+ this.collection = collection;
+ collectionURI = collection.getHrefResolved();
+ nextURI = collectionURI;
+ getNextEntries();
+ }
+
+ /**
+ * Returns true if more entries are available.
+ */
+ @Override
+ public boolean hasNext() {
+ if (!members.hasNext()) {
+ try {
+ getNextEntries();
+ } catch (final Exception ignored) {
+ LOG.error("An error occured while getting next entries", ignored);
+ }
+ }
+ return members.hasNext();
+ }
+
+ /**
+ * Get next entry in collection.
+ */
+ @Override
+ public ClientEntry next() {
+ if (hasNext()) {
+ final Entry romeEntry = members.next();
+ try {
+ if (!romeEntry.isMediaEntry()) {
+ return new ClientEntry(null, collection, romeEntry, true);
+ } else {
+ return new ClientMediaEntry(null, collection, romeEntry, true);
+ }
+ } catch (final ProponoException e) {
+ throw new RuntimeException("Unexpected exception creating ClientEntry or ClientMedia", e);
+ }
+ }
+ throw new NoSuchElementException();
+ }
+
+ /**
+ * Remove entry is not implemented.
+ */
+ @Override
+ public void remove() {
+ // optional method, not implemented
+ }
+
+ private void getNextEntries() throws ProponoException {
+ if (nextURI == null) {
+ return;
+ }
+ final GetMethod colGet = new GetMethod(collection.getHrefResolved(nextURI));
+ collection.addAuthentication(colGet);
+ try {
+ collection.getHttpClient().executeMethod(colGet);
+ final SAXBuilder builder = new SAXBuilder();
+ final Document doc = builder.build(colGet.getResponseBodyAsStream());
+ final WireFeedInput feedInput = new WireFeedInput();
+ col = (Feed) feedInput.build(doc);
+ } catch (final 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();
+ col.getEntries().size();
+
+ nextURI = null;
+ final List altLinks = col.getOtherLinks();
+ if (altLinks != null) {
+ for (final Link link : altLinks) {
+ if ("next".equals(link.getRel())) {
+ nextURI = link.getHref();
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/client/GDataAuthStrategy.java b/src/main/java/com/rometools/propono/atom/client/GDataAuthStrategy.java
new file mode 100644
index 0000000..0912397
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/client/GDataAuthStrategy.java
@@ -0,0 +1,64 @@
+/*
+ * 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 com.rometools.propono.atom.client;
+
+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;
+
+import com.rometools.propono.utils.ProponoException;
+
+public class GDataAuthStrategy implements AuthStrategy {
+ private final String email;
+ private final String password;
+ private final String service;
+ private String authToken;
+
+ public GDataAuthStrategy(final String email, final String password, final String service) throws ProponoException {
+ this.email = email;
+ this.password = password;
+ this.service = service;
+ init();
+ }
+
+ private void init() throws ProponoException {
+ try {
+ final HttpClient httpClient = new HttpClient();
+ final PostMethod method = new PostMethod("https://www.google.com/accounts/ClientLogin");
+ final 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);
+
+ final String responseBody = method.getResponseBodyAsString();
+ final int authIndex = responseBody.indexOf("Auth=");
+
+ authToken = "GoogleLogin auth=" + responseBody.trim().substring(authIndex + 5);
+
+ } catch (final Throwable t) {
+ t.printStackTrace();
+ throw new ProponoException("ERROR obtaining Google authentication string", t);
+ }
+ }
+
+ @Override
+ public void addAuthentication(final HttpClient httpClient, final HttpMethodBase method) throws ProponoException {
+ httpClient.getParams().setAuthenticationPreemptive(true);
+ method.setRequestHeader("Authorization", authToken);
+ }
+}
diff --git a/src/main/java/com/rometools/propono/atom/client/NoAuthStrategy.java b/src/main/java/com/rometools/propono/atom/client/NoAuthStrategy.java
new file mode 100644
index 0000000..2d45687
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/client/NoAuthStrategy.java
@@ -0,0 +1,33 @@
+/*
+ * 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 com.rometools.propono.atom.client;
+
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+
+import com.rometools.propono.utils.ProponoException;
+
+/**
+ * No authentication
+ */
+public class NoAuthStrategy implements AuthStrategy {
+
+ @Override
+ public void addAuthentication(final HttpClient httpClient, final HttpMethodBase method) throws ProponoException {
+ // no-op
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/client/OAuthStrategy.java b/src/main/java/com/rometools/propono/atom/client/OAuthStrategy.java
new file mode 100644
index 0000000..ae610f6
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/client/OAuthStrategy.java
@@ -0,0 +1,295 @@
+/*
+ * 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 com.rometools.propono.atom.client;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+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;
+
+import com.rometools.propono.utils.ProponoException;
+
+/**
+ * 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 final String username;
+ private final String consumerKey;
+ private final String consumerSecret;
+ private final String keyType;
+
+ private final String reqUrl;
+ private final String authzUrl;
+ private final String accessUrl;
+
+ private final String nonce;
+ private final 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(final String username, final String key, final String secret, final String keyType, final String reqUrl, final String authzUrl,
+ final String accessUrl) throws ProponoException {
+
+ this.username = username;
+ this.reqUrl = reqUrl;
+ this.authzUrl = authzUrl;
+ this.accessUrl = accessUrl;
+ consumerKey = key;
+ consumerSecret = secret;
+ this.keyType = keyType;
+
+ nonce = UUID.randomUUID().toString();
+ timestamp = new Date().getTime() / 1000L;
+
+ init();
+ }
+
+ private void init() throws ProponoException {
+ callOAuthUri(reqUrl);
+ callOAuthUri(authzUrl);
+ callOAuthUri(accessUrl);
+ }
+
+ @Override
+ public void addAuthentication(final HttpClient httpClient, final 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;
+ @SuppressWarnings("unchecked")
+ final List parameters = new ParameterParser().parse(qstring, '&');
+ originalqlist = parameters;
+ } else {
+ originalqlist = new ArrayList();
+ }
+
+ // put query string into hashmap form to please OAuth.net classes
+ final Map params = new HashMap();
+ for (final Object element : originalqlist) {
+ final NameValuePair pair = (NameValuePair) element;
+ 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;
+ final OAuthServiceProvider provider = new OAuthServiceProvider(reqUrl, authzUrl, accessUrl);
+ final OAuthConsumer consumer = new OAuthConsumer(null, consumerKey, consumerSecret, provider);
+ final 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 (final 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(final String uri) throws ProponoException {
+
+ final HttpClient httpClient = new HttpClient();
+
+ final HttpMethodBase method;
+ final String content;
+
+ final Map params = new HashMap();
+ 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");
+
+ final OAuthServiceProvider provider = new OAuthServiceProvider(reqUrl, authzUrl, accessUrl);
+ final OAuthConsumer consumer = new OAuthConsumer(null, consumerKey, consumerSecret, provider);
+ final OAuthAccessor accessor = new OAuthAccessor(consumer);
+
+ if (state == State.UNAUTHORIZED) {
+
+ try {
+ final OAuthMessage message = new OAuthMessage("GET", uri, params.entrySet());
+ message.sign(accessor);
+
+ final String finalUri = OAuth.addParameters(message.URL, message.getParameters());
+ method = new GetMethod(finalUri);
+ httpClient.executeMethod(method);
+ content = method.getResponseBodyAsString();
+
+ } catch (final 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;
+
+ final OAuthMessage message = new OAuthMessage("POST", uri, params.entrySet());
+ message.sign(accessor);
+
+ final String finalUri = OAuth.addParameters(message.URL, message.getParameters());
+ method = new PostMethod(finalUri);
+ httpClient.executeMethod(method);
+ content = method.getResponseBodyAsString();
+
+ } catch (final 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;
+
+ final OAuthMessage message = new OAuthMessage("GET", uri, params.entrySet());
+ message.sign(accessor);
+
+ final String finalUri = OAuth.addParameters(message.URL, message.getParameters());
+ method = new GetMethod(finalUri);
+ httpClient.executeMethod(method);
+ content = method.getResponseBodyAsString();
+
+ } catch (final 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) {
+ final String[] settings = content.split("&");
+ for (final String setting2 : settings) {
+ final String[] setting = setting2.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];
+ }
+ }
+ }
+ }
+
+ // TODO review switch without 'default'
+
+ 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/com/rometools/propono/atom/client/atomclient-diagram.gif b/src/main/java/com/rometools/propono/atom/client/atomclient-diagram.gif
new file mode 100644
index 0000000..21b1094
Binary files /dev/null and b/src/main/java/com/rometools/propono/atom/client/atomclient-diagram.gif differ
diff --git a/src/main/java/com/rometools/propono/atom/common/AtomService.java b/src/main/java/com/rometools/propono/atom/common/AtomService.java
new file mode 100644
index 0000000..43d8a64
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/common/AtomService.java
@@ -0,0 +1,115 @@
+/*
+ * 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 com.rometools.propono.atom.common;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.Namespace;
+
+import com.rometools.propono.utils.ProponoException;
+
+/**
+ * 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(final Workspace workspace) {
+ workspaces.add(workspace);
+ }
+
+ /**
+ * Get Workspaces available from service.
+ */
+ public List getWorkspaces() {
+ return workspaces;
+ }
+
+ /**
+ * Set Workspaces of service.
+ */
+ public void setWorkspaces(final 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(final String title) {
+ for (final Object element : workspaces) {
+ final Workspace ws = (Workspace) element;
+ if (title.equals(ws.getTitle())) {
+ return ws;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Deserialize an Atom service XML document into an object
+ */
+ public static AtomService documentToService(final Document document) throws ProponoException {
+ final AtomService service = new AtomService();
+ final Element root = document.getRootElement();
+ final List spaces = root.getChildren("workspace", ATOM_PROTOCOL);
+ for (final Element e : spaces) {
+ service.addWorkspace(Workspace.elementToWorkspace(e));
+ }
+ return service;
+ }
+
+ /**
+ * Serialize an AtomService object into an XML document
+ */
+ public Document serviceToDocument() {
+ final AtomService service = this;
+
+ final Document doc = new Document();
+ final Element root = new Element("service", ATOM_PROTOCOL);
+ doc.setRootElement(root);
+ final List spaces = service.getWorkspaces();
+ for (final Workspace space : spaces) {
+ root.addContent(space.workspaceToElement());
+ }
+ return doc;
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/common/Categories.java b/src/main/java/com/rometools/propono/atom/common/Categories.java
new file mode 100644
index 0000000..c3bde6e
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/common/Categories.java
@@ -0,0 +1,159 @@
+/*
+ * 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 com.rometools.propono.atom.common;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jdom2.Element;
+
+import com.rometools.rome.feed.atom.Category;
+import com.rometools.rome.io.impl.Atom10Parser;
+
+/**
+ * Models an Atom protocol Categories element, which may contain ROME Atom
+ * {@link com.rometools.rome.feed.atom.Category} elements.
+ */
+public class Categories {
+
+ private final List categories = new ArrayList();
+ 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(final Element e, final String baseURI) {
+ categoriesElement = e;
+ this.baseURI = baseURI;
+ parseCategoriesElement(e);
+ }
+
+ /** Add category list of those specified */
+ public void addCategory(final Category cat) {
+ categories.add(cat);
+ }
+
+ /**
+ * Iterate over Category objects
+ *
+ * @return List of ROME Atom {@link com.rometools.rome.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(final 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(final 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(final 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() {
+ final Categories cats = this;
+ final 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 elements
+ for (final Object element : cats.getCategories()) {
+ final Category cat = (Category) element;
+ final 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(final 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 elemenents to Category objects
+ final List catElems = catsElem.getChildren("category", AtomService.ATOM_FORMAT);
+ for (final Element catElem : catElems) {
+ final 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/com/rometools/propono/atom/common/Collection.java b/src/main/java/com/rometools/propono/atom/common/Collection.java
new file mode 100644
index 0000000..9fb0834
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/common/Collection.java
@@ -0,0 +1,258 @@
+/*
+ * 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 com.rometools.propono.atom.common;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jdom2.Element;
+
+import com.rometools.propono.utils.ProponoException;
+import com.rometools.rome.io.impl.Atom10Parser;
+
+/**
+ * Models an Atom workspace collection.
+ */
+public class Collection {
+
+ public static final String ENTRY_TYPE = "application/atom+xml;type=entry";
+
+ private final List categories = new ArrayList();
+
+ 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();
+ private String href = null;
+
+ /**
+ * 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(final String title, final String titleType, final String href) {
+ this.title = title;
+ this.titleType = titleType;
+ this.href = href;
+ }
+
+ /** Load self from XML element */
+ public Collection(final Element e) throws ProponoException {
+ collectionElement = e;
+ parseCollectionElement(e);
+ }
+
+ /** Load self from XML element and base URI for resolving relative URIs */
+ public Collection(final Element e, final String baseURI) throws ProponoException {
+ collectionElement = e;
+ this.baseURI = baseURI;
+ parseCollectionElement(e);
+ }
+
+ /**
+ * List of content-type ranges accepted by collection.
+ */
+ public List getAccepts() {
+ return accepts;
+ }
+
+ public void addAccept(final String accept) {
+ accepts.add(accept);
+ }
+
+ public void setAccepts(final List accepts) {
+ this.accepts = accepts;
+ }
+
+ /** The URI of the collection */
+ public String getHref() {
+ return href;
+ }
+
+ /**
+ * Set URI of collection
+ */
+ public void setHref(final 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) {
+ final 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(final String relativeUri) {
+ if (Atom10Parser.isAbsoluteURI(relativeUri)) {
+ return relativeUri;
+ } else if (baseURI != null && collectionElement != null) {
+ final 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(final 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(final String titleType) {
+ this.titleType = titleType;
+ }
+
+ /** Workspace can have multiple Categories objects */
+ public void addCategories(final Categories cats) {
+ categories.add(cats);
+ }
+
+ /**
+ * Get categories allowed by collection.
+ *
+ * @return Collection of {@link com.rometools.rome.propono.atom.common.Categories} objects.
+ */
+ public List getCategories() {
+ return categories;
+ }
+
+ /**
+ * Returns true if contentType is accepted by collection.
+ */
+ public boolean accepts(final String ct) {
+ for (final Object element : accepts) {
+ final String accept = (String) element;
+ if (accept != null && accept.trim().equals("*/*")) {
+ return true;
+ }
+ final String entryType = "application/atom+xml";
+ final 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 {
+ final String[] rules = accepts.toArray(new String[accepts.size()]);
+ for (final String rule2 : rules) {
+ String rule = rule2.trim();
+ if (rule.equals(ct)) {
+ return true;
+ }
+ final 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() {
+ final Collection collection = this;
+ final Element element = new Element("collection", AtomService.ATOM_PROTOCOL);
+ element.setAttribute("href", collection.getHref());
+
+ final 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 elements
+ for (final Object element2 : collection.getCategories()) {
+ final Categories cats = (Categories) element2;
+ element.addContent(cats.categoriesToElement());
+ }
+
+ for (final Object element2 : collection.getAccepts()) {
+ final String range = (String) element2;
+ final 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(final Element element) throws ProponoException {
+ return new Collection(element);
+ }
+
+ protected void parseCollectionElement(final Element element) throws ProponoException {
+ setHref(element.getAttribute("href").getValue());
+
+ final 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());
+ }
+ }
+
+ final List acceptElems = element.getChildren("accept", AtomService.ATOM_PROTOCOL);
+ if (acceptElems != null && !acceptElems.isEmpty()) {
+ for (final Element acceptElem : acceptElems) {
+ addAccept(acceptElem.getTextTrim());
+ }
+ }
+
+ // Loop to parse element to Categories objects
+ final List catsElems = element.getChildren("categories", AtomService.ATOM_PROTOCOL);
+ for (final Element catsElem : catsElems) {
+ final Categories cats = new Categories(catsElem, baseURI);
+ addCategories(cats);
+ }
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/common/Workspace.java b/src/main/java/com/rometools/propono/atom/common/Workspace.java
new file mode 100644
index 0000000..c7452c2
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/common/Workspace.java
@@ -0,0 +1,148 @@
+/*
+ * 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 com.rometools.propono.atom.common;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jdom2.Element;
+
+import com.rometools.propono.utils.ProponoException;
+
+/**
+ * Models an Atom workspace.
+ */
+public class Workspace {
+
+ private String title = null;
+ private String titleType = null; // may be TEXT, HTML, XHTML
+ private final 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(final String title, final String titleType) {
+ this.title = title;
+ this.titleType = titleType;
+ }
+
+ public Workspace(final Element elem) throws ProponoException {
+ parseWorkspaceElement(elem);
+ }
+
+ /** Iterate over collections in workspace */
+ public List getCollections() {
+ return collections;
+ }
+
+ /** Add new collection to workspace */
+ public void addCollection(final Collection col) {
+ collections.add(col);
+ }
+
+ /**
+ * DefaultWorkspace must have a human readable title
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Set title of workspace.
+ */
+ public void setTitle(final 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(final 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(final String title, final String contentType) {
+ for (final Object element : collections) {
+ final Collection col = (Collection) element;
+ 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(final Element element) throws ProponoException {
+ return new Workspace(element);
+ }
+
+ /**
+ * Serialize an AtomService.DefaultWorkspace object into an XML element
+ */
+ public Element workspaceToElement() {
+ final Workspace space = this;
+
+ final Element element = new Element("workspace", AtomService.ATOM_PROTOCOL);
+
+ final 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);
+
+ for (final Collection col : space.getCollections()) {
+ element.addContent(col.collectionToElement());
+ }
+
+ return element;
+ }
+
+ /** Deserialize a Atom workspace XML element into an object */
+ protected void parseWorkspaceElement(final Element element) throws ProponoException {
+ final 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());
+ }
+ final List collections = element.getChildren("collection", AtomService.ATOM_PROTOCOL);
+ for (final Element e : collections) {
+ addCollection(new Collection(e));
+ }
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/common/rome/AppModule.java b/src/main/java/com/rometools/propono/atom/common/rome/AppModule.java
new file mode 100644
index 0000000..dde22be
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/common/rome/AppModule.java
@@ -0,0 +1,44 @@
+/*
+ * 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 com.rometools.propono.atom.common.rome;
+
+import java.util.Date;
+
+import com.rometools.rome.feed.module.Module;
+
+/**
+ * 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/com/rometools/propono/atom/common/rome/AppModuleGenerator.java b/src/main/java/com/rometools/propono/atom/common/rome/AppModuleGenerator.java
new file mode 100644
index 0000000..7f57b6d
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/common/rome/AppModuleGenerator.java
@@ -0,0 +1,87 @@
+/*
+ * 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 com.rometools.propono.atom.common.rome;
+
+import java.text.SimpleDateFormat;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.TimeZone;
+
+import org.jdom2.Element;
+import org.jdom2.Namespace;
+
+import com.rometools.rome.feed.module.Module;
+import com.rometools.rome.io.ModuleGenerator;
+
+/**
+ * Creates JDOM representation for APP Extension Module.
+ */
+public class AppModuleGenerator implements ModuleGenerator {
+
+ private static final Namespace APP_NS = Namespace.getNamespace("app", AppModule.URI);
+
+ @Override
+ public String getNamespaceUri() {
+ return AppModule.URI;
+ }
+
+ private static final Set NAMESPACES;
+
+ static {
+ final Set nss = new HashSet();
+ nss.add(APP_NS);
+ NAMESPACES = Collections.unmodifiableSet(nss);
+ }
+
+ /** Get namespaces associated with this module */
+ @Override
+ public Set getNamespaces() {
+ return NAMESPACES;
+ }
+
+ /** Generate JDOM element for module and add it to parent element */
+ @Override
+ public void generate(final Module module, final Element parent) {
+ final AppModule m = (AppModule) module;
+
+ if (m.getDraft() != null) {
+ final String draft = m.getDraft().booleanValue() ? "yes" : "no";
+ final Element control = new Element("control", APP_NS);
+ control.addContent(generateSimpleElement("draft", draft));
+ parent.addContent(control);
+ }
+ if (m.getEdited() != null) {
+ final Element edited = new Element("edited", APP_NS);
+ // Inclulde millis in date/time
+ final 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(final String name, final String value) {
+ final Element element = new Element(name, APP_NS);
+ element.addContent(value);
+ return element;
+ }
+}
diff --git a/src/main/java/com/rometools/propono/atom/common/rome/AppModuleImpl.java b/src/main/java/com/rometools/propono/atom/common/rome/AppModuleImpl.java
new file mode 100644
index 0000000..c1a42e3
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/common/rome/AppModuleImpl.java
@@ -0,0 +1,80 @@
+/*
+ * 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 com.rometools.propono.atom.common.rome;
+
+import java.util.Date;
+
+import com.rometools.rome.feed.CopyFrom;
+import com.rometools.rome.feed.module.ModuleImpl;
+
+/**
+ * Bean representation of APP module.
+ */
+public class AppModuleImpl extends ModuleImpl implements AppModule {
+
+ private static final long serialVersionUID = 1L;
+
+ private boolean draft = false;
+ private Date edited = null;
+
+ public AppModuleImpl() {
+ super(AppModule.class, AppModule.URI);
+ }
+
+ /** True if entry is draft */
+ @Override
+ public Boolean getDraft() {
+ return draft ? Boolean.TRUE : Boolean.FALSE;
+ }
+
+ /** Set to true if entry is draft */
+ @Override
+ public void setDraft(final Boolean draft) {
+ this.draft = draft.booleanValue();
+ }
+
+ /** Time of last edit */
+ @Override
+ public Date getEdited() {
+ return edited;
+ }
+
+ /** Set time of last edit */
+ @Override
+ public void setEdited(final Date edited) {
+ this.edited = edited;
+ }
+
+ /** Get interface class of module */
+ @Override
+ public Class getInterface() {
+ return AppModule.class;
+ }
+
+ /** Copy from other module */
+ @Override
+ public void copyFrom(final CopyFrom obj) {
+ final AppModule m = (AppModule) obj;
+ setDraft(m.getDraft());
+ setEdited(m.getEdited());
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/common/rome/AppModuleParser.java b/src/main/java/com/rometools/propono/atom/common/rome/AppModuleParser.java
new file mode 100644
index 0000000..a9d0e87
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/common/rome/AppModuleParser.java
@@ -0,0 +1,72 @@
+/*
+ * 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 com.rometools.propono.atom.common.rome;
+
+import java.util.Locale;
+
+import org.jdom2.Element;
+import org.jdom2.Namespace;
+
+import com.rometools.rome.feed.module.Module;
+import com.rometools.rome.io.ModuleParser;
+import com.rometools.rome.io.impl.DateParser;
+
+/**
+ * Parses APP module information from a JDOM element and into AppModule form.
+ */
+public class AppModuleParser implements ModuleParser {
+
+ /** Get URI of module namespace */
+ @Override
+ public String getNamespaceUri() {
+ return AppModule.URI;
+ }
+
+ /** Get namespace of module */
+ public Namespace getContentNamespace() {
+ return Namespace.getNamespace(AppModule.URI);
+ }
+
+ /** Parse JDOM element into module */
+ @Override
+ public Module parse(final Element elem, final Locale locale) {
+ final AppModule m = new AppModuleImpl();
+ final Element control = elem.getChild("control", getContentNamespace());
+ if (control != null) {
+ final 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);
+ }
+ }
+ }
+ final Element edited = elem.getChild("edited", getContentNamespace());
+ if (edited != null) {
+ try {
+ m.setEdited(DateParser.parseW3CDateTime(edited.getTextTrim(), locale));
+ } catch (final Exception ignored) {
+ }
+ }
+ return m;
+ }
+}
diff --git a/src/main/java/com/rometools/propono/atom/server/AtomException.java b/src/main/java/com/rometools/propono/atom/server/AtomException.java
new file mode 100644
index 0000000..c8d06e4
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/AtomException.java
@@ -0,0 +1,59 @@
+/*
+ * 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 com.rometools.propono.atom.server;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Exception thrown by {@link com.rometools.rome.propono.atom.server.AtomHandler} and extended by
+ * other Propono Atom exception classes.
+ */
+public class AtomException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ /** Construct new exception */
+ public AtomException() {
+ super();
+ }
+
+ /** Construct new exception with message */
+ public AtomException(final String msg) {
+ super(msg);
+ }
+
+ /** Contruct new exception with message and wrapping existing exception */
+ public AtomException(final String msg, final Throwable t) {
+ super(msg, t);
+ }
+
+ /** Construct new exception to wrap existing one. */
+ public AtomException(final 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/com/rometools/propono/atom/server/AtomHandler.java b/src/main/java/com/rometools/propono/atom/server/AtomHandler.java
new file mode 100644
index 0000000..f08762b
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/AtomHandler.java
@@ -0,0 +1,150 @@
+/*
+ * 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 com.rometools.propono.atom.server;
+
+import com.rometools.propono.atom.common.AtomService;
+import com.rometools.propono.atom.common.Categories;
+import com.rometools.rome.feed.atom.Entry;
+import com.rometools.rome.feed.atom.Feed;
+
+/**
+ * Interface for handling single Atom protocol requests.
+ *
+ *
+ * To create your own Atom protocol implementation you must implement this interface and create a
+ * concrete sub-class of {@link com.rometools.rome.propono.atom.server.AtomHandlerFactory} which is
+ * capable of instantiating it.
+ *
+ */
+public interface AtomHandler {
+ /**
+ * Get username of authenticated user. Return the username of the authenticated user
+ */
+ public String getAuthenticatedUsername();
+
+ /**
+ * Return {@link com.rometools.rome.propono.atom.common.AtomService} object that contains the
+ * {@link com.rometools.rome.propono.atom.common.Workspace} objects available to the currently
+ * authenticated user and within those the
+ * {@link com.rometools.rome.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/com/rometools/propono/atom/server/AtomHandlerFactory.java b/src/main/java/com/rometools/propono/atom/server/AtomHandlerFactory.java
new file mode 100644
index 0000000..3bd635f
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/AtomHandlerFactory.java
@@ -0,0 +1,100 @@
+/*
+ * 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 com.rometools.propono.atom.server;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Defines a factory that enables the {@link com.rometools.rome.propono.atom.server.AtomServlet} to
+ * obtain an {@link com.rometools.rome.propono.atom.server.AtomHandler} that handles an Atom
+ * request.
+ *
+ *
+ * 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.rometools.rome.propono.atom.server.AtomHandler} impementation.
+ *
+ */
+public abstract class AtomHandlerFactory {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AtomHandlerFactory.class);
+
+ private static final String DEFAULT_PROPERTY_NAME = "com.rometools.propono.atom.server.AtomHandlerFactory";
+ private static final String FALLBACK_IMPL_NAME = "com.rometools.propono.atom.server.impl.FileBasedAtomHandlerFactory";
+
+ /*
+ *
Protected constructor to prevent instantiation. Use {@link #newInstance()}.
+ */
+ protected AtomHandlerFactory() {
+ }
+
+ /**
+ * Obtain a new instance of a AtomHandlerFactory. This static method creates a new
+ * factory instance. This method uses the following ordered lookup procedure to determine the
+ * AtomHandlerFactory implementation class to load:
+ *
+ *
+ * Use the com.rometools.rome.propono.atom.server.AtomHandlerFactory system
+ * property.
+ *
+ * Use the properties file "/propono.properties" in the classpath. This configuration file is in
+ * standard java.util.Properties 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.
+ *
+ * If not available, to determine the classname. The Services API will look for a classname in
+ * the file: META-INF/services/com.rometools.rome.AtomHandlerFactory in jars
+ * available to the runtime.
+ *
+ * Platform default AtomHandlerFactory instance.
+ *
+ *
+ * Once an application has obtained a reference to a AtomHandlerFactory it can use
+ * the factory to configure and obtain parser instances.
+ *
+ * @return New instance of a AtomHandlerFactory
+ *
+ * @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 (final ConfigurationError e) {
+ LOG.error("An error occured while finding factory", e);
+ throw new FactoryConfigurationError(e.getException(), e.getMessage());
+ }
+ }
+
+ /**
+ * Creates a new instance of a {@link com.rometools.rome.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/com/rometools/propono/atom/server/AtomMediaResource.java b/src/main/java/com/rometools/propono/atom/server/AtomMediaResource.java
new file mode 100644
index 0000000..6b427d7
--- /dev/null
+++ b/src/main/java/com/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 com.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 (final Exception ignored) {
+ }
+ }
+ }
+
+ public AtomMediaResource(final File resource) throws FileNotFoundException {
+ contentType = map.getContentType(resource.getName());
+ contentLength = resource.length();
+ lastModified = new Date(resource.lastModified());
+ inputStream = new FileInputStream(resource);
+ }
+
+ public AtomMediaResource(final String name, final long length, final Date lastModified, final InputStream is) throws FileNotFoundException {
+ contentType = map.getContentType(name);
+ contentLength = length;
+ this.lastModified = lastModified;
+ inputStream = is;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public void setContentType(final String contentType) {
+ this.contentType = contentType;
+ }
+
+ public long getContentLength() {
+ return contentLength;
+ }
+
+ public void setContentLength(final long contentLength) {
+ this.contentLength = contentLength;
+ }
+
+ public InputStream getInputStream() {
+ return inputStream;
+ }
+
+ public void setInputStream(final InputStream inputStream) {
+ this.inputStream = inputStream;
+ }
+
+ public Date getLastModified() {
+ return lastModified;
+ }
+
+ public void setLastModified(final Date lastModified) {
+ this.lastModified = lastModified;
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/server/AtomNotAuthorizedException.java b/src/main/java/com/rometools/propono/atom/server/AtomNotAuthorizedException.java
new file mode 100644
index 0000000..8cd2e00
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/AtomNotAuthorizedException.java
@@ -0,0 +1,58 @@
+/*
+ * 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 com.rometools.propono.atom.server;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Exception to be thrown by AtomHandler implementations in the case that a user is not
+ * authorized to access a resource.
+ */
+public class AtomNotAuthorizedException extends AtomException {
+
+ private static final long serialVersionUID = 1L;
+
+ /** Construct new exception */
+ public AtomNotAuthorizedException() {
+ super();
+ }
+
+ /** Construct new exception with message */
+ public AtomNotAuthorizedException(final String msg) {
+ super(msg);
+ }
+
+ /** Construct new exception with message and root cause */
+ public AtomNotAuthorizedException(final String msg, final Throwable t) {
+ super(msg, t);
+ }
+
+ /** Construct new exception to wrap root cause */
+ public AtomNotAuthorizedException(final Throwable t) {
+ super(t);
+ }
+
+ /** Get HTTP status code of exception (HTTP 403 unauthorized) */
+ @Override
+ public int getStatus() {
+ return HttpServletResponse.SC_UNAUTHORIZED;
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/server/AtomNotFoundException.java b/src/main/java/com/rometools/propono/atom/server/AtomNotFoundException.java
new file mode 100644
index 0000000..4655656
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/AtomNotFoundException.java
@@ -0,0 +1,56 @@
+/*
+ * 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 com.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 {
+
+ private static final long serialVersionUID = 1L;
+
+ /** Construct new exception */
+ public AtomNotFoundException() {
+ super();
+ }
+
+ /** Construct new exception with message */
+ public AtomNotFoundException(final String msg) {
+ super(msg);
+ }
+
+ /** Construct new exception with message and root cause */
+ public AtomNotFoundException(final String msg, final Throwable t) {
+ super(msg, t);
+ }
+
+ /** Construct new exception with root cause */
+ public AtomNotFoundException(final Throwable t) {
+ super(t);
+ }
+
+ /** Get HTTP status code associated with exception (404 not found) */
+ @Override
+ public int getStatus() {
+ return HttpServletResponse.SC_NOT_FOUND;
+ }
+}
diff --git a/src/main/java/com/rometools/propono/atom/server/AtomRequest.java b/src/main/java/com/rometools/propono/atom/server/AtomRequest.java
new file mode 100644
index 0000000..cb8d1a2
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/AtomRequest.java
@@ -0,0 +1,137 @@
+/*
+ * 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 com.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/com/rometools/propono/atom/server/AtomRequestImpl.java b/src/main/java/com/rometools/propono/atom/server/AtomRequestImpl.java
new file mode 100644
index 0000000..3de3fc9
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/AtomRequestImpl.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 com.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(final HttpServletRequest wrapped) {
+ this.wrapped = wrapped;
+ }
+
+ @Override
+ public String getPathInfo() {
+ return wrapped.getPathInfo() != null ? wrapped.getPathInfo() : "";
+ }
+
+ @Override
+ public String getQueryString() {
+ return wrapped.getQueryString();
+ }
+
+ @Override
+ public String getRemoteUser() {
+ return wrapped.getRemoteUser();
+ }
+
+ @Override
+ public boolean isUserInRole(final String arg0) {
+ return wrapped.isUserInRole(arg0);
+ }
+
+ @Override
+ public Principal getUserPrincipal() {
+ return wrapped.getUserPrincipal();
+ }
+
+ @Override
+ public String getRequestURI() {
+ return wrapped.getRequestURI();
+ }
+
+ @Override
+ public StringBuffer getRequestURL() {
+ return wrapped.getRequestURL();
+ }
+
+ @Override
+ public int getContentLength() {
+ return wrapped.getContentLength();
+ }
+
+ @Override
+ public String getContentType() {
+ return wrapped.getContentType();
+ }
+
+ @Override
+ public String getParameter(final String arg0) {
+ return wrapped.getParameter(arg0);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Enumeration getParameterNames() {
+ return wrapped.getParameterNames();
+ }
+
+ @Override
+ public String[] getParameterValues(final String arg0) {
+ return wrapped.getParameterValues(arg0);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Map getParameterMap() {
+ return wrapped.getParameterMap();
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return wrapped.getInputStream();
+ }
+
+ @Override
+ public long getDateHeader(final String arg0) {
+ return wrapped.getDateHeader(arg0);
+ }
+
+ @Override
+ public String getHeader(final String arg0) {
+ return wrapped.getHeader(arg0);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Enumeration getHeaders(final String arg0) {
+ return wrapped.getHeaders(arg0);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Enumeration getHeaderNames() {
+ return wrapped.getHeaderNames();
+ }
+
+ @Override
+ public int getIntHeader(final String arg0) {
+ return wrapped.getIntHeader(arg0);
+ }
+}
diff --git a/src/main/java/com/rometools/propono/atom/server/AtomServlet.java b/src/main/java/com/rometools/propono/atom/server/AtomServlet.java
new file mode 100644
index 0000000..9933f33
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/AtomServlet.java
@@ -0,0 +1,371 @@
+/*
+ * 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 com.rometools.propono.atom.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Writer;
+import java.util.Collections;
+import java.util.Locale;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.jdom2.Document;
+import org.jdom2.output.Format;
+import org.jdom2.output.XMLOutputter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.rometools.propono.atom.common.AtomService;
+import com.rometools.propono.atom.common.Categories;
+import com.rometools.propono.utils.Utilities;
+import com.rometools.rome.feed.atom.Content;
+import com.rometools.rome.feed.atom.Entry;
+import com.rometools.rome.feed.atom.Feed;
+import com.rometools.rome.feed.atom.Link;
+import com.rometools.rome.io.WireFeedOutput;
+import com.rometools.rome.io.impl.Atom10Generator;
+import com.rometools.rome.io.impl.Atom10Parser;
+
+/**
+ * Atom Servlet implements Atom protocol by calling an
+ * {@link com.rometools.rome.propono.atom.server.AtomHandler} implementation. This servlet takes
+ * care of parsing incoming XML into ROME Atom {@link com.rometools.rome.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 {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 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 final Logger LOG = LoggerFactory.getLogger(AtomServlet.class);
+
+ static {
+ Atom10Parser.setResolveURIs(true);
+ }
+
+ // -----------------------------------------------------------------------------
+ /**
+ * Create an Atom request handler. TODO: make AtomRequestHandler implementation configurable.
+ */
+ private AtomHandler createAtomRequestHandler(final HttpServletRequest request, final HttpServletResponse response) throws ServletException {
+ final AtomHandlerFactory ahf = AtomHandlerFactory.newInstance();
+ return ahf.newAtomHandler(request, response);
+ }
+
+ // -----------------------------------------------------------------------------
+ /**
+ * Handles an Atom GET by calling handler and writing results to response.
+ */
+ @Override
+ protected void doGet(final HttpServletRequest req, final HttpServletResponse res) throws ServletException, IOException {
+ LOG.debug("Entering");
+ final AtomHandler handler = createAtomRequestHandler(req, res);
+ final String userName = handler.getAuthenticatedUsername();
+ if (userName != null) {
+ final AtomRequest areq = new AtomRequestImpl(req);
+ try {
+ if (handler.isAtomServiceURI(areq)) {
+ // return an Atom Service document
+ final AtomService service = handler.getAtomService(areq);
+ final Document doc = service.serviceToDocument();
+ res.setContentType("application/atomsvc+xml; charset=utf-8");
+ final Writer writer = res.getWriter();
+ final XMLOutputter outputter = new XMLOutputter();
+ outputter.setFormat(Format.getPrettyFormat());
+ outputter.output(doc, writer);
+ writer.close();
+ res.setStatus(HttpServletResponse.SC_OK);
+ } else if (handler.isCategoriesURI(areq)) {
+ final Categories cats = handler.getCategories(areq);
+ res.setContentType("application/xml");
+ final Writer writer = res.getWriter();
+ final Document catsDoc = new Document();
+ catsDoc.setRootElement(cats.categoriesToElement());
+ final XMLOutputter outputter = new XMLOutputter();
+ outputter.output(catsDoc, writer);
+ writer.close();
+ res.setStatus(HttpServletResponse.SC_OK);
+ } else if (handler.isCollectionURI(areq)) {
+ // return a collection
+ final Feed col = handler.getCollection(areq);
+ col.setFeedType(FEED_TYPE);
+ final WireFeedOutput wireFeedOutput = new WireFeedOutput();
+ final Document feedDoc = wireFeedOutput.outputJDom(col);
+ res.setContentType("application/atom+xml; charset=utf-8");
+ final Writer writer = res.getWriter();
+ final 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
+ final Entry entry = handler.getEntry(areq);
+ if (entry != null) {
+ res.setContentType("application/atom+xml; type=entry; charset=utf-8");
+ final Writer writer = res.getWriter();
+ Atom10Generator.serializeEntry(entry, writer);
+ writer.close();
+ } else {
+ res.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ }
+ } else if (handler.isMediaEditURI(areq)) {
+ final 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 (final AtomException ae) {
+ res.sendError(ae.getStatus(), ae.getMessage());
+ LOG.debug("An error occured while processing GET", ae);
+ } catch (final Exception e) {
+ res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+ LOG.debug("An error occured while 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.
+ */
+ @Override
+ protected void doPost(final HttpServletRequest req, final HttpServletResponse res) throws ServletException, IOException {
+ LOG.debug("Entering");
+ final AtomHandler handler = createAtomRequestHandler(req, res);
+ final String userName = handler.getAuthenticatedUsername();
+ if (userName != null) {
+ final AtomRequest areq = new AtomRequestImpl(req);
+ try {
+ if (handler.isCollectionURI(areq)) {
+
+ if (req.getContentType().startsWith("application/atom+xml")) {
+
+ // parse incoming entry
+ final Entry entry = Atom10Parser.parseEntry(new BufferedReader(new InputStreamReader(req.getInputStream(), "UTF-8")), null, Locale.US);
+
+ // call handler to post it
+ final Entry newEntry = handler.postEntry(areq, entry);
+
+ // set Location and Content-Location headers
+ for (final Object element : newEntry.getOtherLinks()) {
+ final Link link = (Link) element;
+ if ("edit".equals(link.getRel())) {
+ res.addHeader("Location", link.getHrefResolved());
+ break;
+ }
+ }
+ for (final Object element : newEntry.getAlternateLinks()) {
+ final Link link = (Link) element;
+ 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");
+
+ final Writer writer = res.getWriter();
+ Atom10Generator.serializeEntry(newEntry, writer);
+ writer.close();
+
+ } else if (req.getContentType() != null) {
+
+ // get incoming title and slug from HTTP header
+ final String title = areq.getHeader("Title");
+
+ // create new entry for resource, set title and type
+ final Entry resource = new Entry();
+ resource.setTitle(title);
+ final Content content = new Content();
+ content.setType(areq.getContentType());
+ resource.setContents(Collections.singletonList(content));
+
+ // hand input stream off to hander to post file
+ final Entry newEntry = handler.postMedia(areq, resource);
+
+ // set Location and Content-Location headers
+ for (final Object element : newEntry.getOtherLinks()) {
+ final Link link = (Link) element;
+ if ("edit".equals(link.getRel())) {
+ res.addHeader("Location", link.getHrefResolved());
+ break;
+ }
+ }
+ for (final Object element : newEntry.getAlternateLinks()) {
+ final Link link = (Link) element;
+ 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");
+
+ final 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 (final AtomException ae) {
+ res.sendError(ae.getStatus(), ae.getMessage());
+ LOG.debug("An error occured while processing POST", ae);
+ } catch (final Exception e) {
+ res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+ LOG.debug("An error occured while 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.
+ */
+ @Override
+ protected void doPut(final HttpServletRequest req, final HttpServletResponse res) throws ServletException, IOException {
+ LOG.debug("Entering");
+ final AtomHandler handler = createAtomRequestHandler(req, res);
+ final String userName = handler.getAuthenticatedUsername();
+ if (userName != null) {
+ final AtomRequest areq = new AtomRequestImpl(req);
+ try {
+ if (handler.isEntryURI(areq)) {
+
+ // parse incoming entry
+ final Entry unsavedEntry = Atom10Parser.parseEntry(new BufferedReader(new InputStreamReader(req.getInputStream(), "UTF-8")), null,
+ Locale.US);
+
+ // 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 (final AtomException ae) {
+ res.sendError(ae.getStatus(), ae.getMessage());
+ LOG.debug("An error occured while processing PUT", ae);
+ } catch (final Exception e) {
+ res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+ LOG.debug("An error occured while 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.
+ */
+ @Override
+ protected void doDelete(final HttpServletRequest req, final HttpServletResponse res) throws ServletException, IOException {
+ LOG.debug("Entering");
+ final AtomHandler handler = createAtomRequestHandler(req, res);
+ final String userName = handler.getAuthenticatedUsername();
+ if (userName != null) {
+ final 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 (final AtomException ae) {
+ res.sendError(ae.getStatus(), ae.getMessage());
+ LOG.debug("An error occured while processing DELETE", ae);
+ } catch (final Exception e) {
+ res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+ LOG.debug("An error occured while 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.
+ */
+ @Override
+ public void init(final 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/com/rometools/propono/atom/server/ConfigurationError.java b/src/main/java/com/rometools/propono/atom/server/ConfigurationError.java
new file mode 100644
index 0000000..1aca365
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/ConfigurationError.java
@@ -0,0 +1,21 @@
+package com.rometools.propono.atom.server;
+
+class ConfigurationError extends Error {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Exception exception;
+
+ /**
+ * Construct a new instance with the specified detail string and exception.
+ */
+ ConfigurationError(final String msg, final Exception x) {
+ super(msg);
+ exception = x;
+ }
+
+ Exception getException() {
+ return exception;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/rometools/propono/atom/server/FactoryConfigurationError.java b/src/main/java/com/rometools/propono/atom/server/FactoryConfigurationError.java
new file mode 100644
index 0000000..62a1b98
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/FactoryConfigurationError.java
@@ -0,0 +1,101 @@
+/*
+ * 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 com.rometools.propono.atom.server;
+
+/**
+ * Thrown when a problem with configuration with the
+ * {@link com.rometools.rome.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 {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Exception that represents the error.
+ */
+ private final Exception exception;
+
+ /**
+ * Create a new FactoryConfigurationError with no detail mesage.
+ */
+ public FactoryConfigurationError() {
+ super();
+ exception = null;
+ }
+
+ /**
+ * Create a new FactoryConfigurationError with the String specified
+ * as an error message.
+ *
+ * @param msg The error message for the exception.
+ */
+ public FactoryConfigurationError(final String msg) {
+ super(msg);
+ exception = null;
+ }
+
+ /**
+ * Create a new FactoryConfigurationError with a given Exception base
+ * cause of the error.
+ *
+ * @param e The exception to be encapsulated in a FactoryConfigurationError.
+ */
+ public FactoryConfigurationError(final Exception e) {
+ super(e.toString());
+ exception = e;
+ }
+
+ /**
+ * Create a new FactoryConfigurationError with the given Exception
+ * base cause and detail message.
+ *
+ * @param e The exception to be encapsulated in a FactoryConfigurationError
+ * @param msg The detail message.
+ */
+ public FactoryConfigurationError(final Exception e, final String msg) {
+ super(msg);
+ 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.
+ */
+ @Override
+ public String getMessage() {
+ final 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/com/rometools/propono/atom/server/FactoryFinder.java b/src/main/java/com/rometools/propono/atom/server/FactoryFinder.java
new file mode 100644
index 0000000..12f0b05
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/FactoryFinder.java
@@ -0,0 +1,248 @@
+/*
+ * 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 com.rometools.propono.atom.server;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Properties;
+
+/**
+ * Find {@link com.rometools.rome.propono.atom.server.AtomHandlerFactory} based on properties
+ * files.
+ */
+class FactoryFinder {
+
+ private static boolean debug = false;
+ private static Properties cacheProps = new Properties();
+ private static SecuritySupport ss = new SecuritySupport();
+ private static boolean firstTime = true;
+
+ private static void dPrint(final 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(final String className, ClassLoader cl, final 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 (final ClassNotFoundException x) {
+ if (doFallback) {
+ // Fall back to current classloader
+ cl = FactoryFinder.class.getClassLoader();
+ providerClass = cl.loadClass(className);
+ } else {
+ throw x;
+ }
+ }
+ }
+
+ final Object instance = providerClass.newInstance();
+ dPrint("created new instance of " + providerClass + " using ClassLoader: " + cl);
+ return instance;
+ } catch (final ClassNotFoundException x) {
+ throw new ConfigurationError("Provider " + className + " not found", x);
+ } catch (final 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(final String factoryId, final 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 {
+ final String systemProp = ss.getSystemProperty(factoryId);
+ if (systemProp != null) {
+ dPrint("found system property, value=" + systemProp);
+ return newInstance(systemProp, classLoader, true);
+ }
+ } catch (final 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 {
+ final String configFile = "/propono.properties";
+ String factoryClassName = null;
+ if (firstTime) {
+ synchronized (cacheProps) {
+ if (firstTime) {
+ try {
+ final InputStream is = FactoryFinder.class.getResourceAsStream(configFile);
+ firstTime = false;
+ if (is != null) {
+ dPrint("Read properties file: " + configFile);
+ cacheProps.load(is);
+ }
+ } catch (final Exception intentionallyIgnored) {
+ }
+ }
+ }
+ }
+ factoryClassName = cacheProps.getProperty(factoryId);
+
+ if (factoryClassName != null) {
+ dPrint("found in $java.home/propono.properties, value=" + factoryClassName);
+ return newInstance(factoryClassName, classLoader, true);
+ }
+ } catch (final Exception ex) {
+ if (debug) {
+ ex.printStackTrace();
+ }
+ }
+
+ // Try Jar Service Provider Mechanism
+ final 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(final String factoryId) throws ConfigurationError {
+
+ final 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 (final 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 (final 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;
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/server/SecuritySupport.java b/src/main/java/com/rometools/propono/atom/server/SecuritySupport.java
new file mode 100644
index 0000000..5f7b2b4
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/SecuritySupport.java
@@ -0,0 +1,98 @@
+/*
+ * 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 com.rometools.propono.atom.server;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+
+/**
+ * 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() {
+ final PrivilegedAction action = new PrivilegedAction() {
+ @Override
+ public ClassLoader run() {
+ ClassLoader cl = null;
+ try {
+ cl = Thread.currentThread().getContextClassLoader();
+ } catch (final SecurityException ex) {
+ }
+ return cl;
+ }
+ };
+ return AccessController.doPrivileged(action);
+ }
+
+ String getSystemProperty(final String propName) {
+ final PrivilegedAction action = new PrivilegedAction() {
+ @Override
+ public String run() {
+ return System.getProperty(propName);
+ }
+ };
+ return AccessController.doPrivileged(action);
+ }
+
+ FileInputStream getFileInputStream(final File file) throws FileNotFoundException {
+ try {
+ final PrivilegedExceptionAction action = new PrivilegedExceptionAction() {
+ @Override
+ public FileInputStream run() throws FileNotFoundException {
+ return new FileInputStream(file);
+ }
+ };
+ return AccessController.doPrivileged(action);
+ } catch (final PrivilegedActionException e) {
+ throw (FileNotFoundException) e.getException();
+ }
+ }
+
+ InputStream getResourceAsStream(final ClassLoader cl, final String name) {
+ final PrivilegedAction action = new PrivilegedAction() {
+ @Override
+ public InputStream run() {
+ InputStream ris;
+ if (cl == null) {
+ ris = ClassLoader.getSystemResourceAsStream(name);
+ } else {
+ ris = cl.getResourceAsStream(name);
+ }
+ return ris;
+ }
+ };
+ return AccessController.doPrivileged(action);
+ }
+
+ boolean doesFileExist(final File f) {
+ final PrivilegedAction action = new PrivilegedAction() {
+ @Override
+ public Boolean run() {
+ return f.exists();
+ }
+ };
+ return AccessController.doPrivileged(action).booleanValue();
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomHandler.java b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomHandler.java
new file mode 100644
index 0000000..dc754da
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomHandler.java
@@ -0,0 +1,469 @@
+/*
+ * 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 com.rometools.propono.atom.server.impl;
+
+import java.io.File;
+import java.util.StringTokenizer;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.rometools.propono.atom.common.AtomService;
+import com.rometools.propono.atom.common.Categories;
+import com.rometools.propono.atom.server.AtomException;
+import com.rometools.propono.atom.server.AtomHandler;
+import com.rometools.propono.atom.server.AtomMediaResource;
+import com.rometools.propono.atom.server.AtomRequest;
+import com.rometools.propono.atom.server.AtomServlet;
+import com.rometools.rome.feed.atom.Entry;
+import com.rometools.rome.feed.atom.Feed;
+
+/**
+ * File-based {@link com.rometools.rome.propono.atom.server.AtomHandler} implementation that stores
+ * entries and media-entries to disk. Implemented using
+ * {@link com.rometools.rome.propono.atom.server.impl.FileBasedAtomService}.
+ */
+public class FileBasedAtomHandler implements AtomHandler {
+
+ private static final Logger LOG = LoggerFactory.getLogger(FileBasedAtomHandler.class);
+
+ private String userName = null;
+ private String atomProtocolURL = null;
+ private String contextURI = null;
+ private FileBasedAtomService service = null;
+
+ /**
+ * Construct handler to handle one request.
+ *
+ * @param req Request to be handled.
+ */
+ public FileBasedAtomHandler(final 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(final HttpServletRequest req, final 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 (final 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(final String login, final String password) {
+ return true;
+ }
+
+ /**
+ * Get username of authenticated user
+ *
+ * @return User name.
+ */
+ @Override
+ 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.rometools.rome.propono.atom.server.AtomException Unexpected exception.
+ * @return AtomService object with workspaces and collections.
+ */
+ @Override
+ public AtomService getAtomService(final AtomRequest areq) throws AtomException {
+ return service;
+ }
+
+ /**
+ * Returns null because we use in-line categories.
+ *
+ * @throws com.rometools.rome.propono.atom.server.AtomException Unexpected exception.
+ * @return Categories object
+ */
+ @Override
+ public Categories getCategories(final AtomRequest areq) throws AtomException {
+ LOG.debug("getCollection");
+ final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/");
+ final String handle = pathInfo[0];
+ final String collection = pathInfo[1];
+ final FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ return col.getCategories(true).get(0);
+ }
+
+ /**
+ * Get collection specified by pathinfo.
+ *
+ * @param areq Details of HTTP request
+ * @return ROME feed representing collection.
+ * @throws com.rometools.rome.propono.atom.server.AtomException Invalid collection or other
+ * exception.
+ */
+ @Override
+ public Feed getCollection(final AtomRequest areq) throws AtomException {
+ LOG.debug("getCollection");
+ final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/");
+ final String handle = pathInfo[0];
+ final String collection = pathInfo[1];
+ final 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.rometools.rome.propono.atom.server.AtomException On invalid collection or other
+ * error.
+ * @return Entry as represented on server.
+ */
+ @Override
+ public Entry postEntry(final AtomRequest areq, final Entry entry) throws AtomException {
+ LOG.debug("postEntry");
+
+ final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/");
+ final String handle = pathInfo[0];
+ final String collection = pathInfo[1];
+ final FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ try {
+ return col.addEntry(entry);
+
+ } catch (final Exception fe) {
+ fe.printStackTrace();
+ throw new AtomException(fe);
+ }
+ }
+
+ /**
+ * Get entry specified by pathInfo.
+ *
+ * @param areq Details of HTTP request
+ * @throws com.rometools.rome.propono.atom.server.AtomException On invalid pathinfo or other
+ * error.
+ * @return ROME Entry object.
+ */
+ @Override
+ public Entry getEntry(final AtomRequest areq) throws AtomException {
+ LOG.debug("getEntry");
+ final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/");
+ final String handle = pathInfo[0];
+ final String collection = pathInfo[1];
+ final String fileName = pathInfo[2];
+ final FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ try {
+ return col.getEntry(fileName);
+
+ } catch (final 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.rometools.rome.propono.atom.server.AtomException
+ */
+ @Override
+ public void putEntry(final AtomRequest areq, final Entry entry) throws AtomException {
+ LOG.debug("putEntry");
+ final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/");
+ final String handle = pathInfo[0];
+ final String collection = pathInfo[1];
+ final String fileName = pathInfo[2];
+ final FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ try {
+ col.updateEntry(entry, fileName);
+
+ } catch (final Exception fe) {
+ throw new AtomException(fe);
+ }
+ }
+
+ /**
+ * Delete entry specified by pathInfo.
+ *
+ * @param areq Details of HTTP request
+ */
+ @Override
+ public void deleteEntry(final AtomRequest areq) throws AtomException {
+ LOG.debug("deleteEntry");
+ final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/");
+ final String handle = pathInfo[0];
+ final String collection = pathInfo[1];
+ final String fileName = pathInfo[2];
+ final FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ try {
+ col.deleteEntry(fileName);
+
+ } catch (final Exception e) {
+ final 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
+ */
+ @Override
+ public Entry postMedia(final AtomRequest areq, final Entry entry) throws AtomException {
+
+ // get incoming slug from HTTP header
+ final String slug = areq.getHeader("Slug");
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("postMedia - title: " + entry.getTitle() + " slug:" + slug);
+ }
+
+ try {
+ final File tempFile = null;
+ final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/");
+ final String handle = pathInfo[0];
+ final String collection = pathInfo[1];
+ final FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ try {
+ col.addMediaEntry(entry, slug, areq.getInputStream());
+
+ } catch (final Exception e) {
+ e.printStackTrace();
+ final String msg = "ERROR reading posted file";
+ LOG.error(msg, e);
+ throw new AtomException(msg, e);
+ } finally {
+ if (tempFile != null) {
+ tempFile.delete();
+ }
+ }
+
+ } catch (final 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
+ */
+ @Override
+ public void putMedia(final AtomRequest areq) throws AtomException {
+
+ LOG.debug("putMedia");
+ final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/");
+ final String handle = pathInfo[0];
+ final String collection = pathInfo[1];
+ final String fileName = pathInfo[3];
+ final FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ try {
+ col.updateMediaEntry(fileName, areq.getContentType(), areq.getInputStream());
+
+ } catch (final Exception re) {
+ throw new AtomException("ERROR: posting media");
+ }
+ }
+
+ @Override
+ public AtomMediaResource getMediaResource(final AtomRequest areq) throws AtomException {
+ LOG.debug("putMedia");
+ final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/");
+ final String handle = pathInfo[0];
+ final String collection = pathInfo[1];
+ final String fileName = pathInfo[3];
+ final FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ try {
+ return col.getMediaResource(fileName);
+
+ } catch (final Exception re) {
+ throw new AtomException("ERROR: posting media");
+ }
+ }
+
+ /**
+ * Return true if specified pathinfo represents URI of service doc.
+ */
+ @Override
+ public boolean isAtomServiceURI(final AtomRequest areq) {
+ final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/");
+ if (pathInfo.length == 0) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Return true if specified pathinfo represents URI of category doc.
+ */
+ @Override
+ public boolean isCategoriesURI(final AtomRequest areq) {
+ LOG.debug("isCategoriesDocumentURI");
+ final 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.
+ */
+ @Override
+ public boolean isCollectionURI(final AtomRequest areq) {
+ LOG.debug("isCollectionURI");
+ // workspace/collection-plural
+ // if length is 2 and points to a valid collection then YES
+ final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/");
+ if (pathInfo.length == 2) {
+ final String handle = pathInfo[0];
+ final 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.
+ */
+ @Override
+ public boolean isEntryURI(final AtomRequest areq) {
+ LOG.debug("isEntryURI");
+ // workspace/collection-singular/fsid
+ // if length is 3 and points to a valid collection then YES
+ final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/");
+ if (pathInfo.length == 3) {
+ final String handle = pathInfo[0];
+ final String collection = pathInfo[1];
+ if (service.findCollectionByHandle(handle, collection) != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return true if specified pathinfo represents media-edit URI.
+ */
+ @Override
+ public boolean isMediaEditURI(final 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
+ final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/");
+ if (pathInfo.length == 4) {
+ final String handle = pathInfo[0];
+ final String collection = pathInfo[1];
+ final String media = pathInfo[2];
+ // final String fsid = pathInfo[3];
+ if (service.findCollectionByHandle(handle, collection) != null && media.equals("media")) {
+ return true;
+ }
+ }
+ return false;
+
+ }
+
+ /**
+ * BASIC authentication.
+ */
+ public String authenticateBASIC(final HttpServletRequest request) {
+ LOG.debug("authenticateBASIC");
+ boolean valid = false;
+ String userID = null;
+ String password = null;
+ try {
+ final String authHeader = request.getHeader("Authorization");
+ if (authHeader != null) {
+ final StringTokenizer st = new StringTokenizer(authHeader);
+ if (st.hasMoreTokens()) {
+ final String basic = st.nextToken();
+ if (basic.equalsIgnoreCase("Basic")) {
+ final String credentials = st.nextToken();
+ final String userPass = new String(Base64.decodeBase64(credentials.getBytes()));
+ final 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 (final Exception e) {
+ LOG.debug("An error occured while processing Basic authentication", e);
+ }
+ if (valid) {
+ // For now assume userID as userName
+ return userID;
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomHandlerFactory.java b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomHandlerFactory.java
new file mode 100644
index 0000000..f4cb6bc
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomHandlerFactory.java
@@ -0,0 +1,38 @@
+/*
+ * 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 com.rometools.propono.atom.server.impl;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.rometools.propono.atom.server.AtomHandler;
+import com.rometools.propono.atom.server.AtomHandlerFactory;
+
+/**
+ * Extends {@link com.rometools.rome.propono.atom.server.AtomHandlerFactory} to create and return
+ * {@link com.rometools.rome.propono.atom.server.impl.FileBasedAtomHandler}.
+ */
+public class FileBasedAtomHandlerFactory extends AtomHandlerFactory {
+
+ /**
+ * Create new AtomHandler.
+ */
+ @Override
+ public AtomHandler newAtomHandler(final HttpServletRequest req, final HttpServletResponse res) {
+ return new FileBasedAtomHandler(req);
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomService.java b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomService.java
new file mode 100644
index 0000000..3df365c
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomService.java
@@ -0,0 +1,193 @@
+/*
+ * 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 com.rometools.propono.atom.server.impl;
+
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+
+import com.rometools.propono.atom.common.AtomService;
+import com.rometools.propono.utils.Utilities;
+
+/**
+ * File based Atom service. Supports one workspace per user. Collections in workspace are defined in
+ * /propono.properties, for example:
+ *
+ *
+ *
+ * If no such properties are found, then service will fall back to two collections: 'entries' for
+ * Atom entries and 'resources' for any content-type.
+ *
+ *
+ *
+ * URI structure used for accessing collections and entries
+ *
+ *
+ *
+ * Collection feed (URI allows GET to get collection, POST to add to it)
+ * [servlet-context-uri]/app/[workspace-handle]/[collection-plural]
+ *
+ *
+ *
+ * Collection entry (URI allows GET, PUT and DELETE)
+ * [servlet-context-uri]/app/[workspace-handle]/[collection-singular]/[entryid]
+ *
+ *
+ *
+ * Collection entry media (URI allows GET, PUT and DELETE)
+ * [servlet-context-uri]/app/[workspace-handle]/[collection-singular]/media/[entryid]
+ *
+ *
+ *
+ * Categories URI if not using inline categories (URI allows GET)
+ * [servlet-context-uri]/app/[workspace-handle]/[collection-plural]/categories
+ *
+ *
+ *
+ *
+ * Directory structure used to store collections and entries
+ *
+ *
+ *
+ * Collection feed (kept constantly up to date)
+ * [servlet-context-dir]/[workspace-handle]/[collection-plural]/feed.xml
+ *
+ *
+ *
+ * Collection entry (individual entries also stored as entry.xml files)
+ * [servlet-context-dir]/[workspace-handle]/[collection-plural]/id/entry.xml
+ *
+ *
+ *
+ * Collection entry media (media file stored under entry directory)
+ * [servlet-context-dir]/[workspace-handle]/[collection-plural]/id/media/id
+ *
+ */
+public class FileBasedAtomService extends AtomService {
+
+ private final Map workspaceMap = new TreeMap();
+ private final Map collectionMap = new TreeMap();
+ private static Properties cacheProps = new Properties();
+ private boolean firstTime = true;
+
+ /**
+ * Creates a new instance of FileBasedAtomService.
+ */
+ public FileBasedAtomService(final String userName, final String baseDir, final String contextURI, final String contextPath, final String servletPath)
+ throws Exception {
+ final String workspaceHandle = userName;
+
+ // One workspace per user
+ final FileBasedWorkspace workspace = new FileBasedWorkspace(workspaceHandle, baseDir);
+ workspaceMap.put(userName, workspace);
+
+ if (firstTime) {
+ synchronized (cacheProps) {
+ final 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();
+ }
+
+ final String relativeURIsString = cacheProps.getProperty("propono.atomserver.filebased.relativeURIs");
+ final boolean relativeURIs = "true".equals(relativeURIsString);
+
+ final String inlineCategoriesString = cacheProps.getProperty("propono.atomserver.filebased.inlineCategories");
+ final boolean inlineCategories = "true".equals(inlineCategoriesString);
+
+ final String colnames = cacheProps.getProperty("propono.atomserver.filebased.collections");
+ if (colnames != null) {
+
+ // collections specified in propono.properties, use those
+
+ final String[] colarray = Utilities.stringToStringArray(colnames, ",");
+ for (final String element : colarray) {
+ final String prefix = "propono.atomserver.filebased.collection." + element + ".";
+ final String collectionTitle = cacheProps.getProperty(prefix + "title");
+ final String collectionSingular = cacheProps.getProperty(prefix + "singular");
+ final String collectionPlural = cacheProps.getProperty(prefix + "plural");
+ final String collectionAccept = cacheProps.getProperty(prefix + "accept");
+
+ final String catNamesString = cacheProps.getProperty(prefix + "categories");
+ final String[] catNames = Utilities.stringToStringArray(catNamesString, ",");
+
+ final 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.)
+
+ final String[] catNames = new String[] { "general", "category1", "category2" };
+
+ final 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);
+
+ final 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(final String handle) {
+ return workspaceMap.get(handle);
+ }
+
+ FileBasedCollection findCollectionByHandle(final String handle, final String collection) {
+ return collectionMap.get(handle + "|" + collection);
+ }
+}
diff --git a/src/main/java/com/rometools/propono/atom/server/impl/FileBasedCollection.java b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedCollection.java
new file mode 100644
index 0000000..ab296c7
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedCollection.java
@@ -0,0 +1,815 @@
+/*
+ * 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 com.rometools.propono.atom.server.impl;
+
+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.Locale;
+import java.util.StringTokenizer;
+
+import javax.activation.FileTypeMap;
+import javax.activation.MimetypesFileTypeMap;
+
+import org.jdom2.Document;
+import org.jdom2.output.XMLOutputter;
+
+import com.rometools.propono.atom.common.Categories;
+import com.rometools.propono.atom.common.Collection;
+import com.rometools.propono.atom.common.rome.AppModule;
+import com.rometools.propono.atom.common.rome.AppModuleImpl;
+import com.rometools.propono.atom.server.AtomException;
+import com.rometools.propono.atom.server.AtomMediaResource;
+import com.rometools.propono.atom.server.AtomNotFoundException;
+import com.rometools.propono.utils.Utilities;
+import com.rometools.rome.feed.WireFeed;
+import com.rometools.rome.feed.atom.Category;
+import com.rometools.rome.feed.atom.Content;
+import com.rometools.rome.feed.atom.Entry;
+import com.rometools.rome.feed.atom.Feed;
+import com.rometools.rome.feed.atom.Link;
+import com.rometools.rome.feed.module.Module;
+import com.rometools.rome.io.FeedException;
+import com.rometools.rome.io.WireFeedInput;
+import com.rometools.rome.io.WireFeedOutput;
+import com.rometools.rome.io.impl.Atom10Generator;
+import com.rometools.rome.io.impl.Atom10Parser;
+
+/**
+ * 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 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 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(final String title, final String handle, final String collection, final String singular, final String accept,
+ final boolean inlineCats, final String[] catNames, final boolean relativeURIs, final String contextURI, final String contextPath,
+ final String servletPath, final 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.servletPath = servletPath;
+
+ addAccept(accept);
+ }
+
+ /**
+ * Get feed document representing collection.
+ *
+ * @throws com.rometools.rome.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 {
+ final WireFeedInput input = new WireFeedInput();
+ final WireFeed wireFeed = input.build(new InputStreamReader(in, "UTF-8"));
+ return (Feed) wireFeed;
+ } catch (final 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(final boolean inline) {
+ final Categories cats = new Categories();
+ cats.setFixed(true);
+ cats.setScheme(contextURI + "/" + handle + "/" + singular);
+ if (inline) {
+ for (final String catName : catNames) {
+ final Category cat = new Category();
+ cat.setTerm(catName);
+ 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.
+ */
+ @Override
+ 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(final Entry entry) throws Exception {
+ synchronized (FileStore.getFileStore()) {
+ final Feed f = getFeedDocument();
+
+ final String fsid = FileStore.getFileStore().getNextId();
+ updateTimestamps(entry);
+
+ // Save entry to file
+ final String entryPath = getEntryPath(fsid);
+
+ final 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 [collection-plural]/[entryid]/media/[entryid]. An Atom entry will be
+ * created to store metadata for the entry and it will exist at the path
+ * [collection-plural]/[entryid]/entry.xml. 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(final Entry entry, final String slug, final InputStream is) throws Exception {
+ synchronized (FileStore.getFileStore()) {
+
+ // Save media file temp file
+ final Content content = entry.getContents().get(0);
+ if (entry.getTitle() == null) {
+ entry.setTitle(slug);
+ }
+ final String fileName = createFileName(slug != null ? slug : entry.getTitle(), content.getType());
+ final File tempFile = File.createTempFile(fileName, "tmp");
+ final FileOutputStream fos = new FileOutputStream(tempFile);
+ Utilities.copyInputToOutput(is, fos);
+ fos.close();
+
+ // Save media file
+ final FileInputStream fis = new FileInputStream(tempFile);
+ saveMediaFile(fileName, content.getType(), tempFile.length(), fis);
+ fis.close();
+ final File resourceFile = new File(getEntryMediaPath(fileName));
+
+ // Create media-link entry
+ updateTimestamps(entry);
+
+ // Save media-link entry
+ final String entryPath = getEntryPath(fileName);
+ final 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
+ final 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());
+ }
+
+ final String entryPath = getEntryPath(fsid);
+
+ checkExistence(entryPath);
+ final InputStream in = FileStore.getFileStore().getFileInputStream(entryPath);
+
+ final Entry entry;
+ final 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(final String fileName) throws Exception {
+ final String filePath = getEntryMediaPath(fileName);
+ final 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(final Entry entry, String fsid) throws Exception {
+ synchronized (FileStore.getFileStore()) {
+
+ final 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);
+
+ final String entryPath = getEntryPath(fsid);
+ final 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(final String fileName, final String contentType, final InputStream is) throws Exception {
+ synchronized (FileStore.getFileStore()) {
+
+ final File tempFile = File.createTempFile(fileName, "tmp");
+ final FileOutputStream fos = new FileOutputStream(tempFile);
+ Utilities.copyInputToOutput(is, fos);
+ fos.close();
+
+ // Update media file
+ final FileInputStream fis = new FileInputStream(tempFile);
+ saveMediaFile(fileName, contentType, tempFile.length(), fis);
+ fis.close();
+ final File resourceFile = new File(getEntryMediaPath(fileName));
+
+ // Load media-link entry to return
+ final String entryPath = getEntryPath(fileName);
+ final InputStream in = FileStore.getFileStore().getFileInputStream(entryPath);
+ final Entry atomEntry = loadAtomResourceEntry(in, resourceFile);
+
+ updateTimestamps(atomEntry);
+ updateMediaEntryAppLinks(atomEntry, fileName, false);
+
+ // Update feed with new entry
+ final Feed f = getFeedDocument();
+ updateFeedDocumentWithExistingEntry(f, atomEntry);
+
+ // Save updated media-link entry
+ final 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(final String fsid) throws Exception {
+ synchronized (FileStore.getFileStore()) {
+
+ // Remove entry from Feed
+ final Feed feed = getFeedDocument();
+ updateFeedDocumentRemovingEntry(feed, fsid);
+
+ final String entryFilePath = getEntryPath(fsid);
+ FileStore.getFileStore().deleteFile(entryFilePath);
+
+ final String entryMediaPath = getEntryMediaPath(fsid);
+ if (entryMediaPath != null) {
+ FileStore.getFileStore().deleteFile(entryMediaPath);
+ }
+
+ final String entryDirPath = getEntryDirPath(fsid);
+ FileStore.getFileStore().deleteDirectory(entryDirPath);
+
+ try {
+ Thread.sleep(500L);
+ } catch (final Exception ignored) {
+ }
+ }
+ }
+
+ private void updateFeedDocumentWithNewEntry(final Feed f, final Entry e) throws AtomException {
+ boolean inserted = false;
+ for (int i = 0; i < f.getEntries().size(); i++) {
+ final Entry entry = f.getEntries().get(i);
+ final AppModule mod = (AppModule) entry.getModule(AppModule.URI);
+ final 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(final Feed f, final String id) throws AtomException {
+ final Entry e = findEntry("urn:uuid:" + id, f);
+ f.getEntries().remove(e);
+ updateFeedDocument(f);
+ }
+
+ private void updateFeedDocumentWithExistingEntry(final Feed f, final Entry e) throws AtomException {
+ final Entry old = findEntry(e.getId(), f);
+ f.getEntries().remove(old);
+
+ boolean inserted = false;
+ for (int i = 0; i < f.getEntries().size(); i++) {
+ final Entry entry = f.getEntries().get(i);
+ final AppModule entryAppModule = (AppModule) entry.getModule(AppModule.URI);
+ final 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(final String id, final Feed feed) {
+ for (final Entry entry : feed.getEntries()) {
+ if (id.equals(entry.getId())) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ private void updateFeedDocument(final Feed f) throws AtomException {
+ try {
+ synchronized (FileStore.getFileStore()) {
+ final WireFeedOutput wireFeedOutput = new WireFeedOutput();
+ final Document feedDoc = wireFeedOutput.outputJDom(f);
+ final XMLOutputter outputter = new XMLOutputter();
+ // outputter.setFormat(Format.getPrettyFormat());
+ final OutputStream fos = FileStore.getFileStore().getFileOutputStream(getFeedPath());
+ outputter.output(feedDoc, new OutputStreamWriter(fos, "UTF-8"));
+ }
+ } catch (final FeedException fex) {
+ throw new AtomException(fex);
+ } catch (final IOException ex) {
+ throw new AtomException(ex);
+ }
+ }
+
+ private InputStream createDefaultFeedDocument(final String uri) throws AtomException {
+
+ final Feed f = new Feed();
+ f.setTitle("Feed");
+ f.setId(uri);
+ f.setFeedType(FEED_TYPE);
+
+ final Link selfLink = new Link();
+ selfLink.setRel("self");
+ selfLink.setHref(uri);
+ f.getOtherLinks().add(selfLink);
+
+ try {
+ final WireFeedOutput wireFeedOutput = new WireFeedOutput();
+ final Document feedDoc = wireFeedOutput.outputJDom(f);
+ final XMLOutputter outputter = new XMLOutputter();
+ // outputter.setFormat(Format.getCompactFormat());
+ final OutputStream fos = FileStore.getFileStore().getFileOutputStream(getFeedPath());
+ outputter.output(feedDoc, new OutputStreamWriter(fos, "UTF-8"));
+
+ } catch (final FeedException ex) {
+ throw new AtomException(ex);
+ } catch (final IOException ex) {
+ throw new AtomException(ex);
+ } catch (final Exception e) {
+
+ e.printStackTrace();
+ }
+ return FileStore.getFileStore().getFileInputStream(getFeedPath());
+ }
+
+ private Entry loadAtomResourceEntry(final InputStream in, final File file) {
+ try {
+ final Entry entry = Atom10Parser.parseEntry(new BufferedReader(new InputStreamReader(in)), null, Locale.US);
+ updateMediaEntryAppLinks(entry, file.getName(), true);
+ return entry;
+
+ } catch (final Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private void updateEntryAppLinks(final Entry entry, final String fsid, final 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 (final Link link : altLinks) {
+ 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 (final Link link : otherLinks) {
+ 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(final Entry entry, final String fileName, final boolean singleEntry) {
+
+ // TODO: figure out why PNG is missing from Java MIME types
+ final FileTypeMap map = FileTypeMap.getDefaultFileTypeMap();
+ if (map instanceof MimetypesFileTypeMap) {
+ try {
+ ((MimetypesFileTypeMap) map).addMimeTypes("image/png png PNG");
+ } catch (final Exception ignored) {
+ }
+ }
+ entry.setId(getEntryMediaViewURI(fileName));
+ entry.setTitle(fileName);
+ entry.setUpdated(new Date());
+
+ final List otherlinks = new ArrayList();
+ entry.setOtherLinks(otherlinks);
+
+ final Link editlink = new Link();
+ editlink.setRel("edit");
+ editlink.setHref(getEntryEditURI(fileName, relativeURIs, singleEntry));
+ otherlinks.add(editlink);
+
+ final Link editMedialink = new Link();
+ editMedialink.setRel("edit-media");
+ editMedialink.setHref(getEntryMediaEditURI(fileName, relativeURIs, singleEntry));
+ otherlinks.add(editMedialink);
+
+ final Content content = entry.getContents().get(0);
+ content.setSrc(getEntryMediaViewURI(fileName));
+ final 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(final InputStream in) {
+ try {
+ return Atom10Parser.parseEntry(new BufferedReader(new InputStreamReader(in, "UTF-8")), null, Locale.US);
+ } catch (final Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ /**
+ * Update existing or add new app:edited.
+ */
+ private void updateTimestamps(final 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();
+ final 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(final String name, final String contentType, final long size, final InputStream is) throws AtomException {
+
+ final byte[] buffer = new byte[8192];
+ int bytesRead = 0;
+
+ final 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 (final Exception e) {
+ throw new AtomException("ERROR uploading file", e);
+ } finally {
+ try {
+ bos.flush();
+ bos.close();
+ } catch (final 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(final String title, final 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;
+
+ final 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.
+ final String[] typeTokens = contentType.split("/");
+ final String ext = typeTokens[1];
+
+ if (title != null && !title.trim().equals("")) {
+ // We've got a title, so use it to build file name
+ final String base = Utilities.replaceNonAlphanumeric(title, ' ');
+ final 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(final String fsid, final boolean relative, final 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(final String fsid) {
+ return contextURI + "/" + handle + "/" + collection + "/" + fsid + "/entry.xml";
+ }
+
+ private String getEntryMediaEditURI(final String fsid, final boolean relative, final 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(final 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(final String id) {
+ return getBaseDir() + handle + File.separator + collection + File.separator + id;
+ }
+
+ private String getEntryPath(final String id) {
+ return getEntryDirPath(id) + File.separator + "entry.xml";
+ }
+
+ private String getEntryMediaPath(final String id) {
+ return getEntryDirPath(id) + File.separator + "media" + File.separator + id;
+ }
+
+ private static void checkExistence(final String path) throws AtomNotFoundException {
+ if (!FileStore.getFileStore().exists(path)) {
+ throw new AtomNotFoundException("Entry does not exist");
+ }
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/server/impl/FileBasedWorkspace.java b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedWorkspace.java
new file mode 100644
index 0000000..163c6cc
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedWorkspace.java
@@ -0,0 +1,30 @@
+/*
+ * 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 com.rometools.propono.atom.server.impl;
+
+import com.rometools.propono.atom.common.Workspace;
+
+/**
+ * File based Atom service Workspace.
+ */
+public class FileBasedWorkspace extends Workspace {
+
+ /** Creates a new instance of FileBasedWorkspace */
+ public FileBasedWorkspace(final String handle, final String baseDir) {
+ super(handle, "text");
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/atom/server/impl/FileStore.java b/src/main/java/com/rometools/propono/atom/server/impl/FileStore.java
new file mode 100644
index 0000000..035c341
--- /dev/null
+++ b/src/main/java/com/rometools/propono/atom/server/impl/FileStore.java
@@ -0,0 +1,117 @@
+/*
+ * 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 com.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.lang3.RandomStringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class which helps in handling File persistence related operations.
+ */
+class FileStore {
+
+ private static final Logger LOG = LoggerFactory.getLogger(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(final String path) {
+ final File f = new File(path);
+ return f.exists();
+ }
+
+ public InputStream getFileInputStream(final String path) {
+ LOG.debug("getFileContents path: " + path);
+ try {
+ return new BufferedInputStream(new FileInputStream(path));
+ } catch (final FileNotFoundException e) {
+ LOG.debug(" File not found: " + path);
+ return null;
+ }
+ }
+
+ public OutputStream getFileOutputStream(final String path) {
+ LOG.debug("getFileOutputStream path: " + path);
+ try {
+ final File f = new File(path);
+ f.getParentFile().mkdirs();
+ return new BufferedOutputStream(new FileOutputStream(f));
+ } catch (final FileNotFoundException e) {
+ LOG.debug(" File not found: " + path);
+ return null;
+ }
+ }
+
+ public void createOrUpdateFile(final String path, final InputStream content) throws FileNotFoundException, IOException {
+ LOG.debug("createOrUpdateFile path: " + path);
+ final File f = new File(path);
+ f.mkdirs();
+ final FileOutputStream out = new FileOutputStream(f);
+
+ final byte[] buffer = new byte[2048];
+ int read;
+ while ((read = content.read(buffer)) != -1) {
+ out.write(buffer, 0, read);
+ }
+ out.close();
+ }
+
+ public void deleteFile(final String path) {
+ LOG.debug("deleteFile path: " + path);
+ final File f = new File(path);
+ if (!f.delete()) {
+ LOG.debug(" Failed to delete: " + f.getAbsolutePath());
+ }
+ }
+
+ public static FileStore getFileStore() {
+ return fileStore;
+ }
+
+ public boolean deleteDirectory(final String path) {
+ return deleteDirectory(new File(path));
+ }
+
+ public boolean deleteDirectory(final File path) {
+ LOG.debug("deleteDirectory path: " + path);
+ if (path.exists()) {
+ final File[] files = path.listFiles();
+ for (final File file : files) {
+ if (file.isDirectory()) {
+ deleteDirectory(file);
+ } else {
+ file.delete();
+ }
+ }
+ }
+ return path.delete();
+ }
+}
diff --git a/src/main/java/com/rometools/propono/blogclient/BaseBlogEntry.java b/src/main/java/com/rometools/propono/blogclient/BaseBlogEntry.java
new file mode 100644
index 0000000..7d5aca9
--- /dev/null
+++ b/src/main/java/com/rometools/propono/blogclient/BaseBlogEntry.java
@@ -0,0 +1,212 @@
+/*
+ * 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 com.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(final Blog blog) {
+ this.blog = blog;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPermalink() {
+ return permalink;
+ }
+
+ void setPermalink(final String permalink) {
+ this.permalink = permalink;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Person getAuthor() {
+ return author;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setAuthor(final Person author) {
+ this.author = author;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Content getContent() {
+ return content;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setContent(final Content content) {
+ this.content = content;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean getDraft() {
+ return draft;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setDraft(final boolean draft) {
+ this.draft = draft;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Date getPublicationDate() {
+ return publicationDate;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setPublicationDate(final Date pubDate) {
+ publicationDate = pubDate;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Date getModificationDate() {
+ return modificationDate;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setModificationDate(final Date date) {
+ modificationDate = date;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setTitle(final String title) {
+ this.title = title;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getSummary() {
+ return summary;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setSummary(final String summary) {
+ this.summary = summary;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List getCategories() {
+ return categories;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setCategories(final List categories) {
+ this.categories = categories;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Blog getBlog() {
+ return blog;
+ }
+
+ void setBlog(final Blog blog) {
+ this.blog = blog;
+ }
+
+ /**
+ * String representation, returns id.
+ */
+ @Override
+ public String toString() {
+ return id;
+ }
+}
diff --git a/src/main/java/com/rometools/propono/blogclient/Blog.java b/src/main/java/com/rometools/propono/blogclient/Blog.java
new file mode 100644
index 0000000..eb5d505
--- /dev/null
+++ b/src/main/java/com/rometools/propono/blogclient/Blog.java
@@ -0,0 +1,235 @@
+/*
+ * 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 com.rometools.propono.blogclient;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ *
+ * 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.
+ *
+ */
+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.rometools.rome.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.
+ */
+ @Deprecated
+ 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.
+ */
+ @Deprecated
+ 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.
+ */
+ @Deprecated
+ 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.
+ */
+ @Deprecated
+ 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.
+ */
+ @Deprecated
+ public List getCategories() throws BlogClientException;
+}
diff --git a/src/main/java/com/rometools/propono/blogclient/BlogClientException.java b/src/main/java/com/rometools/propono/blogclient/BlogClientException.java
new file mode 100644
index 0000000..ac44e48
--- /dev/null
+++ b/src/main/java/com/rometools/propono/blogclient/BlogClientException.java
@@ -0,0 +1,44 @@
+/*
+ * 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 com.rometools.propono.blogclient;
+
+/**
+ * Represents a Blog Client exception, the library throws these instead of implementation specific
+ * exceptions.
+ */
+public class BlogClientException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Construct a new exception
+ *
+ * @param msg Text message that explains exception
+ */
+ public BlogClientException(final 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(final String msg, final Throwable t) {
+ super(msg, t);
+ }
+}
diff --git a/src/main/java/com/rometools/propono/blogclient/BlogConnection.java b/src/main/java/com/rometools/propono/blogclient/BlogConnection.java
new file mode 100644
index 0000000..4eb2fcc
--- /dev/null
+++ b/src/main/java/com/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 com.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/com/rometools/propono/blogclient/BlogConnectionFactory.java b/src/main/java/com/rometools/propono/blogclient/BlogConnectionFactory.java
new file mode 100644
index 0000000..0494a37
--- /dev/null
+++ b/src/main/java/com/rometools/propono/blogclient/BlogConnectionFactory.java
@@ -0,0 +1,74 @@
+/*
+ * 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 com.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.rometools.propono.blogclient.atomprotocol.AtomConnection";
+
+ private static String METAWEBLOG_IMPL_CLASS = "com.rometools.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(final String type, final String url, final String username, final 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(final String className, final String url, final String username, final String password)
+ throws BlogClientException {
+
+ Class> conClass;
+ try {
+ conClass = Class.forName(className);
+ } catch (final ClassNotFoundException ex) {
+ throw new BlogClientException("BlogConnection impl. class not found: " + className, ex);
+ }
+ final 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 (final Throwable t) {
+ throw new BlogClientException("ERROR instantiating BlogConnection impl.", t);
+ }
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/blogclient/BlogEntry.java b/src/main/java/com/rometools/propono/blogclient/BlogEntry.java
new file mode 100644
index 0000000..854e5a6
--- /dev/null
+++ b/src/main/java/com/rometools/propono/blogclient/BlogEntry.java
@@ -0,0 +1,261 @@
+/*
+ * 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 com.rometools.propono.blogclient;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 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(final 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(final 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(final 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(final 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(final String email) {
+ this.email = email;
+ }
+
+ /** Get person's name */
+ public String getName() {
+ return name;
+ }
+
+ /** Set person's name */
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ /** Get person's URL */
+ public String getUrl() {
+ return url;
+ }
+
+ /** Set person's URL */
+ public void setUrl(final String url) {
+ this.url = url;
+ }
+
+ /** Returns person's name */
+ @Override
+ 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(final String id) {
+ this.id = id;
+ name = id;
+ }
+
+ /**
+ * Determines if categories are equal based on id.
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ final 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(final String id) {
+ this.id = id;
+ }
+
+ /** Get category display name */
+ public String getName() {
+ return name;
+ }
+
+ /** Set category display name */
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ /** Get URL of category domain */
+ public String getUrl() {
+ return url;
+ }
+
+ /** Set URL of category domain */
+ public void setUrl(final String url) {
+ this.url = url;
+ }
+
+ /** Return category's name or id for display */
+ @Override
+ public String toString() {
+ return name != null ? name : id;
+ }
+ }
+}
diff --git a/src/main/java/com/rometools/propono/blogclient/BlogResource.java b/src/main/java/com/rometools/propono/blogclient/BlogResource.java
new file mode 100644
index 0000000..e9ffa55
--- /dev/null
+++ b/src/main/java/com/rometools/propono/blogclient/BlogResource.java
@@ -0,0 +1,37 @@
+/*
+ * 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 com.rometools.propono.blogclient;
+
+import java.io.InputStream;
+
+/**
+ * Represents a file that has been uploaded to a blog.
+ *
+ * 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/com/rometools/propono/blogclient/atomprotocol/AtomBlog.java b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomBlog.java
new file mode 100644
index 0000000..03c3949
--- /dev/null
+++ b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomBlog.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 com.rometools.propono.blogclient.atomprotocol;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import com.rometools.propono.atom.client.ClientAtomService;
+import com.rometools.propono.atom.client.ClientCollection;
+import com.rometools.propono.atom.client.ClientEntry;
+import com.rometools.propono.atom.client.ClientMediaEntry;
+import com.rometools.propono.atom.client.ClientWorkspace;
+import com.rometools.propono.blogclient.Blog;
+import com.rometools.propono.blogclient.BlogClientException;
+import com.rometools.propono.blogclient.BlogEntry;
+import com.rometools.propono.blogclient.BlogResource;
+import com.rometools.propono.utils.ProponoException;
+
+/**
+ * Atom protocol implementation of the BlogClient Blog interface.
+ */
+public class AtomBlog implements Blog {
+
+ private String name = null;
+ private ClientAtomService service;
+ private ClientWorkspace workspace = null;
+ private AtomCollection entriesCollection = null;
+ private AtomCollection resourcesCollection = null;
+ private final 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(final ClientAtomService service, final ClientWorkspace workspace) {
+
+ setService(service);
+ setWorkspace(workspace);
+ name = workspace.getTitle();
+ final List collect = workspace.getCollections();
+ final Iterator members = collect.iterator();
+
+ while (members.hasNext()) {
+ final 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}
+ */
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * String display of blog, returns name.
+ */
+ @Override
+ public String toString() {
+ return getName();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getToken() {
+ return entriesCollection.getToken();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BlogEntry newEntry() throws BlogClientException {
+ if (entriesCollection == null) {
+ throw new BlogClientException("No entry collection");
+ }
+ return entriesCollection.newEntry();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BlogEntry getEntry(final String token) throws BlogClientException {
+ ClientEntry clientEntry = null;
+ try {
+ clientEntry = getService().getEntry(token);
+ } catch (final 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}
+ */
+ @Override
+ public Iterator getEntries() throws BlogClientException {
+ if (entriesCollection == null) {
+ throw new BlogClientException("No primary entry collection");
+ }
+ return new AtomEntryIterator(entriesCollection);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Iterator getResources() throws BlogClientException {
+ if (resourcesCollection == null) {
+ throw new BlogClientException("No primary entry collection");
+ }
+ return new AtomEntryIterator(resourcesCollection);
+ }
+
+ String saveEntry(final BlogEntry entry) throws BlogClientException {
+ if (entriesCollection == null) {
+ throw new BlogClientException("No primary entry collection");
+ }
+ return entriesCollection.saveEntry(entry);
+ }
+
+ void deleteEntry(final BlogEntry entry) throws BlogClientException {
+ if (entriesCollection == null) {
+ throw new BlogClientException("No primary entry collection");
+ }
+ entriesCollection.deleteEntry(entry);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List getCategories() throws BlogClientException {
+ if (entriesCollection == null) {
+ throw new BlogClientException("No primary entry collection");
+ }
+ return entriesCollection.getCategories();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BlogResource newResource(final String name, final String contentType, final byte[] bytes) throws BlogClientException {
+ if (resourcesCollection == null) {
+ throw new BlogClientException("No resource collection");
+ }
+ return resourcesCollection.newResource(name, contentType, bytes);
+ }
+
+ String saveResource(final BlogResource res) throws BlogClientException {
+ if (resourcesCollection == null) {
+ throw new BlogClientException("No primary resource collection");
+ }
+ return resourcesCollection.saveResource(res);
+ }
+
+ void deleteResource(final BlogResource resource) throws BlogClientException {
+ deleteEntry(resource);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List getCollections() throws BlogClientException {
+ return new ArrayList(collections.values());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Blog.Collection getCollection(final String token) throws BlogClientException {
+ return collections.get(token);
+ }
+
+ ClientAtomService getService() {
+ return service;
+ }
+
+ void setService(final ClientAtomService service) {
+ this.service = service;
+ }
+
+ ClientWorkspace getWorkspace() {
+ return workspace;
+ }
+
+ void setWorkspace(final ClientWorkspace workspace) {
+ this.workspace = workspace;
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomCollection.java b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomCollection.java
new file mode 100644
index 0000000..d944b89
--- /dev/null
+++ b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomCollection.java
@@ -0,0 +1,173 @@
+/*
+ * 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 com.rometools.propono.blogclient.atomprotocol;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import com.rometools.propono.atom.client.ClientAtomService;
+import com.rometools.propono.atom.client.ClientCollection;
+import com.rometools.propono.atom.client.ClientEntry;
+import com.rometools.propono.atom.common.Categories;
+import com.rometools.propono.blogclient.Blog;
+import com.rometools.propono.blogclient.BlogClientException;
+import com.rometools.propono.blogclient.BlogEntry;
+import com.rometools.propono.blogclient.BlogResource;
+import com.rometools.rome.feed.atom.Category;
+
+/**
+ * Atom protocol implementation of BlogClient Blog.Collection.
+ */
+public class AtomCollection implements Blog.Collection {
+
+ private Blog blog = null;
+ private List categories = new ArrayList();
+
+ private ClientCollection clientCollection = null;
+
+ AtomCollection(final AtomBlog blog, final ClientCollection col) {
+ this.blog = blog;
+ clientCollection = col;
+ for (final Object element : col.getCategories()) {
+ final Categories cats = (Categories) element;
+ for (final Object element2 : cats.getCategories()) {
+ final Category cat = (Category) element2;
+ final BlogEntry.Category blogCat = new BlogEntry.Category(cat.getTerm());
+ blogCat.setName(cat.getLabel());
+ blogCat.setUrl(cat.getScheme());
+ getCategories().add(blogCat);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getTitle() {
+ return getClientCollection().getTitle();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getToken() {
+ return getClientCollection().getHrefResolved();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List getAccepts() {
+ return getClientCollection().getAccepts();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean accepts(final String ct) {
+ return getClientCollection().accepts(ct);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Iterator getEntries() throws BlogClientException {
+ return new AtomEntryIterator(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BlogEntry newEntry() throws BlogClientException {
+ final AtomBlog atomBlog = (AtomBlog) getBlog();
+ final BlogEntry entry = new AtomEntry(atomBlog, this);
+ return entry;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BlogResource newResource(final String name, final String contentType, final byte[] bytes) throws BlogClientException {
+ return new AtomResource(this, name, contentType, bytes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String saveResource(final BlogResource res) throws BlogClientException {
+ ((AtomResource) res).setCollection(this);
+ res.save();
+ return res.getContent().getSrc();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String saveEntry(final BlogEntry entry) throws BlogClientException {
+ ((AtomEntry) entry).setCollection(this);
+ entry.save();
+ return entry.getPermalink();
+ }
+
+ void deleteEntry(final BlogEntry entry) throws BlogClientException {
+ try {
+ final ClientAtomService service = ((AtomBlog) getBlog()).getService();
+ final ClientEntry clientEntry = service.getEntry(entry.getToken());
+ clientEntry.remove();
+
+ } catch (final Exception e) {
+ throw new BlogClientException("ERROR deleting entry", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Blog getBlog() {
+ return blog;
+ }
+
+ void setBlog(final AtomBlog blog) {
+ this.blog = blog;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List getCategories() {
+ return categories;
+ }
+
+ void setCategories(final List categories) {
+ this.categories = categories;
+ }
+
+ ClientCollection getClientCollection() {
+ return clientCollection;
+ }
+}
diff --git a/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomConnection.java b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomConnection.java
new file mode 100644
index 0000000..db2cc78
--- /dev/null
+++ b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomConnection.java
@@ -0,0 +1,85 @@
+/*
+ * 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 com.rometools.propono.blogclient.atomprotocol;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import com.rometools.propono.atom.client.AtomClientFactory;
+import com.rometools.propono.atom.client.BasicAuthStrategy;
+import com.rometools.propono.atom.client.ClientAtomService;
+import com.rometools.propono.atom.client.ClientWorkspace;
+import com.rometools.propono.atom.common.Workspace;
+import com.rometools.propono.blogclient.Blog;
+import com.rometools.propono.blogclient.BlogClientException;
+import com.rometools.propono.blogclient.BlogConnection;
+
+/**
+ * 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 final 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(final String uri, final String username, final String password) throws BlogClientException {
+
+ try {
+ final ClientAtomService service = AtomClientFactory.getAtomService(uri, new BasicAuthStrategy(username, password));
+ final Iterator iter = service.getWorkspaces().iterator();
+ while (iter.hasNext()) {
+ final ClientWorkspace workspace = (ClientWorkspace) iter.next();
+ final Blog blog = new AtomBlog(service, workspace);
+ blogs.put(blog.getToken(), blog);
+ }
+ } catch (final Throwable t) {
+ throw new BlogClientException("Error connecting to blog server", t);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List getBlogs() {
+ return new ArrayList(blogs.values());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Blog getBlog(final String token) {
+ return blogs.get(token);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setAppkey(final String appkey) {
+ }
+}
diff --git a/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomEntry.java b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomEntry.java
new file mode 100644
index 0000000..945473b
--- /dev/null
+++ b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomEntry.java
@@ -0,0 +1,226 @@
+/*
+ * 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 com.rometools.propono.blogclient.atomprotocol;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import com.rometools.propono.atom.client.ClientEntry;
+import com.rometools.propono.atom.common.rome.AppModule;
+import com.rometools.propono.atom.common.rome.AppModuleImpl;
+import com.rometools.propono.blogclient.BaseBlogEntry;
+import com.rometools.propono.blogclient.BlogClientException;
+import com.rometools.propono.blogclient.BlogEntry;
+import com.rometools.propono.utils.ProponoException;
+import com.rometools.rome.feed.atom.Entry;
+import com.rometools.rome.feed.atom.Link;
+import com.rometools.rome.feed.module.Module;
+import com.rometools.rome.feed.synd.SyndPerson;
+
+/**
+ * Atom protocol implementation of BlogEntry.
+ */
+public class AtomEntry extends BaseBlogEntry implements BlogEntry {
+
+ String editURI = null;
+ AtomCollection collection = null;
+
+ AtomEntry(final AtomBlog blog, final AtomCollection collection) throws BlogClientException {
+ super(blog);
+ this.collection = collection;
+ }
+
+ AtomEntry(final AtomCollection collection, final ClientEntry entry) throws BlogClientException {
+ this((AtomBlog) collection.getBlog(), collection);
+ // clientEntry = entry;
+ copyFromRomeEntry(entry);
+ }
+
+ AtomEntry(final AtomBlog blog, final ClientEntry entry) throws BlogClientException {
+ super(blog);
+ // clientEntry = entry;
+ copyFromRomeEntry(entry);
+ }
+
+ @Override
+ public String getToken() {
+ return editURI;
+ }
+
+ AtomCollection getCollection() {
+ return collection;
+ }
+
+ void setCollection(final AtomCollection collection) {
+ this.collection = collection;
+ }
+
+ /**
+ * True if entry's token's are equal.
+ */
+ @Override
+ public boolean equals(final Object o) {
+ if (o instanceof AtomEntry) {
+ final AtomEntry other = (AtomEntry) o;
+ if (other.getToken() != null && getToken() != null) {
+ return other.getToken().equals(getToken());
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void save() throws BlogClientException {
+ final boolean create = getToken() == null;
+ if (create && getCollection() == null) {
+ throw new BlogClientException("Cannot save entry, no collection");
+ } else if (create) {
+ try {
+ final ClientEntry clientEntry = collection.getClientCollection().createEntry();
+ copyToRomeEntry(clientEntry);
+ collection.getClientCollection().addEntry(clientEntry);
+ copyFromRomeEntry(clientEntry);
+ } catch (final ProponoException ex) {
+ throw new BlogClientException("Error saving entry", ex);
+ }
+ } else {
+ try {
+ final ClientEntry clientEntry = ((AtomBlog) getBlog()).getService().getEntry(getToken());
+ copyToRomeEntry(clientEntry);
+ clientEntry.update();
+ copyFromRomeEntry(clientEntry);
+ } catch (final ProponoException ex) {
+ throw new BlogClientException("Error updating entry", ex);
+ }
+ }
+ }
+
+ @Override
+ public void delete() throws BlogClientException {
+ if (getToken() == null) {
+ throw new BlogClientException("Cannot delete unsaved entry");
+ }
+ try {
+ final ClientEntry clientEntry = ((AtomBlog) getBlog()).getService().getEntry(editURI);
+ clientEntry.remove();
+ } catch (final ProponoException ex) {
+ throw new BlogClientException("Error removing entry", ex);
+ }
+ }
+
+ void copyFromRomeEntry(final ClientEntry entry) {
+ id = entry.getId();
+ title = entry.getTitle();
+ editURI = entry.getEditURI();
+ final List altlinks = entry.getAlternateLinks();
+ if (altlinks != null) {
+ for (final Link link : altlinks) {
+ if ("alternate".equals(link.getRel()) || link.getRel() == null) {
+ permalink = link.getHrefResolved();
+ break;
+ }
+ }
+ }
+ final List contents = entry.getContents();
+ com.rometools.rome.feed.atom.Content romeContent = null;
+ if (contents != null && !contents.isEmpty()) {
+ romeContent = contents.get(0);
+ }
+ if (romeContent != null) {
+ content = new BlogEntry.Content(romeContent.getValue());
+ content.setType(romeContent.getType());
+ content.setSrc(romeContent.getSrc());
+ }
+ if (entry.getCategories() != null) {
+ final List cats = new ArrayList();
+ final List romeCats = entry.getCategories();
+ for (final com.rometools.rome.feed.atom.Category romeCat : romeCats) {
+ final BlogEntry.Category cat = new BlogEntry.Category();
+ cat.setId(romeCat.getTerm());
+ cat.setUrl(romeCat.getScheme());
+ cat.setName(romeCat.getLabel());
+ cats.add(cat);
+ }
+ categories = cats;
+ }
+ final List authors = entry.getAuthors();
+ if (authors != null && !authors.isEmpty()) {
+ final com.rometools.rome.feed.atom.Person romeAuthor = (com.rometools.rome.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();
+
+ final AppModule control = (AppModule) entry.getModule(AppModule.URI);
+ if (control != null && control.getDraft() != null) {
+ draft = control.getDraft().booleanValue();
+ } else {
+ draft = false;
+ }
+ }
+
+ Entry copyToRomeEntry(final ClientEntry entry) {
+ if (id != null) {
+ entry.setId(id);
+ }
+ entry.setTitle(title);
+ if (author != null) {
+ final com.rometools.rome.feed.atom.Person person = new com.rometools.rome.feed.atom.Person();
+ person.setName(author.getName());
+ person.setEmail(author.getEmail());
+ person.setUrl(author.getUrl());
+ final List authors = new ArrayList();
+ authors.add(person);
+ entry.setAuthors(authors);
+ }
+ if (content != null) {
+ final com.rometools.rome.feed.atom.Content romeContent = new com.rometools.rome.feed.atom.Content();
+ romeContent.setValue(content.getValue());
+ romeContent.setType(content.getType());
+ final List contents = new ArrayList();
+ contents.add(romeContent);
+ entry.setContents(contents);
+ }
+ if (categories != null) {
+ final List romeCats = new ArrayList();
+ for (final Category cat : categories) {
+ final com.rometools.rome.feed.atom.Category romeCategory = new com.rometools.rome.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);
+
+ final List modules = new ArrayList();
+ final AppModule control = new AppModuleImpl();
+ control.setDraft(new Boolean(draft));
+ modules.add(control);
+ entry.setModules(modules);
+
+ return entry;
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomEntryIterator.java b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomEntryIterator.java
new file mode 100644
index 0000000..74e0956
--- /dev/null
+++ b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomEntryIterator.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 com.rometools.propono.blogclient.atomprotocol;
+
+import java.util.Iterator;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.rometools.propono.atom.client.ClientEntry;
+import com.rometools.propono.atom.client.ClientMediaEntry;
+import com.rometools.propono.blogclient.BlogClientException;
+import com.rometools.propono.blogclient.BlogEntry;
+
+/**
+ * Atom protocol implementation of BlogClient entry iterator.
+ */
+public class AtomEntryIterator implements Iterator {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AtomEntryIterator.class);
+
+ private Iterator iterator = null;
+ private AtomCollection collection = null;
+
+ AtomEntryIterator(final AtomCollection collection) throws BlogClientException {
+ try {
+ this.collection = collection;
+ iterator = collection.getClientCollection().getEntries();
+ } catch (final Exception e) {
+ throw new BlogClientException("ERROR fetching collection", e);
+ }
+ }
+
+ /**
+ * True if more entries are available.
+ */
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ /**
+ * Get next entry.
+ */
+ @Override
+ public BlogEntry next() {
+ try {
+ final ClientEntry entry = iterator.next();
+ if (entry instanceof ClientMediaEntry) {
+ return new AtomResource(collection, (ClientMediaEntry) entry);
+ } else {
+ return new AtomEntry(collection, entry);
+ }
+ } catch (final Exception e) {
+ LOG.error("An error occured while fetching entry", e);
+ }
+ return null;
+ }
+
+ /**
+ * Remove is not supported.
+ */
+ @Override
+ public void remove() {
+ // optional method, not implemented
+ }
+}
diff --git a/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomResource.java b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomResource.java
new file mode 100644
index 0000000..59ccf60
--- /dev/null
+++ b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomResource.java
@@ -0,0 +1,132 @@
+/*
+ * 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 com.rometools.propono.blogclient.atomprotocol;
+
+import java.io.InputStream;
+import java.util.List;
+
+import com.rometools.propono.atom.client.ClientAtomService;
+import com.rometools.propono.atom.client.ClientCollection;
+import com.rometools.propono.atom.client.ClientEntry;
+import com.rometools.propono.atom.client.ClientMediaEntry;
+import com.rometools.propono.blogclient.BlogClientException;
+import com.rometools.propono.blogclient.BlogEntry;
+import com.rometools.propono.blogclient.BlogResource;
+import com.rometools.rome.feed.atom.Link;
+
+/**
+ * Atom protocol implementation of BlogResource.
+ */
+public class AtomResource extends AtomEntry implements BlogResource {
+
+ private AtomCollection collection;
+ private byte[] bytes;
+
+ AtomResource(final AtomCollection collection, final String name, final String contentType, final byte[] bytes) throws BlogClientException {
+ super((AtomBlog) collection.getBlog(), collection);
+ this.collection = collection;
+ this.bytes = bytes;
+ final BlogEntry.Content rcontent = new BlogEntry.Content();
+ rcontent.setType(contentType);
+ setContent(rcontent);
+ }
+
+ AtomResource(final AtomCollection collection, final ClientMediaEntry entry) throws BlogClientException {
+ super(collection, entry);
+ }
+
+ AtomResource(final AtomBlog blog, final ClientMediaEntry entry) throws BlogClientException {
+ super(blog, entry);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getName() {
+ return getTitle();
+ }
+
+ byte[] getBytes() {
+ return bytes;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public InputStream getAsStream() throws BlogClientException {
+ try {
+ return null; // ((ClientMediaEntry)clientEntry).getAsStream();
+ } catch (final Exception e) {
+ throw new BlogClientException("Error creating entry", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void save() throws BlogClientException {
+ try {
+ if (getToken() == null) {
+ final ClientAtomService clientService = ((AtomBlog) getBlog()).getService();
+ final ClientCollection clientCollection = collection.getClientCollection();
+
+ final ClientMediaEntry clientEntry = new ClientMediaEntry(clientService, clientCollection, getTitle(), "", getContent().getType(), getBytes());
+
+ copyToRomeEntry(clientEntry);
+ collection.getClientCollection().addEntry(clientEntry);
+ editURI = clientEntry.getEditURI();
+
+ } else {
+ final ClientAtomService clientService = ((AtomBlog) getBlog()).getService();
+ final ClientMediaEntry clientEntry = (ClientMediaEntry) clientService.getEntry(editURI);
+ clientEntry.update();
+ }
+ } catch (final Exception e) {
+ throw new BlogClientException("Error creating entry", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void update(final byte[] newBytes) throws BlogClientException {
+ try {
+ // ((ClientMediaEntry)clientEntry).setBytes(newBytes);
+ // clientEntry.update();
+ } catch (final Exception e) {
+ throw new BlogClientException("Error creating entry", e);
+ }
+ }
+
+ @Override
+ void copyFromRomeEntry(final ClientEntry entry) {
+ super.copyFromRomeEntry(entry);
+ final List links = entry.getOtherLinks();
+ if (links != null) {
+ for (final Link link : links) {
+ if ("edit-media".equals(link.getRel())) {
+ id = link.getHrefResolved();
+ break;
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/com/rometools/propono/blogclient/blogclient-diagram.gif b/src/main/java/com/rometools/propono/blogclient/blogclient-diagram.gif
new file mode 100644
index 0000000..35dac52
Binary files /dev/null and b/src/main/java/com/rometools/propono/blogclient/blogclient-diagram.gif differ
diff --git a/src/main/java/com/rometools/propono/blogclient/metaweblog/MetaWeblogBlog.java b/src/main/java/com/rometools/propono/blogclient/metaweblog/MetaWeblogBlog.java
new file mode 100644
index 0000000..5367d57
--- /dev/null
+++ b/src/main/java/com/rometools/propono/blogclient/metaweblog/MetaWeblogBlog.java
@@ -0,0 +1,467 @@
+/*
+ * 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 com.rometools.propono.blogclient.metaweblog;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+import org.apache.xmlrpc.client.XmlRpcClient;
+import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
+
+import com.rometools.propono.blogclient.Blog;
+import com.rometools.propono.blogclient.BlogClientException;
+import com.rometools.propono.blogclient.BlogEntry;
+import com.rometools.propono.blogclient.BlogEntry.Category;
+import com.rometools.propono.blogclient.BlogResource;
+
+/**
+ * Blog implementation that uses a mix of Blogger and MetaWeblog API methods.
+ */
+public class MetaWeblogBlog implements Blog {
+
+ private final String blogid;
+ private final String name;
+ private final URL url;
+ private final String userName;
+ private final String password;
+ private final Map collections;
+
+ private String appkey = "dummy";
+ private XmlRpcClient xmlRpcClient = null;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getToken() {
+ return blogid;
+ }
+
+ /**
+ * String representation of blog, returns the name.
+ */
+ @Override
+ public String toString() {
+ return getName();
+ }
+
+ private XmlRpcClient getXmlRpcClient() {
+
+ if (xmlRpcClient == null) {
+ final XmlRpcClientConfigImpl xmlrpcConfig = new XmlRpcClientConfigImpl();
+ xmlrpcConfig.setServerURL(url);
+ xmlRpcClient = new XmlRpcClient();
+ xmlRpcClient.setConfig(xmlrpcConfig);
+ }
+ return xmlRpcClient;
+ }
+
+ MetaWeblogBlog(final String blogid, final String name, final URL url, final String userName, final String password) {
+ this.blogid = blogid;
+ this.name = name;
+ this.url = url;
+ this.userName = userName;
+ this.password = password;
+ collections = new TreeMap();
+ collections.put("entries", new MetaWeblogBlogCollection(this, "entries", "Entries", "entry"));
+ collections.put("resources", new MetaWeblogBlogCollection(this, "resources", "Resources", "*"));
+ }
+
+ MetaWeblogBlog(final String blogId, final String name, final URL url, final String userName, final String password, final String appkey) {
+ this(blogId, name, url, userName, password);
+ this.appkey = appkey;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BlogEntry newEntry() {
+ return new MetaWeblogEntry(this, new HashMap());
+ }
+
+ String saveEntry(final BlogEntry entry) throws BlogClientException {
+ final Blog.Collection col = collections.get("entries");
+ return col.saveEntry(entry);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BlogEntry getEntry(final String id) throws BlogClientException {
+ try {
+ final Object[] params = new Object[] { id, userName, password };
+ final Object response = getXmlRpcClient().execute("metaWeblog.getPost", params);
+ @SuppressWarnings("unchecked")
+ final Map result = (Map) response;
+ return new MetaWeblogEntry(this, result);
+ } catch (final Exception e) {
+ throw new BlogClientException("ERROR: XML-RPC error getting entry", e);
+ }
+ }
+
+ void deleteEntry(final String id) throws BlogClientException {
+ try {
+ getXmlRpcClient().execute("blogger.deletePost", new Object[] { appkey, id, userName, password, Boolean.FALSE });
+ } catch (final Exception e) {
+ throw new BlogClientException("ERROR: XML-RPC error getting entry", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Iterator getEntries() throws BlogClientException {
+ return new EntryIterator();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BlogResource newResource(final String name, final String contentType, final byte[] bytes) throws BlogClientException {
+ return new MetaWeblogResource(this, name, contentType, bytes);
+ }
+
+ String saveResource(final MetaWeblogResource resource) throws BlogClientException {
+ final Blog.Collection col = collections.get("resources");
+ return col.saveResource(resource);
+ }
+
+ BlogResource getResource(final String token) throws BlogClientException {
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public NoOpIterator getResources() throws BlogClientException {
+ return new NoOpIterator();
+ }
+
+ void deleteResource(final BlogResource resource) throws BlogClientException {
+ // no-op
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List getCategories() throws BlogClientException {
+
+ final ArrayList ret = new ArrayList();
+
+ try {
+
+ final Object result = getXmlRpcClient().execute("metaWeblog.getCategories", new Object[] { blogid, userName, password });
+
+ if (result != null && result instanceof HashMap) {
+
+ // Standard MetaWeblog API style: struct of struts
+ @SuppressWarnings("unchecked")
+ final Map catsmap = (Map) result;
+
+ final Set keys = catsmap.keySet();
+ for (final String key : keys) {
+ @SuppressWarnings("unchecked")
+ final Map catmap = (Map) catsmap.get(key);
+ final BlogEntry.Category category = new BlogEntry.Category(key);
+ final String description = (String) catmap.get("description");
+ category.setName(description);
+ // catmap.get("htmlUrl");
+ // catmap.get("rssUrl");
+ ret.add(category);
+ }
+
+ } else if (result != null && result instanceof Object[]) {
+ // Wordpress style: array of structs
+ final Object[] array = (Object[]) result;
+ for (final Object map : array) {
+ @SuppressWarnings("unchecked")
+ final Map catmap = (Map) map;
+ final String categoryId = (String) catmap.get("categoryId");
+ final String categoryName = (String) catmap.get("categoryName");
+ final BlogEntry.Category category = new BlogEntry.Category(categoryId);
+ category.setName(categoryName);
+ ret.add(category);
+ }
+ }
+ } catch (final Exception e) {
+ e.printStackTrace();
+ }
+
+ return ret;
+
+ }
+
+ private Map createPostStructure(final BlogEntry entry) {
+ return ((MetaWeblogEntry) entry).toPostStructure();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List getCollections() throws BlogClientException {
+ return new ArrayList(collections.values());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Blog.Collection getCollection(final String token) throws BlogClientException {
+ return 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(final Blog blog, final String token, final String title, final String accept) {
+ this.blog = blog;
+ this.accept = accept;
+ this.title = title;
+ this.token = token;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getToken() {
+ return token;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List getAccepts() {
+ return Collections.singletonList(accept);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BlogResource newResource(final String name, final String contentType, final byte[] bytes) throws BlogClientException {
+ return blog.newResource(name, contentType, bytes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BlogEntry newEntry() throws BlogClientException {
+ return blog.newEntry();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean accepts(final 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}
+ */
+ @Override
+ public Iterator getEntries() throws BlogClientException {
+ Iterator ret = null;
+ if (accept.equals("entry")) {
+ ret = MetaWeblogBlog.this.getEntries();
+ } else {
+ ret = getResources();
+ }
+ return ret;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String saveEntry(final 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 (final 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 (final Exception e) {
+ throw new BlogClientException("ERROR: XML-RPC error updating entry", e);
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String saveResource(final BlogResource res) throws BlogClientException {
+ final MetaWeblogResource resource = (MetaWeblogResource) res;
+ try {
+ final HashMap resmap = new HashMap();
+ resmap.put("name", resource.getName());
+ resmap.put("type", resource.getContent().getType());
+ resmap.put("bits", resource.getBytes());
+ final Object[] params = new Object[] { blogid, userName, password, resmap };
+ final Object response = getXmlRpcClient().execute("metaWeblog.newMediaObject", params);
+ @SuppressWarnings("unchecked")
+ final Map result = (Map) response;
+ final String url = (String) result.get("url");
+ res.getContent().setSrc(url);
+ return url;
+ } catch (final Exception e) {
+ throw new BlogClientException("ERROR: loading or uploading file", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public List getCategories() throws BlogClientException {
+ return MetaWeblogBlog.this.getCategories();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ 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