diff --git a/CHANGES b/CHANGES
new file mode 100644
index 0000000..0b2c899
--- /dev/null
+++ b/CHANGES
@@ -0,0 +1,58 @@
+
+ROME Propono Subproject
+
+
+ROME Propono 0.6, September 30, 2007
+------------------------------------
+
+* Updated for APP final (draft #17) w/new APP URI "http://www.w3.org/2007/app"
+
+* Tested file-based server against Tim Bray's Ape (from CVS September 30, 2007)
+ with some warning and one error that is apparently due to a bug in Ape.
+
+* Now includes pre-release of ROME 0.9.1 with key Atom parse fixes.
+
+* Changed arguements in Atom server's AtomHandler interface to accept
+ AtomRequest objects instead of String[] pathinfo arrays.
+
+* Added support for relative URIs in the Service Document
+ - Fixes https://rome.dev.java.net/issues/show_bug.cgi?id=67
+ - Added Collection.getHrefResolved()
+ - Added Categories.getHrefResolved()
+
+* Added new options to the file-based server's propono.properties file so you
+ can turn on/off relative URIs and inline categories.
+ propono.atomserver.filebased.relativeURIs=true
+ propono.atomserver.filebased.inlineCategories=true
+
+* Added support for out-of-line categories in Atom client classes
+ - Added new Categories.href property
+ - New ClientCategories classes can fetch remote categories from href URI
+ - Fixes https://rome.dev.java.net/issues/show_bug.cgi?id=68
+
+* Added support for out-of-line categories in Atom server classes
+ - New AtomHandler.getCategoriesDocument(String[] pathInfo) method
+ - New AtomHandler.isCategoriesDocumentURI(String[] pathInfo) method
+
+* Renamed Introspection to Service Document
+ - AtomHandler.isIntrospectionURI() -> AtomHandler.isSerivceDocumentURI()
+ - AtomHandler.getIntrospection() -> AtomHandler.getServiceDocument()
+ - Added String[] pathInfo argument to getServiceDocument()
+
+* Renamed PubControlModule to AppModule becuase it also supports app:edited
+ - Added rome.properties file to configure AppModule
+
+
+ROME Propono 0.5, April 23, 2007
+--------------------------------
+
+* Fixes in Blog Client constructors
+* AtomServlet uses application/atomsvc+xml for the Service Document
+* Fixed issue #66: don't expect entry to be returned from update
+* Made example builds more configurable
+
+
+ROME Propono 0.4, April 10, 2007
+--------------------------------
+
+* Initial release
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/pom.xml b/pom.xml
new file mode 100644
index 0000000..cd35968
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,292 @@
+
+
+ 4.0.0
+ org.rometools
+ rome-propono\
+ rome-propono
+ 1.1-SNAPSHOT
+ jar
+ 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://www.rometools.org
+
+ https://rometools.jira.com/browse/PROPONO
+
+
+
+
+
+ dev@rome.dev.java.net
+
+
+
+
+ 2007
+
+
+ dev@rome.dev.java.net
+
+ https://rome.dev.java.net/servlets/ProjectMailingListList
+
+
+ https://rome.dev.java.net/servlets/ProjectMailingListList
+
+
+ https://rome.dev.java.net/servlets/SummarizeList?listName=dev
+
+
+
+
+
+ Dave Johnson
+ http://rollerweblogger.org/roller
+ -5
+
+
+ Robert Cooper
+ http://www.screaming-penguin.com
+ -4
+ kebernet@gmail.com
+
+
+
+ ROME Project
+ http://www.rometools.org
+
+
+
+ The Apache Software License, Version 2.0
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+ scm:svn:https://rometools.jira.com/svn/MODULES/trunk
+ scm:svn:https://rometools.jira.com/svn/MODULES/trunk
+ https://rometools.jira.com/source/browse/MODULES
+
+
+
+ oauth
+ http://oauth.googlecode.com/svn/code/maven/
+
+
+
+
+ install
+
+
+ org.apache.maven.plugins
+ maven-release-plugin
+ 2.0-beta-9
+
+ forked-path
+
+
+
+ maven-compiler-plugin
+
+ 1.5
+ 1.5
+
+
+
+ maven-javadoc-plugin
+
+ com.sun.syndication.propono.atom.server.impl
+
+
+
+ maven-surefire-plugin
+
+
+ **/Test*.java
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+
+ jar
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+
+ jar
+
+
+
+
+
+
+
+
+
+
+
+ org.rometools
+ rome
+ 1.1-SNAPSHOT
+
+
+
+ commons-codec
+ commons-codec
+ 1.3
+
+
+
+ commons-httpclient
+ commons-httpclient
+ 3.0.1
+
+
+
+ commons-lang
+ commons-lang
+ 2.4
+
+
+
+ commons-logging
+ commons-logging
+ 1.0.4
+
+
+
+ commons-beanutils
+ commons-beanutils
+ 1.6
+
+
+
+ ws-commons-util
+ ws-commons-util
+ 1.0.1
+
+
+
+ xmlrpc
+ xmlrpc-client
+ 3.0
+
+
+
+ xmlrpc
+ xmlrpc-common
+ 3.0
+
+
+
+ javax.activation
+ activation
+ 1.1
+
+
+
+ javax.servlet
+ servlet-api
+ 2.5
+
+
+
+ net.oauth.core
+ oauth
+ 20090531
+ compile
+
+
+
+ net.oauth.core
+ oauth-provider
+ 20090531
+ compile
+
+
+
+ junit
+ junit
+ 3.8.2
+ test
+
+
+
+ org.mortbay.jetty
+ jetty
+ 4.2.12
+ test
+
+
+
+
+
+
+
+ maven-changes-plugin
+
+ ${basedir}/xdocs/changes.xml
+
+
+
+
+
+
+
+ sonatype-nexus-staging
+ Nexus Release Repository
+ http://oss.sonatype.org/service/local/staging/deploy/maven2
+
+
+ sonatype-nexus-snapshots
+ Sonatype Nexus Snapshots
+ http://oss.sonatype.org/content/repositories/snapshots
+
+
+
+
+
+ release-sign-artifacts
+
+
+ performRelease
+ true
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 1.0-alpha-4
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/sun/syndication/propono/atom/client/AtomClientFactory.java b/src/main/java/com/sun/syndication/propono/atom/client/AtomClientFactory.java
new file mode 100644
index 0000000..4e7935b
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/client/AtomClientFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.client;
+
+import com.sun.syndication.propono.utils.ProponoException;
+import com.sun.syndication.io.impl.Atom10Parser;
+
+
+/**
+ * Creates AtomService or ClientCollection based on username, password and
+ * end-point URI of Atom protocol service.
+ */
+public class AtomClientFactory {
+
+ static {
+ Atom10Parser.setResolveURIs(true);
+ }
+
+ /**
+ * Create AtomService by reading service doc from Atom Server.
+ */
+ public static ClientAtomService getAtomService(
+ String uri, AuthStrategy authStrategy) throws ProponoException {
+ return new ClientAtomService(uri, authStrategy);
+ }
+
+ /**
+ * Create ClientCollection bound to URI.
+ */
+ public static ClientCollection getCollection(
+ String uri, AuthStrategy authStrategy) throws ProponoException {
+ return new ClientCollection(uri, authStrategy);
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/client/AuthStrategy.java b/src/main/java/com/sun/syndication/propono/atom/client/AuthStrategy.java
new file mode 100644
index 0000000..e8bd37f
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/client/AuthStrategy.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2007 Dave Johnson (Blogapps project)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.client;
+
+import com.sun.syndication.propono.utils.ProponoException;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+
+
+public interface AuthStrategy {
+
+ /**
+ * Add authentication credenticals, tokens, etc. to HTTP method
+ */
+ void addAuthentication(HttpClient httpClient, HttpMethodBase method)
+ throws ProponoException;
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/client/BasicAuthStrategy.java b/src/main/java/com/sun/syndication/propono/atom/client/BasicAuthStrategy.java
new file mode 100644
index 0000000..ce200f5
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/client/BasicAuthStrategy.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2009 Dave Johnson (Blogapps project)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.client;
+
+import com.sun.syndication.io.impl.Base64;
+import com.sun.syndication.propono.utils.ProponoException;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+
+
+public class BasicAuthStrategy implements AuthStrategy {
+ private String credentials;
+
+ public BasicAuthStrategy(String username, String password) {
+ this.credentials =
+ new String(new Base64().encode((username + ":" + password).getBytes()));
+ }
+
+ public void init() throws ProponoException {
+ // op-op
+ }
+
+ public void addAuthentication(HttpClient httpClient, HttpMethodBase method) throws ProponoException {
+ httpClient.getParams().setAuthenticationPreemptive(true);
+ String header = "Basic " + credentials;
+ method.setRequestHeader("Authorization", header);
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/client/ClientAtomService.java b/src/main/java/com/sun/syndication/propono/atom/client/ClientAtomService.java
new file mode 100644
index 0000000..873d824
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/client/ClientAtomService.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.client;
+
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.io.impl.Atom10Parser;
+import com.sun.syndication.propono.utils.ProponoException;
+import com.sun.syndication.propono.atom.common.AtomService;
+import java.io.InputStreamReader;
+import java.util.Iterator;
+import java.util.List;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.input.SAXBuilder;
+
+
+/**
+ * This class models an Atom Publising Protocol Service Document.
+ * It extends the common
+ * {@link com.sun.syndication.propono.atom.common.Collection}
+ * class to add a getEntry() method and to return
+ * {@link com.sun.syndication.propono.atom.client.ClientWorkspace}
+ * objects instead of common
+ * {@link com.sun.syndication.propono.atom.common.Workspace}s.
+ */
+public class ClientAtomService extends AtomService {
+ private static Log logger = LogFactory.getLog(ClientAtomService.class);
+ private String uri = null;
+ private HttpClient httpClient = null;
+ private AuthStrategy authStrategy = null;
+
+ /**
+ * Create Atom blog service instance for specified URL and user account.
+ * @param url End-point URL of Atom service
+ */
+ ClientAtomService(String uri, AuthStrategy authStrategy)
+ throws ProponoException {
+ this.uri = uri;
+ this.authStrategy = authStrategy;
+ Document doc = getAtomServiceDocument();
+ parseAtomServiceDocument(doc);
+ }
+
+ /**
+ * Get full entry from service by entry edit URI.
+ */
+ public ClientEntry getEntry(String uri) throws ProponoException {
+ GetMethod method = new GetMethod(uri);
+ authStrategy.addAuthentication(httpClient, method);
+ try {
+ httpClient.executeMethod(method);
+ if (method.getStatusCode() != 200) {
+ throw new ProponoException("ERROR HTTP status code=" + method.getStatusCode());
+ }
+ Entry romeEntry = Atom10Parser.parseEntry(
+ new InputStreamReader(method.getResponseBodyAsStream()), uri);
+ if (!romeEntry.isMediaEntry()) {
+ return new ClientEntry(this, null, romeEntry, false);
+ } else {
+ return new ClientMediaEntry(this, null, romeEntry, false);
+ }
+ } catch (Exception e) {
+ throw new ProponoException("ERROR: getting or parsing entry/media", e);
+ } finally {
+ method.releaseConnection();
+ }
+ }
+
+ void addAuthentication(HttpMethodBase method) throws ProponoException {
+ authStrategy.addAuthentication(httpClient, method);
+ }
+
+ AuthStrategy getAuthStrategy() {
+ return authStrategy;
+ }
+
+ private Document getAtomServiceDocument() throws ProponoException {
+ GetMethod method = null;
+ int code = -1;
+ try {
+ httpClient = new HttpClient(new MultiThreadedHttpConnectionManager());
+ // TODO: make connection timeout configurable
+ httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(30000);
+
+ method = new GetMethod(uri);
+ authStrategy.addAuthentication(httpClient, method);
+ httpClient.executeMethod(method);
+
+ SAXBuilder builder = new SAXBuilder();
+ return builder.build(method.getResponseBodyAsStream());
+
+ } catch (Throwable t) {
+ String msg = "ERROR retrieving Atom Service Document, code: "+code;
+ logger.debug(msg, t);
+ throw new ProponoException(msg, t);
+ } finally {
+ if (method != null) method.releaseConnection();
+ }
+ }
+
+ /** Deserialize an Atom service XML document into an object */
+ private void parseAtomServiceDocument(Document document) throws ProponoException {
+ Element root = document.getRootElement();
+ List spaces = root.getChildren("workspace", AtomService.ATOM_PROTOCOL);
+ Iterator iter = spaces.iterator();
+ while (iter.hasNext()) {
+ Element e = (Element) iter.next();
+ addWorkspace(new ClientWorkspace(e, this, uri));
+ }
+ }
+
+ /**
+ * Package access to httpClient.
+ */
+ HttpClient getHttpClient() {
+ return httpClient;
+ }
+}
+
diff --git a/src/main/java/com/sun/syndication/propono/atom/client/ClientCategories.java b/src/main/java/com/sun/syndication/propono/atom/client/ClientCategories.java
new file mode 100644
index 0000000..965d1ca
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/client/ClientCategories.java
@@ -0,0 +1,70 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package com.sun.syndication.propono.atom.client;
+
+import com.sun.syndication.feed.atom.Category;
+import com.sun.syndication.propono.atom.common.*;
+import com.sun.syndication.propono.utils.ProponoException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.JDOMException;
+import org.jdom.input.SAXBuilder;
+
+
+/**
+ * Models an Atom protocol Categories element, which may contain ROME Atom
+ * {@link com.sun.syndication.feed.atom.Category} elements.
+ */
+public class ClientCategories extends Categories {
+ private ClientCollection clientCollection = null;
+
+ /** Load select from XML element */
+ public ClientCategories(Element e, ClientCollection clientCollection) throws ProponoException {
+ this.clientCollection = clientCollection;
+ parseCategoriesElement(e);
+ if (getHref() != null) fetchContents();
+ }
+
+ public void fetchContents() throws ProponoException {
+ GetMethod method = new GetMethod(getHrefResolved());
+ clientCollection.addAuthentication(method);
+ try {
+ clientCollection.getHttpClient().executeMethod(method);
+ if (method.getStatusCode() != 200) {
+ throw new ProponoException("ERROR HTTP status code=" + method.getStatusCode());
+ }
+ SAXBuilder builder = new SAXBuilder();
+ Document catsDoc = builder.build(
+ new InputStreamReader(method.getResponseBodyAsStream()));
+ parseCategoriesElement(catsDoc.getRootElement());
+
+ } catch (IOException ioe) {
+ throw new ProponoException(
+ "ERROR: reading out-of-line categories", ioe);
+ } catch (JDOMException jde) {
+ throw new ProponoException(
+ "ERROR: parsing out-of-line categories", jde);
+ } finally {
+ method.releaseConnection();
+ }
+ }
+}
+
diff --git a/src/main/java/com/sun/syndication/propono/atom/client/ClientCollection.java b/src/main/java/com/sun/syndication/propono/atom/client/ClientCollection.java
new file mode 100644
index 0000000..94b270a
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/client/ClientCollection.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.client;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.io.InputStreamReader;
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.io.impl.Atom10Parser;
+import com.sun.syndication.propono.atom.common.AtomService;
+import com.sun.syndication.propono.atom.common.Categories;
+import com.sun.syndication.propono.utils.ProponoException;
+import com.sun.syndication.propono.atom.common.Collection;
+import com.sun.syndication.propono.atom.common.Workspace;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.jdom.Element;
+
+/**
+ * Models an Atom collection, extends Collection and adds methods for adding,
+ * retrieving, updateing and deleting entries.
+ */
+public class ClientCollection extends Collection {
+ static final Log logger = LogFactory.getLog(ClientCollection.class);
+
+ private List categories = new ArrayList();
+ private HttpClient httpClient = null;
+ private AuthStrategy authStrategy = null;
+ private boolean writable = true;
+
+ private ClientWorkspace workspace = null;
+ private ClientAtomService service = null;
+
+ ClientCollection(Element e, ClientWorkspace workspace, String baseURI) throws ProponoException {
+ super(e, baseURI);
+ this.workspace = workspace;
+ this.service = workspace.getAtomService();
+ this.httpClient = workspace.getAtomService().getHttpClient();
+ this.authStrategy = workspace.getAtomService().getAuthStrategy();
+ parseCollectionElement(e);
+ }
+
+ ClientCollection(String href, AuthStrategy authStrategy) throws ProponoException {
+ super("Standalone connection", "text", href);
+ this.authStrategy = authStrategy;
+ try {
+ httpClient = new HttpClient(new MultiThreadedHttpConnectionManager());
+ // TODO: make connection timeout configurable
+ httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(30000);
+ } catch (Throwable t) {
+ throw new ProponoException("ERROR creating HTTPClient", t);
+ }
+ }
+
+ void addAuthentication(HttpMethodBase method) throws ProponoException {
+ authStrategy.addAuthentication(httpClient, method);
+ }
+
+ /**
+ * Package access to httpClient to allow use by ClientEntry and ClientMediaEntry.
+ */
+ HttpClient getHttpClient() {
+ return httpClient;
+ }
+
+ /**
+ * Get iterator over entries in this collection. Entries returned are
+ * considered to be partial entries cannot be saved/updated.
+ */
+ public Iterator getEntries() throws ProponoException {
+ return new EntryIterator(this);
+ }
+
+ /**
+ * Get full entry specified by entry edit URI.
+ * Note that entry may or may not be associated with this collection.
+ * @return ClientEntry or ClientMediaEntry specified by URI.
+ */
+ public ClientEntry getEntry(String uri) throws ProponoException {
+ GetMethod method = new GetMethod(uri);
+ authStrategy.addAuthentication(httpClient, method);
+ try {
+ httpClient.executeMethod(method);
+ if (method.getStatusCode() != 200) {
+ throw new ProponoException("ERROR HTTP status code=" + method.getStatusCode());
+ }
+ Entry romeEntry = Atom10Parser.parseEntry(
+ new InputStreamReader(method.getResponseBodyAsStream()), uri);
+ if (!romeEntry.isMediaEntry()) {
+ return new ClientEntry(service, this, romeEntry, false);
+ } else {
+ return new ClientMediaEntry(service, this, romeEntry, false);
+ }
+ } catch (Exception e) {
+ throw new ProponoException("ERROR: getting or parsing entry/media, HTTP code: ", e);
+ } finally {
+ method.releaseConnection();
+ }
+ }
+
+ /**
+ * Get workspace or null if collection is not associated with a workspace.
+ */
+ public Workspace getWorkspace() {
+ return workspace;
+ }
+
+ /**
+ * Determines if collection is writable.
+ */
+ public boolean isWritable() {
+ return writable;
+ }
+
+ /**
+ * Create new entry associated with collection, but do not save to server.
+ * @throws ProponoException if collecton is not writable.
+ */
+ public ClientEntry createEntry() throws ProponoException {
+ if (!isWritable()) throw new ProponoException("Collection is not writable");
+ return new ClientEntry(service, this);
+ }
+
+ /**
+ * Create new media entry assocaited with collection, but do not save.
+ * server. Depending on the Atom server, you may or may not be able to
+ * persist the properties of the entry that is returned.
+ * @param title Title to used for uploaded file.
+ * @param slug String to be used in file-name of stored file
+ * @param contentType MIME content-type of file.
+ * @param bytes Data to be uploaded as byte array.
+ * @throws ProponoException if collecton is not writable
+ */
+ public ClientMediaEntry createMediaEntry(
+ String title, String slug, String contentType, byte[] bytes)
+ throws ProponoException {
+ if (!isWritable()) throw new ProponoException("Collection is not writable");
+ return new ClientMediaEntry(service, this, title, slug, contentType, bytes);
+ }
+
+ /**
+ * Create new media entry assocaited with collection, but do not save.
+ * server. Depending on the Atom server, you may or may not be able to.
+ * persist the properties of the entry that is returned.
+ * @param title Title to used for uploaded file.
+ * @param slug String to be used in file-name of stored file
+ * @param contentType MIME content-type of file.
+ * @param is Data to be uploaded as InputStream.
+ * @throws ProponoException if collecton is not writable
+ */
+ public ClientMediaEntry createMediaEntry(
+ String title, String slug, String contentType, InputStream is)
+ throws ProponoException {
+ if (!isWritable()) throw new ProponoException("Collection is not writable");
+ return new ClientMediaEntry(service, this, title, slug, contentType, is);
+ }
+
+ /**
+ * Save to collection a new entry that was created by a createEntry()
+ * or createMediaEntry() and save it to the server.
+ * @param entry Entry to be saved.
+ * @throws ProponoException on error, if collection is not writable or if entry is partial.
+ */
+ public void addEntry(ClientEntry entry) throws ProponoException {
+ if (!isWritable()) throw new ProponoException("Collection is not writable");
+ entry.addToCollection(this);
+ }
+
+ protected void parseCollectionElement(Element element) throws ProponoException {
+ if (workspace == null) return;
+
+ setHref(element.getAttribute("href").getValue());
+
+ Element titleElem = element.getChild("title", AtomService.ATOM_FORMAT);
+ if (titleElem != null) {
+ setTitle(titleElem.getText());
+ if (titleElem.getAttribute("type", AtomService.ATOM_FORMAT) != null) {
+ setTitleType(titleElem.getAttribute("type", AtomService.ATOM_FORMAT).getValue());
+ }
+ }
+
+ List acceptElems = element.getChildren("accept", AtomService.ATOM_PROTOCOL);
+ if (acceptElems != null && acceptElems.size() > 0) {
+ for (Iterator it = acceptElems.iterator(); it.hasNext();) {
+ Element acceptElem = (Element)it.next();
+ addAccept(acceptElem.getTextTrim());
+ }
+ }
+
+ // Loop to parse element to Categories objects
+ List catsElems = element.getChildren("categories", AtomService.ATOM_PROTOCOL);
+ for (Iterator catsIter = catsElems.iterator(); catsIter.hasNext();) {
+ Element catsElem = (Element) catsIter.next();
+ Categories cats = new ClientCategories(catsElem, this);
+ addCategories(cats);
+ }
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/client/ClientEntry.java b/src/main/java/com/sun/syndication/propono/atom/client/ClientEntry.java
new file mode 100644
index 0000000..d22f303
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/client/ClientEntry.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.client;
+
+import com.sun.syndication.feed.atom.Content;
+import com.sun.syndication.feed.atom.Link;
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.io.impl.Atom10Generator;
+import com.sun.syndication.io.impl.Atom10Parser;
+import com.sun.syndication.propono.utils.ProponoException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+import org.apache.commons.httpclient.methods.DeleteMethod;
+import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
+import org.apache.commons.httpclient.methods.PutMethod;
+import org.apache.commons.httpclient.methods.StringRequestEntity;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import com.sun.syndication.propono.utils.Utilities;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.beanutils.BeanUtils;
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+import org.apache.commons.httpclient.methods.PostMethod;
+
+/**
+ * Client implementation of Atom entry, extends ROME Entry to add methods for
+ * easily getting/setting content, updating and removing the entry from the server.
+ */
+public class ClientEntry extends Entry {
+ private static final Log logger = LogFactory.getLog(ClientEntry.class);
+
+ boolean partial = false;
+
+ private ClientAtomService service = null;
+ private ClientCollection collection = null;
+
+ public ClientEntry(ClientAtomService service, ClientCollection collection) {
+ super();
+ this.service = service;
+ this.collection = collection;
+ }
+
+ public ClientEntry(ClientAtomService service, ClientCollection collection,
+ Entry entry, boolean partial) throws ProponoException {
+ super();
+ this.service = service;
+ this.collection = collection;
+ this.partial = partial;
+ try {
+ BeanUtils.copyProperties(this, entry);
+ } catch (Exception e) {
+ throw new ProponoException("ERROR: copying fields from ROME entry", e);
+ }
+ }
+
+ /**
+ * Set content of entry.
+ * @param contentString content string.
+ * @param type Must be "text" for plain text, "html" for escaped HTML,
+ * "xhtml" for XHTML or a valid MIME content-type.
+ */
+ public void setContent(String contentString, String type) {
+ Content newContent = new Content();
+ newContent.setType(type == null ? Content.HTML : type);
+ newContent.setValue(contentString);
+ ArrayList contents = new ArrayList();
+ contents.add(newContent);
+ setContents(contents);
+ }
+
+ /**
+ * Convenience method to set first content object in content collection.
+ * Atom 1.0 allows only one content element per entry.
+ */
+ public void setContent(Content c) {
+ ArrayList contents = new ArrayList();
+ contents.add(c);
+ setContents(contents);
+ }
+
+ /**
+ * Convenience method to get first content object in content collection.
+ * Atom 1.0 allows only one content element per entry.
+ */
+ public Content getContent() {
+ if (getContents() != null && getContents().size() > 0) {
+ Content c = (Content)getContents().get(0);
+ return c;
+ }
+ return null;
+ }
+
+ /**
+ * Determines if entries are equal based on edit URI.
+ */
+ public boolean equals(Object o) {
+ if (o instanceof ClientEntry) {
+ ClientEntry other = (ClientEntry)o;
+ if (other.getEditURI() != null && getEditURI() != null) {
+ return other.getEditURI().equals(getEditURI());
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Update entry by posting new representation of entry to server.
+ * Note that you should not attempt to update entries that you get from
+ * iterating over a collection they may be "partial" entries. If you want
+ * to update an entry, you must get it via one of the getEntry()
+ * methods in
+ * {@link com.sun.syndication.propono.atom.common.Collection} or
+ * {@link com.sun.syndication.propono.atom.common.AtomService}.
+ * @throws ProponoException If entry is a "partial" entry.
+ */
+ public void update() throws ProponoException {
+ if (partial) throw new ProponoException("ERROR: attempt to update partial entry");
+ EntityEnclosingMethod method = new PutMethod(getEditURI());
+ addAuthentication(method);
+ StringWriter sw = new StringWriter();
+ int code = -1;
+ try {
+ Atom10Generator.serializeEntry(this, sw);
+ method.setRequestEntity(new StringRequestEntity(sw.toString()));
+ method.setRequestHeader(
+ "Content-type", "application/atom+xml; charset=utf-8");
+ getHttpClient().executeMethod(method);
+ InputStream is = method.getResponseBodyAsStream();
+ if (method.getStatusCode() != 200 && method.getStatusCode() != 201) {
+ throw new ProponoException(
+ "ERROR HTTP status=" + method.getStatusCode() + " : " + Utilities.streamToString(is));
+ }
+
+ } catch (Exception e) {
+ String msg = "ERROR: updating entry, HTTP code: " + code;
+ logger.debug(msg, e);
+ throw new ProponoException(msg, e);
+ } finally {
+ method.releaseConnection();
+ }
+ }
+
+ /**
+ * Remove entry from server.
+ */
+ public void remove() throws ProponoException {
+ if (getEditURI() == null) {
+ throw new ProponoException("ERROR: cannot delete unsaved entry");
+ }
+ DeleteMethod method = new DeleteMethod(getEditURI());
+ addAuthentication(method);
+ try {
+ getHttpClient().executeMethod(method);
+ } catch (IOException ex) {
+ throw new ProponoException("ERROR: removing entry, HTTP code", ex);
+ } finally {
+ method.releaseConnection();
+ }
+ }
+
+ void setCollection(ClientCollection collection) {
+ this.collection = collection;
+ }
+
+ ClientCollection getCollection() {
+ return collection;
+ }
+
+ /**
+ * Get the URI that can be used to edit the entry via HTTP PUT or DELETE.
+ */
+ public String getEditURI() {
+ for (int i=0; i 0) {
+ Content c = (Content)getContents().get(0);
+ if (c.getSrc() != null) {
+ return getResourceAsStream();
+ } else if (inputStream != null) {
+ return inputStream;
+ } else if (bytes != null) {
+ return new ByteArrayInputStream(bytes);
+ } else {
+ throw new ProponoException("ERROR: no src URI or binary data to return");
+ }
+ }
+ else {
+ throw new ProponoException("ERROR: no content found in entry");
+ }
+ }
+
+ private InputStream getResourceAsStream() throws ProponoException {
+ if (getEditURI() == null) {
+ throw new ProponoException("ERROR: not yet saved to server");
+ }
+ GetMethod method = new GetMethod(((Content)getContents()).getSrc());
+ try {
+ getCollection().getHttpClient().executeMethod(method);
+ if (method.getStatusCode() != 200) {
+ throw new ProponoException("ERROR HTTP status=" + method.getStatusCode());
+ }
+ return method.getResponseBodyAsStream();
+ } catch (IOException e) {
+ throw new ProponoException("ERROR: getting media entry", e);
+ }
+ }
+
+ /**
+ * Update entry on server.
+ */
+ public void update() throws ProponoException {
+ if (partial) throw new ProponoException("ERROR: attempt to update partial entry");
+ EntityEnclosingMethod method = null;
+ Content updateContent = (Content)getContents().get(0);
+ try {
+ if (getMediaLinkURI() != null && getBytes() != null) {
+ // existing media entry and new file, so PUT file to edit-media URI
+ method = new PutMethod(getMediaLinkURI());
+ if (inputStream != null) {
+ method.setRequestEntity(new InputStreamRequestEntity(inputStream));
+ } else {
+ method.setRequestEntity(new InputStreamRequestEntity(new ByteArrayInputStream(getBytes())));
+ }
+
+ method.setRequestHeader("Content-type", updateContent.getType());
+ }
+ else if (getEditURI() != null) {
+ // existing media entry and NO new file, so PUT entry to edit URI
+ method = new PutMethod(getEditURI());
+ StringWriter sw = new StringWriter();
+ Atom10Generator.serializeEntry(this, sw);
+ method.setRequestEntity(new StringRequestEntity(sw.toString()));
+ method.setRequestHeader(
+ "Content-type", "application/atom+xml; charset=utf8");
+ } else {
+ throw new ProponoException("ERROR: media entry has no edit URI or media-link URI");
+ }
+ this.getCollection().addAuthentication(method);
+ method.addRequestHeader("Title", getTitle());
+ getCollection().getHttpClient().executeMethod(method);
+ if (inputStream != null) inputStream.close();
+ InputStream is = method.getResponseBodyAsStream();
+ if (method.getStatusCode() != 200 && method.getStatusCode() != 201) {
+ throw new ProponoException(
+ "ERROR HTTP status=" + method.getStatusCode() + " : " + Utilities.streamToString(is));
+ }
+
+ } catch (Exception e) {
+ throw new ProponoException("ERROR: saving media entry");
+ }
+ if (method.getStatusCode() != 201) {
+ throw new ProponoException("ERROR HTTP status=" + method.getStatusCode());
+ }
+ }
+
+ /** Package access, to be called by DefaultClientCollection */
+ void addToCollection(ClientCollection col) throws ProponoException {
+ setCollection(col);
+ EntityEnclosingMethod method = new PostMethod(col.getHrefResolved());
+ getCollection().addAuthentication(method);
+ StringWriter sw = new StringWriter();
+ boolean error = false;
+ try {
+ Content c = (Content)getContents().get(0);
+ if (inputStream != null) {
+ method.setRequestEntity(new InputStreamRequestEntity(inputStream));
+ } else {
+ method.setRequestEntity(new InputStreamRequestEntity(new ByteArrayInputStream(getBytes())));
+ }
+ method.setRequestHeader("Content-type", c.getType());
+ method.setRequestHeader("Title", getTitle());
+ method.setRequestHeader("Slug", getSlug());
+ getCollection().getHttpClient().executeMethod(method);
+ if (inputStream != null) inputStream.close();
+ InputStream is = method.getResponseBodyAsStream();
+ if (method.getStatusCode() == 200 || method.getStatusCode() == 201) {
+ Entry romeEntry = Atom10Parser.parseEntry(
+ new InputStreamReader(is), col.getHrefResolved());
+ BeanUtils.copyProperties(this, romeEntry);
+
+ } else {
+ throw new ProponoException(
+ "ERROR HTTP status-code=" + method.getStatusCode()
+ + " status-line: " + method.getStatusLine());
+ }
+ } catch (IOException ie) {
+ throw new ProponoException("ERROR: saving media entry", ie);
+ } catch (JDOMException je) {
+ throw new ProponoException("ERROR: saving media entry", je);
+ } catch (FeedException fe) {
+ throw new ProponoException("ERROR: saving media entry", fe);
+ } catch (IllegalAccessException ae) {
+ throw new ProponoException("ERROR: saving media entry", ae);
+ } catch (InvocationTargetException te) {
+ throw new ProponoException("ERROR: saving media entry", te);
+ }
+ Header locationHeader = method.getResponseHeader("Location");
+ if (locationHeader == null) {
+ logger.warn("WARNING added entry, but no location header returned");
+ } else if (getEditURI() == null) {
+ List links = getOtherLinks();
+ Link link = new Link();
+ link.setHref(locationHeader.getValue());
+ link.setRel("edit");
+ links.add(link);
+ setOtherLinks(links);
+ }
+ }
+
+ /** Set string to be used in file name of new media resource on server. */
+ public String getSlug() {
+ return slug;
+ }
+
+ /** Get string to be used in file name of new media resource on server. */
+ public void setSlug(String slug) {
+ this.slug = slug;
+ }
+
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/sun/syndication/propono/atom/client/ClientWorkspace.java b/src/main/java/com/sun/syndication/propono/atom/client/ClientWorkspace.java
new file mode 100644
index 0000000..6377662
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/client/ClientWorkspace.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.client;
+
+import com.sun.syndication.propono.atom.common.AtomService;
+import com.sun.syndication.propono.atom.common.Workspace;
+import com.sun.syndication.propono.atom.common.Workspace;
+import com.sun.syndication.propono.utils.ProponoException;
+import java.util.Iterator;
+import java.util.List;
+import org.jdom.Element;
+
+
+/**
+ * Represents Atom protocol workspace on client-side.
+ * It extends the common
+ * {@link com.sun.syndication.propono.atom.common.Workspace}
+ * to return
+ * {@link com.sun.syndication.propono.atom.client.ClientCollection}
+ * objects instead of common
+ * {@link com.sun.syndication.propono.atom.common.Collection}s.
+ */
+public class ClientWorkspace extends Workspace {
+ private ClientAtomService atomService = null;
+
+ ClientWorkspace(Element e, ClientAtomService atomService, String baseURI) throws ProponoException {
+ super("dummy", "dummy");
+ this.atomService = atomService;
+ parseWorkspaceElement(e, baseURI);
+ }
+
+ /**
+ * Package access to parent service.
+ */
+ ClientAtomService getAtomService() {
+ return atomService;
+ }
+
+ /** Deserialize a Atom workspace XML element into an object */
+ protected void parseWorkspaceElement(Element element, String baseURI) throws ProponoException {
+ Element titleElem = element.getChild("title", AtomService.ATOM_FORMAT);
+ setTitle(titleElem.getText());
+ if (titleElem.getAttribute("type", AtomService.ATOM_FORMAT) != null) {
+ setTitleType(titleElem.getAttribute("type", AtomService.ATOM_FORMAT).getValue());
+ }
+ List collections = element.getChildren("collection", AtomService.ATOM_PROTOCOL);
+ Iterator iter = collections.iterator();
+ while (iter.hasNext()) {
+ Element e = (Element) iter.next();
+ addCollection(new ClientCollection(e, this, baseURI));
+ }
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/client/EntryIterator.java b/src/main/java/com/sun/syndication/propono/atom/client/EntryIterator.java
new file mode 100644
index 0000000..5014d8f
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/client/EntryIterator.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.client;
+
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.feed.atom.Feed;
+import com.sun.syndication.feed.atom.Link;
+import com.sun.syndication.io.WireFeedInput;
+import com.sun.syndication.propono.utils.ProponoException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jdom.Document;
+import org.jdom.input.SAXBuilder;
+
+
+/**
+ * Enables iteration over entries in Atom protocol collection.
+ */
+public class EntryIterator implements Iterator {
+ static final Log logger = LogFactory.getLog(EntryIterator.class);
+ private final ClientCollection collection;
+
+ int maxEntries = 20;
+ int offset = 0;
+ Iterator members = null;
+ Feed col = null;
+ String collectionURI;
+ String nextURI;
+
+ EntryIterator(ClientCollection collection) throws ProponoException {
+ this.collection = collection;
+ collectionURI = collection.getHrefResolved();
+ nextURI = collectionURI;
+ getNextEntries();
+ }
+
+ /**
+ * Returns true if more entries are available.
+ */
+ public boolean hasNext() {
+ if (!members.hasNext()) {
+ try {
+ getNextEntries();
+ } catch (Exception ignored) {
+ logger.error("ERROR getting next entries", ignored);
+ }
+ }
+ return members.hasNext();
+ }
+
+ /**
+ * Get next entry in collection.
+ */
+ public Object next() {
+ if (hasNext()) {
+ Entry romeEntry = (Entry)members.next();
+ try {
+ if (!romeEntry.isMediaEntry()) {
+ return new ClientEntry(null, collection, romeEntry, true);
+ } else {
+ return new ClientMediaEntry(null, collection, romeEntry, true);
+ }
+ } catch (ProponoException e) {
+ throw new RuntimeException("Unexpected exception creating ClientEntry or ClientMedia", e);
+ }
+ }
+ throw new NoSuchElementException();
+ }
+
+ /**
+ * Remove entry is not implemented.
+ */
+ public void remove() {
+ // optional method, not implemented
+ }
+
+ private void getNextEntries() throws ProponoException {
+ if (nextURI == null) return;
+ GetMethod colGet = new GetMethod( collection.getHrefResolved(nextURI) );
+ collection.addAuthentication(colGet);
+ try {
+ collection.getHttpClient().executeMethod(colGet);
+ SAXBuilder builder = new SAXBuilder();
+ Document doc = builder.build(colGet.getResponseBodyAsStream());
+ WireFeedInput feedInput = new WireFeedInput();
+ col = (Feed) feedInput.build(doc);
+ } catch (Exception e) {
+ throw new ProponoException("ERROR: fetching or parsing next entries, HTTP code: " + (colGet != null ? colGet.getStatusCode() : -1), e);
+ } finally {
+ colGet.releaseConnection();
+ }
+ members = col.getEntries().iterator();
+ offset += col.getEntries().size();
+
+ nextURI = null;
+ List altLinks = col.getOtherLinks();
+ if (altLinks != null) {
+ Iterator iter = altLinks.iterator();
+ while (iter.hasNext()) {
+ Link link = (Link)iter.next();
+ if ("next".equals(link.getRel())) {
+ nextURI = link.getHref();
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/client/GDataAuthStrategy.java b/src/main/java/com/sun/syndication/propono/atom/client/GDataAuthStrategy.java
new file mode 100644
index 0000000..987db9f
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/client/GDataAuthStrategy.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2009 Dave Johnson (Blogapps project)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.client;
+
+import com.sun.syndication.propono.utils.ProponoException;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+import org.apache.commons.httpclient.NameValuePair;
+import org.apache.commons.httpclient.methods.PostMethod;
+
+
+public class GDataAuthStrategy implements AuthStrategy {
+ private String email;
+ private String password;
+ private String service;
+ private String authToken;
+
+ public GDataAuthStrategy(String email, String password, String service) throws ProponoException {
+ this.email = email;
+ this.password = password;
+ this.service = service;
+ init();
+ }
+
+ private void init() throws ProponoException {
+ try {
+ HttpClient httpClient = new HttpClient();
+ PostMethod method = new PostMethod("https://www.google.com/accounts/ClientLogin");
+ NameValuePair[] data = {
+ new NameValuePair("Email", email),
+ new NameValuePair("Passwd", password),
+ new NameValuePair("accountType", "GOOGLE"),
+ new NameValuePair("service", service),
+ new NameValuePair("source", "ROME Propono Atompub Client")
+ };
+ method.setRequestBody(data);
+ httpClient.executeMethod(method);
+
+ String responseBody = method.getResponseBodyAsString();
+ int authIndex = responseBody.indexOf("Auth=");
+
+ authToken = "GoogleLogin auth=" + responseBody.trim().substring(authIndex + 5);
+
+ } catch (Throwable t) {
+ t.printStackTrace();
+ throw new ProponoException("ERROR obtaining Google authentication string", t);
+ }
+ }
+
+ public void addAuthentication(HttpClient httpClient, HttpMethodBase method) throws ProponoException {
+ httpClient.getParams().setAuthenticationPreemptive(true);
+ method.setRequestHeader("Authorization", authToken);
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/client/NoAuthStrategy.java b/src/main/java/com/sun/syndication/propono/atom/client/NoAuthStrategy.java
new file mode 100644
index 0000000..68fb129
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/client/NoAuthStrategy.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2009 Dave Johnson (Blogapps project)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.client;
+
+import com.sun.syndication.propono.utils.ProponoException;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+
+/**
+ * No authentication
+ */
+public class NoAuthStrategy implements AuthStrategy {
+
+ public void addAuthentication(HttpClient httpClient, HttpMethodBase method) throws ProponoException {
+ // no-op
+ }
+
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/client/OAuthStrategy.java b/src/main/java/com/sun/syndication/propono/atom/client/OAuthStrategy.java
new file mode 100644
index 0000000..56f3b0c
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/client/OAuthStrategy.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2009 Dave Johnson (Blogapps project)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.client;
+
+import com.sun.syndication.propono.utils.ProponoException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import net.oauth.OAuth;
+import net.oauth.OAuthAccessor;
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthMessage;
+import net.oauth.OAuthServiceProvider;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+import org.apache.commons.httpclient.NameValuePair;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.util.ParameterParser;
+
+
+/**
+ * Strategy for using OAuth.
+ */
+public class OAuthStrategy implements AuthStrategy {
+
+ private State state = State.UNAUTHORIZED;
+
+ private enum State {
+ UNAUTHORIZED, // have not sent any requests
+ REQUEST_TOKEN, // have a request token
+ AUTHORIZED, // are authorized
+ ACCESS_TOKEN // have access token, ready to make calls
+ }
+
+ private String username;
+ private String consumerKey;
+ private String consumerSecret;
+ private String keyType;
+
+ private String reqUrl;
+ private String authzUrl;
+ private String accessUrl;
+
+ private String nonce;
+ private long timestamp;
+
+ private String requestToken = null;
+ private String accessToken = null;
+ private String tokenSecret = null;
+
+ /**
+ * Create OAuth authentcation strategy and negotiate with services to
+ * obtain access token to be used in subsequent calls.
+ *
+ * @param username Username to be used in authentication
+ * @param key Consumer key
+ * @param secret Consumer secret
+ * @param keyType Key type (e.g. "HMAC-SHA1")
+ * @param reqUrl URL of request token service
+ * @param authzUrl URL of authorize service
+ * @param accessUrl URL of acess token service
+ * @throws ProponoException on any sort of initialization error
+ */
+ public OAuthStrategy(
+ String username, String key, String secret, String keyType,
+ String reqUrl, String authzUrl, String accessUrl) throws ProponoException {
+
+ this.username = username;
+ this.reqUrl = reqUrl;
+ this.authzUrl = authzUrl;
+ this.accessUrl = accessUrl;
+ this.consumerKey = key;
+ this.consumerSecret = secret;
+ this.keyType = keyType;
+
+ this.nonce = UUID.randomUUID().toString();
+ this.timestamp = (long)(new Date().getTime()/1000L);
+
+ init();
+ }
+
+ private void init() throws ProponoException {
+ callOAuthUri(reqUrl);
+ callOAuthUri(authzUrl);
+ callOAuthUri(accessUrl);
+ }
+
+ public void addAuthentication(HttpClient httpClient, HttpMethodBase method) throws ProponoException {
+
+ if (state != State.ACCESS_TOKEN) {
+ throw new ProponoException("ERROR: authentication strategy failed init");
+ }
+
+ // add OAuth name/values to request query string
+
+ // wish we didn't have to parse them apart first, ugh
+ List originalqlist = null;
+ if (method.getQueryString() != null) {
+ String qstring = method.getQueryString().trim();
+ qstring = qstring.startsWith("?") ? qstring.substring(1) : qstring;
+ originalqlist = new ParameterParser().parse(qstring, '&');
+ } else {
+ originalqlist = new ArrayList();
+ }
+
+ // put query string into hashmap form to please OAuth.net classes
+ Map params = new HashMap();
+ for (Iterator it = originalqlist.iterator(); it.hasNext();) {
+ NameValuePair pair = (NameValuePair)it.next();
+ params.put(pair.getName(), pair.getValue());
+ }
+
+ // add OAuth params to query string
+ params.put("xoauth_requestor_id", username);
+ params.put("oauth_consumer_key", consumerKey);
+ params.put("oauth_signature_method", keyType);
+ params.put("oauth_timestamp", Long.toString(timestamp));
+ params.put("oauth_nonce", nonce);
+ params.put("oauth_token", accessToken);
+ params.put("oauth_token_secret", tokenSecret);
+
+ // sign complete URI
+ String finalUri = null;
+ OAuthServiceProvider provider =
+ new OAuthServiceProvider(reqUrl, authzUrl, accessUrl);
+ OAuthConsumer consumer =
+ new OAuthConsumer(null, consumerKey, consumerSecret, provider);
+ OAuthAccessor accessor = new OAuthAccessor(consumer);
+ accessor.tokenSecret = tokenSecret;
+ OAuthMessage message;
+ try {
+ message = new OAuthMessage(
+ method.getName(), method.getURI().toString(), params.entrySet());
+ message.sign(accessor);
+
+ finalUri = OAuth.addParameters(message.URL, message.getParameters());
+
+ } catch (Exception ex) {
+ throw new ProponoException("ERROR: OAuth signing request", ex);
+ }
+
+ // pull query string off and put it back onto method
+ method.setQueryString(finalUri.substring(finalUri.lastIndexOf("?")));
+ }
+
+
+ private void callOAuthUri(String uri) throws ProponoException {
+
+ final HttpClient httpClient = new HttpClient();
+
+ final HttpMethodBase method;
+ final String content;
+
+ Map params = new HashMap();
+ if (params == null) {
+ 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");
+
+ OAuthServiceProvider provider =
+ new OAuthServiceProvider(reqUrl, authzUrl, accessUrl);
+ OAuthConsumer consumer =
+ new OAuthConsumer(null, consumerKey, consumerSecret, provider);
+ OAuthAccessor accessor = new OAuthAccessor(consumer);
+
+ if (state == State.UNAUTHORIZED) {
+
+ try {
+ OAuthMessage message = new OAuthMessage("GET", uri, params.entrySet());
+ message.sign(accessor);
+
+ String finalUri = OAuth.addParameters(message.URL, message.getParameters());
+ method = new GetMethod(finalUri);
+ httpClient.executeMethod(method);
+ content = method.getResponseBodyAsString();
+
+ } catch (Exception e) {
+ throw new ProponoException("ERROR fetching request token", e);
+ }
+
+ } else if (state == State.REQUEST_TOKEN) {
+
+ try {
+ params.put("oauth_token", requestToken);
+ params.put("oauth_token_secret", tokenSecret);
+ accessor.tokenSecret = tokenSecret;
+
+ OAuthMessage message = new OAuthMessage("POST", uri, params.entrySet());
+ message.sign(accessor);
+
+ String finalUri = OAuth.addParameters(message.URL, message.getParameters());
+ method = new PostMethod(finalUri);
+ httpClient.executeMethod(method);
+ content = method.getResponseBodyAsString();
+
+ } catch (Exception e) {
+ throw new ProponoException("ERROR fetching request token", e);
+ }
+
+ } else if (state == State.AUTHORIZED) {
+
+ try {
+ params.put("oauth_token", accessToken);
+ params.put("oauth_token_secret", tokenSecret);
+ accessor.tokenSecret = tokenSecret;
+
+ OAuthMessage message = new OAuthMessage("GET", uri, params.entrySet());
+ message.sign(accessor);
+
+ String finalUri = OAuth.addParameters(message.URL, message.getParameters());
+ method = new GetMethod(finalUri);
+ httpClient.executeMethod(method);
+ content = method.getResponseBodyAsString();
+
+ } catch (Exception e) {
+ throw new ProponoException("ERROR fetching request token", e);
+ }
+
+ } else {
+ method = null;
+ content = null;
+ return;
+ }
+
+
+ String token = null;
+ String secret = null;
+
+ if (content != null) {
+ String[] settings = content.split("&");
+ for (int i=0; i 1) {
+ if ("oauth_token".equals(setting[0])) {
+ token = setting[1];
+ } else if ("oauth_token_secret".equals(setting[0])) {
+ secret = setting[1];
+ }
+ }
+ }
+ }
+
+ switch (state) {
+
+ case UNAUTHORIZED:
+ if (token != null && secret != null) {
+ requestToken = token;
+ tokenSecret = secret;
+ state = State.REQUEST_TOKEN;
+ } else {
+ throw new ProponoException("ERROR: requestToken or tokenSecret is null");
+ }
+ break;
+
+ case REQUEST_TOKEN:
+ if (method.getStatusCode() == 200) {
+ state = State.AUTHORIZED;
+ } else {
+ throw new ProponoException("ERROR: authorization returned code: " + method.getStatusCode());
+ }
+ break;
+
+ case AUTHORIZED:
+ if (token != null && secret != null) {
+ accessToken = token;
+ tokenSecret = secret;
+ state = State.ACCESS_TOKEN;
+ } else {
+ throw new ProponoException("ERROR: accessToken or tokenSecret is null");
+ }
+ break;
+ }
+ }
+}
+
+
diff --git a/src/main/java/com/sun/syndication/propono/atom/client/atomclient-diagram.gif b/src/main/java/com/sun/syndication/propono/atom/client/atomclient-diagram.gif
new file mode 100644
index 0000000..21b1094
Binary files /dev/null and b/src/main/java/com/sun/syndication/propono/atom/client/atomclient-diagram.gif differ
diff --git a/src/main/java/com/sun/syndication/propono/atom/common/AtomService.java b/src/main/java/com/sun/syndication/propono/atom/common/AtomService.java
new file mode 100644
index 0000000..53cf81d
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/common/AtomService.java
@@ -0,0 +1,122 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package com.sun.syndication.propono.atom.common;
+
+
+import com.sun.syndication.propono.utils.ProponoException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.Namespace;
+
+/**
+ * Models an Atom Publishing Protocol Service Document.
+ * Is able to read a Service document from a JDOM Document
+ * and to write Service document out as a JDOM Document.
+ */
+public class AtomService {
+
+ private List workspaces = new ArrayList();
+
+ /** Namespace for Atom Syndication Format */
+ public static Namespace ATOM_FORMAT =
+ Namespace.getNamespace("atom","http://www.w3.org/2005/Atom");
+
+ /** Namespace for Atom Publishing Protocol */
+ public static Namespace ATOM_PROTOCOL =
+ Namespace.getNamespace("app","http://www.w3.org/2007/app");
+
+ /**
+ * Create new and empty Atom service
+ */
+ public AtomService() {
+ }
+
+ /**
+ * Add Workspace to service.
+ */
+ public void addWorkspace(Workspace workspace) {
+ workspaces.add(workspace);
+ }
+
+ /**
+ * Get Workspaces available from service.
+ */
+ public List getWorkspaces() {
+ return workspaces;
+ }
+
+ /**
+ * Set Workspaces of service.
+ */
+ public void setWorkspaces(List workspaces) {
+ this.workspaces = workspaces;
+ }
+
+ /**
+ * Find workspace by title.
+ * @param title Match this title
+ * @return Matching Workspace or null if none found.
+ */
+ public Workspace findWorkspace(String title) {
+ for (Iterator it = workspaces.iterator(); it.hasNext();) {
+ Workspace ws = (Workspace) it.next();
+ if (title.equals(ws.getTitle())) {
+ return ws;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Deserialize an Atom service XML document into an object
+ */
+ public static AtomService documentToService(Document document) throws ProponoException {
+ AtomService service = new AtomService();
+ Element root = document.getRootElement();
+ List spaces = root.getChildren("workspace", ATOM_PROTOCOL);
+ Iterator iter = spaces.iterator();
+ while (iter.hasNext()) {
+ Element e = (Element) iter.next();
+ service.addWorkspace(Workspace.elementToWorkspace(e));
+ }
+ return service;
+ }
+
+ /**
+ * Serialize an AtomService object into an XML document
+ */
+ public Document serviceToDocument() {
+ AtomService service = this;
+
+ Document doc = new Document();
+ Element root = new Element("service", ATOM_PROTOCOL);
+ doc.setRootElement(root);
+ Iterator iter = service.getWorkspaces().iterator();
+ while (iter.hasNext()) {
+ Workspace space = (Workspace) iter.next();
+ root.addContent(space.workspaceToElement());
+ }
+ return doc;
+ }
+
+}
+
diff --git a/src/main/java/com/sun/syndication/propono/atom/common/Categories.java b/src/main/java/com/sun/syndication/propono/atom/common/Categories.java
new file mode 100644
index 0000000..3b0842b
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/common/Categories.java
@@ -0,0 +1,155 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package com.sun.syndication.propono.atom.common;
+
+import com.sun.syndication.feed.atom.Category;
+import com.sun.syndication.io.impl.Atom10Parser;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.jdom.Element;
+
+
+/**
+ * Models an Atom protocol Categories element, which may contain ROME Atom
+ * {@link com.sun.syndication.feed.atom.Category} elements.
+ */
+public class Categories {
+ private List categories = new ArrayList(); // of Category objects
+ private String baseURI = null;
+ private Element categoriesElement = null;
+ private String href = null;
+ private String scheme = null;
+ private boolean fixed = false;
+
+ public Categories() {
+ }
+
+ /** Load select from XML element */
+ public Categories(Element e, String baseURI) {
+ this.categoriesElement = e;
+ this.baseURI = baseURI;
+ parseCategoriesElement(e);
+ }
+
+ /** Add category list of those specified */
+ public void addCategory(Category cat) {
+ categories.add(cat);
+ }
+
+ /**
+ * Iterate over Category objects
+ * @return List of ROME Atom {@link com.sun.syndication.feed.atom.Category}
+ */
+ public List getCategories() {
+ return categories;
+ }
+
+ /** True if clients MUST use one of the categories specified */
+ public boolean isFixed() {
+ return fixed;
+ }
+
+ /** True if clients MUST use one of the categories specified */
+ public void setFixed(boolean fixed) {
+ this.fixed = fixed;
+ }
+
+ /** Category URI scheme to use for Categories without a scheme */
+ public String getScheme() {
+ return scheme;
+ }
+
+ /** Category URI scheme to use for Categories without a scheme */
+ public void setScheme(String scheme) {
+ this.scheme = scheme;
+ }
+
+ /** URI of out-of-line categories */
+ public String getHref() {
+ return href;
+ }
+
+ /** URI of out-of-line categories */
+ public void setHref(String href) {
+ this.href = href;
+ }
+
+ /** Get unresolved URI of the collection, or null if impossible to determine */
+ public String getHrefResolved() {
+ if (Atom10Parser.isAbsoluteURI(href)) {
+ return href;
+ } else if (baseURI != null && categoriesElement != null) {
+ return Atom10Parser.resolveURI(
+ baseURI, categoriesElement, href);
+ }
+ return null;
+ }
+
+ public Element categoriesToElement() {
+ Categories cats = this;
+ Element catsElem = new Element("categories", AtomService.ATOM_PROTOCOL);
+ catsElem.setAttribute("fixed", cats.isFixed() ? "yes" : "no", AtomService.ATOM_PROTOCOL);
+ if (cats.getScheme() != null) {
+ catsElem.setAttribute("scheme", cats.getScheme(), AtomService.ATOM_PROTOCOL);
+ }
+ if (cats.getHref() != null) {
+ catsElem.setAttribute("href", cats.getHref(), AtomService.ATOM_PROTOCOL);
+ } else {
+ // Loop to create elements
+ for (Iterator catIter = cats.getCategories().iterator(); catIter.hasNext();) {
+ Category cat = (Category) catIter.next();
+ Element catElem = new Element("category", AtomService.ATOM_FORMAT);
+ catElem.setAttribute("term", cat.getTerm(), AtomService.ATOM_FORMAT);
+ if (cat.getScheme() != null) { // optional
+ catElem.setAttribute("scheme", cat.getScheme(), AtomService.ATOM_FORMAT);
+ }
+ if (cat.getLabel() != null) { // optional
+ catElem.setAttribute("label", cat.getLabel(), AtomService.ATOM_FORMAT);
+ }
+ catsElem.addContent(catElem);
+ }
+ }
+ return catsElem;
+ }
+
+ protected void parseCategoriesElement(Element catsElem) {
+ if (catsElem.getAttribute("href", AtomService.ATOM_PROTOCOL) != null) {
+ setHref(catsElem.getAttribute("href", AtomService.ATOM_PROTOCOL).getValue());
+ }
+ if (catsElem.getAttribute("fixed", AtomService.ATOM_PROTOCOL) != null) {
+ if ("yes".equals(catsElem.getAttribute("fixed", AtomService.ATOM_PROTOCOL).getValue())) {
+ setFixed(true);
+ }
+ }
+ if (catsElem.getAttribute("scheme", AtomService.ATOM_PROTOCOL) != null) {
+ setScheme(catsElem.getAttribute("scheme", AtomService.ATOM_PROTOCOL).getValue());
+ }
+ // Loop to parse elemenents to Category objects
+ List catElems = catsElem.getChildren("category", AtomService.ATOM_FORMAT);
+ for (Iterator catIter = catElems.iterator(); catIter.hasNext();) {
+ Element catElem = (Element) catIter.next();
+ Category cat = new Category();
+ cat.setTerm(catElem.getAttributeValue("term", AtomService.ATOM_FORMAT));
+ cat.setLabel(catElem.getAttributeValue("label", AtomService.ATOM_FORMAT));
+ cat.setScheme(catElem.getAttributeValue("scheme", AtomService.ATOM_FORMAT));
+ addCategory(cat);
+ }
+ }
+}
+
diff --git a/src/main/java/com/sun/syndication/propono/atom/common/Collection.java b/src/main/java/com/sun/syndication/propono/atom/common/Collection.java
new file mode 100644
index 0000000..0e4f5e7
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/common/Collection.java
@@ -0,0 +1,252 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package com.sun.syndication.propono.atom.common;
+
+import com.sun.syndication.io.impl.Atom10Parser;
+import com.sun.syndication.propono.utils.ProponoException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.jdom.Element;
+
+
+/**
+ * Models an Atom workspace collection.
+ */
+public class Collection {
+
+ public static final String ENTRY_TYPE = "application/atom+xml;type=entry";
+
+ private Element collectionElement = null;
+ private String baseURI = null;
+ private String title = null;
+ private String titleType = null; // may be TEXT, HTML, XHTML
+ private List accepts = new ArrayList(); // of Strings
+ private String listTemplate = null;
+ private String href = null;
+ private List categories = new ArrayList(); // of Categories objects
+
+ /**
+ * Collection MUST have title and href.
+ * @param title Title for collection
+ * @param titleType Content type of title (null for plain text)
+ * @param href Collection URI.
+ */
+ public Collection(String title, String titleType, String href) {
+ this.title = title;
+ this.titleType = titleType;
+ this.href = href;
+ }
+
+ /** Load self from XML element */
+ public Collection(Element e) throws ProponoException {
+ this.collectionElement = e;
+ this.parseCollectionElement(e);
+ }
+
+ /** Load self from XML element and base URI for resolving relative URIs */
+ public Collection(Element e, String baseURI) throws ProponoException {
+ this.collectionElement = e;
+ this.baseURI = baseURI;
+ this.parseCollectionElement(e);
+ }
+
+ /**
+ * List of content-type ranges accepted by collection.
+ */
+ public List getAccepts() {
+ return accepts;
+ }
+
+ public void addAccept(String accept) {
+ this.accepts.add(accept);
+ }
+
+ public void setAccepts(List accepts) {
+ this.accepts = accepts;
+ }
+
+ /** The URI of the collection */
+ public String getHref() {
+ return href;
+ }
+
+ /**
+ * Set URI of collection
+ */
+ public void setHref(String href) {
+ this.href = href;
+ }
+
+ /** Get resolved URI of the collection, or null if impossible to determine */
+ public String getHrefResolved() {
+ if (Atom10Parser.isAbsoluteURI(href)) {
+ return href;
+ } else if (baseURI != null && collectionElement != null) {
+ int lastslash = baseURI.lastIndexOf("/");
+ return Atom10Parser.resolveURI(baseURI.substring(0, lastslash), collectionElement, href);
+ }
+ return null;
+ }
+
+ /** Get resolved URI using collection's baseURI, or null if impossible to determine */
+ public String getHrefResolved(String relativeUri) {
+ if (Atom10Parser.isAbsoluteURI(relativeUri)) {
+ return relativeUri;
+ } else if (baseURI != null && collectionElement != null) {
+ int lastslash = baseURI.lastIndexOf("/");
+ return Atom10Parser.resolveURI(baseURI.substring(0, lastslash), collectionElement, relativeUri);
+ }
+ return null;
+ }
+
+ /** Must have human readable title */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Set title of collection.
+ */
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ /**
+ * Type of title ("text", "html" or "xhtml")
+ */
+ public String getTitleType() {
+ return titleType;
+ }
+
+ /**
+ * Type of title ("text", "html" or "xhtml")
+ */
+ public void setTitleType(String titleType) {
+ this.titleType = titleType;
+ }
+
+ /** Workspace can have multiple Categories objects */
+ public void addCategories(Categories cats) {
+ categories.add(cats);
+ }
+
+ /**
+ * Get categories allowed by collection.
+ * @return Collection of {@link com.sun.syndication.propono.atom.common.Categories} objects.
+ */
+ public List getCategories() {
+ return categories;
+ }
+
+ /**
+ * Returns true if contentType is accepted by collection.
+ */
+ public boolean accepts(String ct) {
+ for (Iterator it = accepts.iterator(); it.hasNext();) {
+ String accept = (String)it.next();
+ if (accept != null && accept.trim().equals("*/*")) return true;
+ String entryType = "application/atom+xml";
+ boolean entry = entryType.equals(ct);
+ if (entry && null == accept) {
+ return true;
+ } else if (entry && "entry".equals(accept)) {
+ return true;
+ } else if (entry && entryType.equals(accept)) {
+ return true;
+ } else {
+ String[] rules = (String[])accepts.toArray(new String[accepts.size()]);
+ for (int i=0; i 0) {
+ rule = rule.substring(0, slashstar + 1);
+ if (ct.startsWith(rule)) return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Serialize an AtomService.Collection into an XML element
+ */
+ public Element collectionToElement() {
+ Collection collection = this;
+ Element element = new Element("collection", AtomService.ATOM_PROTOCOL);
+ element.setAttribute("href", collection.getHref());
+
+ Element titleElem = new Element("title", AtomService.ATOM_FORMAT);
+ titleElem.setText(collection.getTitle());
+ if (collection.getTitleType() != null && !collection.getTitleType().equals("TEXT")) {
+ titleElem.setAttribute("type", collection.getTitleType(), AtomService.ATOM_FORMAT);
+ }
+ element.addContent(titleElem);
+
+ // Loop to create elements
+ for (Iterator it = collection.getCategories().iterator(); it.hasNext();) {
+ Categories cats = (Categories)it.next();
+ element.addContent(cats.categoriesToElement());
+ }
+
+ for (Iterator it = collection.getAccepts().iterator(); it.hasNext();) {
+ String range = (String)it.next();
+ Element acceptElem = new Element("accept", AtomService.ATOM_PROTOCOL);
+ acceptElem.setText(range);
+ element.addContent(acceptElem);
+ }
+
+ return element;
+ }
+
+ /** Deserialize an Atom service collection XML element into an object */
+ public Collection elementToCollection(Element element) throws ProponoException {
+ return new Collection(element);
+ }
+
+ protected void parseCollectionElement(Element element) throws ProponoException {
+ setHref(element.getAttribute("href").getValue());
+
+ Element titleElem = element.getChild("title", AtomService.ATOM_FORMAT);
+ if (titleElem != null) {
+ setTitle(titleElem.getText());
+ if (titleElem.getAttribute("type", AtomService.ATOM_FORMAT) != null) {
+ setTitleType(titleElem.getAttribute("type", AtomService.ATOM_FORMAT).getValue());
+ }
+ }
+
+ List acceptElems = element.getChildren("accept", AtomService.ATOM_PROTOCOL);
+ if (acceptElems != null && acceptElems.size() > 0) {
+ for (Iterator it = acceptElems.iterator(); it.hasNext();) {
+ Element acceptElem = (Element)it.next();
+ addAccept(acceptElem.getTextTrim());
+ }
+ }
+
+ // Loop to parse element to Categories objects
+ List catsElems = element.getChildren("categories", AtomService.ATOM_PROTOCOL);
+ for (Iterator catsIter = catsElems.iterator(); catsIter.hasNext();) {
+ Element catsElem = (Element) catsIter.next();
+ Categories cats = new Categories(catsElem, baseURI);
+ addCategories(cats);
+ }
+ }
+}
+
diff --git a/src/main/java/com/sun/syndication/propono/atom/common/Workspace.java b/src/main/java/com/sun/syndication/propono/atom/common/Workspace.java
new file mode 100644
index 0000000..b15dbee
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/common/Workspace.java
@@ -0,0 +1,147 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package com.sun.syndication.propono.atom.common;
+
+import com.sun.syndication.propono.utils.ProponoException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.jdom.Element;
+
+
+/**
+ * Models an Atom workspace.
+ */
+public class Workspace {
+ private String title = null;
+ private String titleType = null; // may be TEXT, HTML, XHTML
+ private List collections = new ArrayList();
+
+ /**
+ * Collection MUST have title.
+ * @param title Title for collection
+ * @param titleType Content type of title (null for plain text)
+ */
+ public Workspace(String title, String titleType) {
+ this.title = title;
+ this.titleType = titleType;
+ }
+
+ public Workspace(Element elem) throws ProponoException {
+ parseWorkspaceElement(elem);
+ }
+
+ /** Iterate over collections in workspace */
+ public List getCollections() {
+ return collections;
+ }
+
+ /** Add new collection to workspace */
+ public void addCollection(Collection col) {
+ collections.add(col);
+ }
+
+ /**
+ * DefaultWorkspace must have a human readable title
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Set title of workspace.
+ */
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ /**
+ * Get title type ("text", "html", "xhtml" or MIME content-type)
+ */
+ public String getTitleType() {
+ return titleType;
+ }
+
+ /**
+ * Set title type ("text", "html", "xhtml" or MIME content-type)
+ */
+ public void setTitleType(String titleType) {
+ this.titleType = titleType;
+ }
+
+ /**
+ * Find collection by title and/or content-type.
+ * @param title Title or null to match all titles.
+ * @param contentType Content-type or null to match all content-types.
+ * @return First Collection that matches title and/or content-type.
+ */
+ public Collection findCollection(String title, String contentType) {
+ for (Iterator it = collections.iterator(); it.hasNext();) {
+ Collection col = (Collection) it.next();
+ if (title != null && col.accepts(contentType)) {
+ return col;
+ } else if (col.accepts(contentType)) {
+ return col;
+ }
+ }
+ return null;
+ }
+
+ /** Deserialize a Atom workspace XML element into an object */
+ public static Workspace elementToWorkspace(Element element) throws ProponoException {
+ return new Workspace(element);
+ }
+
+ /**
+ * Serialize an AtomService.DefaultWorkspace object into an XML element
+ */
+ public Element workspaceToElement() {
+ Workspace space = this;
+
+ Element element = new Element("workspace", AtomService.ATOM_PROTOCOL);
+
+ Element titleElem = new Element("title", AtomService.ATOM_FORMAT);
+ titleElem.setText(space.getTitle());
+ if (space.getTitleType() != null && !space.getTitleType().equals("TEXT")) {
+ titleElem.setAttribute("type", space.getTitleType(), AtomService.ATOM_FORMAT);
+ }
+ element.addContent(titleElem);
+
+ Iterator iter = space.getCollections().iterator();
+ while (iter.hasNext()) {
+ Collection col = (Collection) iter.next();
+ element.addContent(col.collectionToElement());
+ }
+ return element;
+ }
+
+ /** Deserialize a Atom workspace XML element into an object */
+ protected void parseWorkspaceElement(Element element) throws ProponoException {
+ Element titleElem = element.getChild("title", AtomService.ATOM_FORMAT);
+ setTitle(titleElem.getText());
+ if (titleElem.getAttribute("type", AtomService.ATOM_FORMAT) != null) {
+ setTitleType(titleElem.getAttribute("type", AtomService.ATOM_FORMAT).getValue());
+ }
+ List collections = element.getChildren("collection", AtomService.ATOM_PROTOCOL);
+ Iterator iter = collections.iterator();
+ while (iter.hasNext()) {
+ Element e = (Element) iter.next();
+ addCollection(new Collection(e));
+ }
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/common/rome/AppModule.java b/src/main/java/com/sun/syndication/propono/atom/common/rome/AppModule.java
new file mode 100644
index 0000000..495ee58
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/common/rome/AppModule.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License. For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package com.sun.syndication.propono.atom.common.rome;
+
+import com.sun.syndication.feed.module.Module;
+import java.util.Date;
+
+/**
+ * ROME Extension Module to Atom protocol extensions to Atom format.
+ */
+public interface AppModule extends Module {
+ public static final String URI = "http://www.w3.org/2007/app";
+
+ /** True if entry is a draft */
+ public Boolean getDraft();
+
+ /** Set to true if entry is a draft */
+ public void setDraft(Boolean draft);
+
+ /** Time of last edit */
+ public Date getEdited();
+
+ /** Set time of last edit */
+ public void setEdited(Date edited);
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/common/rome/AppModuleGenerator.java b/src/main/java/com/sun/syndication/propono/atom/common/rome/AppModuleGenerator.java
new file mode 100644
index 0000000..c182609
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/common/rome/AppModuleGenerator.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License. For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package com.sun.syndication.propono.atom.common.rome;
+
+import com.sun.syndication.io.impl.DateParser;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.jdom.Element;
+import org.jdom.Namespace;
+
+import com.sun.syndication.feed.module.Module;
+import com.sun.syndication.io.ModuleGenerator;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Creates JDOM representation for APP Extension Module.
+ */
+public class AppModuleGenerator implements ModuleGenerator {
+ private static final Namespace APP_NS =
+ Namespace.getNamespace("app", AppModule.URI);
+
+ public String getNamespaceUri() {
+ return AppModule.URI;
+ }
+
+ private static final Set NAMESPACES;
+
+ static {
+ Set nss = new HashSet();
+ nss.add(APP_NS);
+ NAMESPACES = Collections.unmodifiableSet(nss);
+ }
+
+ /** Get namespaces associated with this module */
+ public Set getNamespaces() {
+ return NAMESPACES;
+ }
+
+ /** Generate JDOM element for module and add it to parent element */
+ public void generate(Module module, Element parent) {
+ AppModule m = (AppModule)module;
+
+ if (m.getDraft() != null) {
+ String draft = m.getDraft().booleanValue() ? "yes" : "no";
+ Element control = new Element("control", APP_NS);
+ control.addContent(generateSimpleElement("draft", draft));
+ parent.addContent(control);
+ }
+ if (m.getEdited() != null) {
+ Element edited = new Element("edited", APP_NS);
+ // Inclulde millis in date/time
+ SimpleDateFormat dateFormater = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+ dateFormater.setTimeZone(TimeZone.getTimeZone("GMT"));
+ edited.addContent(dateFormater.format(m.getEdited()));
+ parent.addContent(edited);
+ }
+ }
+
+ private Element generateSimpleElement(String name, String value) {
+ Element element = new Element(name, APP_NS);
+ element.addContent(value);
+ return element;
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/common/rome/AppModuleImpl.java b/src/main/java/com/sun/syndication/propono/atom/common/rome/AppModuleImpl.java
new file mode 100644
index 0000000..e6485a5
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/common/rome/AppModuleImpl.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ * Copyright 2011 The ROME Teams
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License. For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package com.sun.syndication.propono.atom.common.rome;
+
+import com.sun.syndication.feed.CopyFrom;
+import com.sun.syndication.feed.module.ModuleImpl;
+import java.util.Date;
+
+/**
+ * Bean representation of APP module.
+ */
+public class AppModuleImpl extends ModuleImpl implements AppModule {
+ private boolean draft = false;
+ private Date edited = null;
+
+ public AppModuleImpl() {
+ super(AppModule.class, AppModule.URI);
+ }
+
+ /** True if entry is draft */
+ public Boolean getDraft() {
+ return draft ? Boolean.TRUE : Boolean.FALSE;
+ }
+
+ /** Set to true if entry is draft */
+ public void setDraft(Boolean draft) {
+ this.draft = draft.booleanValue();
+ }
+
+ /** Time of last edit */
+ public Date getEdited() {
+ return edited;
+ }
+
+ /** Set time of last edit */
+ public void setEdited(Date edited) {
+ this.edited = edited;
+ }
+
+ /** Get interface class of module */
+ public Class getInterface() {
+ return AppModule.class;
+ }
+
+ /** Copy from other module */
+ public void copyFrom(CopyFrom obj) {
+ AppModule m = (AppModule)obj;
+ setDraft(m.getDraft());
+ setEdited(m.getEdited());
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/common/rome/AppModuleParser.java b/src/main/java/com/sun/syndication/propono/atom/common/rome/AppModuleParser.java
new file mode 100644
index 0000000..13b34c4
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/common/rome/AppModuleParser.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License. For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package com.sun.syndication.propono.atom.common.rome;
+
+import com.sun.syndication.io.impl.DateParser;
+import org.jdom.Element;
+import org.jdom.Namespace;
+
+import com.sun.syndication.feed.module.Module;
+import com.sun.syndication.io.ModuleParser;
+
+/**
+ * Parses APP module information from a JDOM element and into
+ * AppModule form.
+ */
+public class AppModuleParser implements ModuleParser {
+
+ /** Get URI of module namespace */
+ public String getNamespaceUri() {
+ return AppModule.URI;
+ }
+
+ /** Get namespace of module */
+ public Namespace getContentNamespace() {
+ return Namespace.getNamespace(AppModule.URI);
+ }
+
+ /** Parse JDOM element into module */
+ public Module parse(Element elem) {
+ boolean foundSomething = false;
+ AppModule m = new AppModuleImpl();
+ Element control = elem.getChild("control", getContentNamespace());
+ if (control != null) {
+ Element draftElem = control.getChild("draft", getContentNamespace());
+ if (draftElem != null) {
+ if ("yes".equals(draftElem.getText())) m.setDraft(Boolean.TRUE);
+ if ("no".equals(draftElem.getText())) m.setDraft(Boolean.FALSE);
+ }
+ }
+ Element edited = elem.getChild("edited", getContentNamespace());
+ if (edited != null) {
+ try {
+ m.setEdited(DateParser.parseW3CDateTime(edited.getTextTrim()));
+ } catch (Exception ignored) {}
+ }
+ return m;
+ }
+}
+
diff --git a/src/main/java/com/sun/syndication/propono/atom/server/AtomException.java b/src/main/java/com/sun/syndication/propono/atom/server/AtomException.java
new file mode 100644
index 0000000..93a8543
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/server/AtomException.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License. For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package com.sun.syndication.propono.atom.server;
+
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * Exception thrown by {@link com.sun.syndication.propono.atom.server.AtomHandler}
+ * and extended by other Propono Atom exception classes.
+ */
+public class AtomException extends Exception {
+ /** Construct new exception */
+ public AtomException() {
+ super();
+ }
+ /** Construct new exception with message */
+ public AtomException(String msg) {
+ super(msg);
+ }
+ /** Contruct new exception with message and wrapping existing exception */
+ public AtomException(String msg, Throwable t) {
+ super(msg, t);
+ }
+ /** Construct new exception to wrap existing one. */
+ public AtomException(Throwable t) {
+ super(t);
+ }
+ /* Get HTTP status code associated with exception (HTTP 500 server error) */
+ /**
+ * Get HTTP status associated with exception.
+ */
+ public int getStatus() {
+ return HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/server/AtomHandler.java b/src/main/java/com/sun/syndication/propono/atom/server/AtomHandler.java
new file mode 100644
index 0000000..8561073
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/server/AtomHandler.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License. For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package com.sun.syndication.propono.atom.server;
+
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.feed.atom.Feed;
+import com.sun.syndication.propono.atom.common.AtomService;
+import com.sun.syndication.propono.atom.common.Categories;
+
+/**
+ * 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.sun.syndication.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.sun.syndication.propono.atom.common.AtomService}
+ * object that contains the
+ * {@link com.sun.syndication.propono.atom.common.Workspace} objects
+ * available to the currently authenticated user and within those the
+ * {@link com.sun.syndication.propono.atom.common.Collection} avalaible.
+ */
+ public AtomService getAtomService(AtomRequest req) throws AtomException;
+
+ /**
+ * Get categories, a list of Categories objects
+ */
+ public Categories getCategories(AtomRequest req) throws AtomException;
+
+ /**
+ * Return collection or portion of collection specified by request.
+ * @param req Details of HTTP request
+ */
+ public Feed getCollection(AtomRequest req) throws AtomException;
+
+ /**
+ * Store new entry in collection specified by request and return
+ * representation of entry as it is stored on server.
+ * @param req Details of HTTP request
+ * @return Location URL of new entry
+ */
+ public Entry postEntry(AtomRequest req, Entry entry) throws AtomException;
+
+ /**
+ * Get entry specified by request.
+ * @param req Details of HTTP request
+ */
+ public Entry getEntry(AtomRequest req) throws AtomException;
+
+ /**
+ * Get media resource specified by request.
+ * @param req Details of HTTP request
+ */
+ public AtomMediaResource getMediaResource(AtomRequest req) throws AtomException;
+
+ /**
+ * Update entry specified by request and return new entry as represented
+ * on the server.
+ * @param req Details of HTTP request
+ */
+ public void putEntry(AtomRequest req, Entry entry) throws AtomException;
+
+
+ /**
+ * Delete entry specified by request.
+ * @param req Details of HTTP request
+ */
+ public void deleteEntry(AtomRequest req) throws AtomException;
+
+ /**
+ * Store media data in collection specified by request, create an Atom
+ * media-link entry to store metadata for the new media file and return
+ * that entry to the caller.
+ * @param req Details of HTTP request
+ * @param entry New entry initialzied with only title and content type
+ * @return Location URL of new media entry
+ */
+ public Entry postMedia(AtomRequest req, Entry entry) throws AtomException;
+
+ /**
+ * Update the media file part of a media-link entry.
+ * @param req Details of HTTP request
+ */
+ public void putMedia(AtomRequest req) throws AtomException;
+
+ /**
+ * Return true if specified request represents URI of a Service Document.
+ * @param req Details of HTTP request
+ */
+ public boolean isAtomServiceURI(AtomRequest req);
+
+ /**
+ * Return true if specified request represents URI of a Categories Document.
+ * @param req Details of HTTP request
+ */
+ public boolean isCategoriesURI(AtomRequest req);
+
+ /**
+ * Return true if specified request represents URI of a collection.
+ * @param req Details of HTTP request
+ */
+ public boolean isCollectionURI(AtomRequest req);
+
+ /**
+ * Return true if specified request represents URI of an Atom entry.
+ * @param req Details of HTTP request
+ */
+ public boolean isEntryURI(AtomRequest req);
+
+ /**
+ * Return true if specified patrequesthinfo represents media-edit URI.
+ * @param req Details of HTTP request
+ */
+ public boolean isMediaEditURI(AtomRequest req);
+}
+
diff --git a/src/main/java/com/sun/syndication/propono/atom/server/AtomHandlerFactory.java b/src/main/java/com/sun/syndication/propono/atom/server/AtomHandlerFactory.java
new file mode 100644
index 0000000..ca5fc55
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/server/AtomHandlerFactory.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.server;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Defines a factory that enables the
+ * {@link com.sun.syndication.propono.atom.server.AtomServlet} to obtain an
+ * {@link com.sun.syndication.propono.atom.server.AtomHandler} that handles an Atom request.
+ *
+ *
To create your own Atom protocol implementation you must sub-class this
+ * class with your own factory that is capable of creating instances of your
+ * {@link com.sun.syndication.propono.atom.server.AtomHandler} impementation.
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.sun.syndication.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.sun.syndication.AtomHandlerFactory
+ * in jars available to the runtime.
+ *
+ *
+ * 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 (FactoryFinder.ConfigurationError e) {
+ log.error("ERROR: finding factory", e);
+ throw new FactoryConfigurationError(e.getException(), e.getMessage());
+ }
+ }
+
+ /**
+ * Creates a new instance of a {@link com.sun.syndication.propono.atom.server.AtomHandler}
+ * using the currently configured parameters.
+ *
+ * @return A new instance of a AtomHandler.
+ *
+ * @throws AtomConfigurationException if a AtomHandler cannot be created
+ * which satisfies the configuration requested.
+ */
+ public abstract AtomHandler newAtomHandler(
+ HttpServletRequest req, HttpServletResponse res);
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/server/AtomMediaResource.java b/src/main/java/com/sun/syndication/propono/atom/server/AtomMediaResource.java
new file mode 100644
index 0000000..b9c1882
--- /dev/null
+++ b/src/main/java/com/sun/syndication/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.sun.syndication.propono.atom.server;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.Date;
+import javax.activation.FileTypeMap;
+import javax.activation.MimetypesFileTypeMap;
+
+/**
+ * Represents a media link entry.
+ */
+public class AtomMediaResource {
+ private String contentType = null;
+ private long contentLength = 0;
+ private InputStream inputStream = null;
+ private Date lastModified = null;
+ private static FileTypeMap map = null;
+
+ static {
+ // TODO: figure out why PNG is missing from Java MIME types
+ map = FileTypeMap.getDefaultFileTypeMap();
+ if (map instanceof MimetypesFileTypeMap) {
+ try {
+ ((MimetypesFileTypeMap)map).addMimeTypes("image/png png PNG");
+ } catch (Exception ignored) {}
+ }
+ }
+
+ public AtomMediaResource(File resource) throws FileNotFoundException {
+ contentType = map.getContentType(resource.getName());
+ contentLength = resource.length();
+ lastModified = new Date(resource.lastModified());
+ inputStream = new FileInputStream(resource);
+ }
+
+ public AtomMediaResource(String name, long length, Date lastModified, InputStream is)
+ throws FileNotFoundException {
+ this.contentType = map.getContentType(name);
+ this.contentLength = length;
+ this.lastModified = lastModified;
+ this.inputStream = is;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public void setContentType(String contentType) {
+ this.contentType = contentType;
+ }
+
+ public long getContentLength() {
+ return contentLength;
+ }
+
+ public void setContentLength(long contentLength) {
+ this.contentLength = contentLength;
+ }
+
+ public InputStream getInputStream() {
+ return inputStream;
+ }
+
+ public void setInputStream(InputStream inputStream) {
+ this.inputStream = inputStream;
+ }
+
+ public Date getLastModified() {
+ return lastModified;
+ }
+
+ public void setLastModified(Date lastModified) {
+ this.lastModified = lastModified;
+ }
+
+
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/server/AtomNotAuthorizedException.java b/src/main/java/com/sun/syndication/propono/atom/server/AtomNotAuthorizedException.java
new file mode 100644
index 0000000..6278b76
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/server/AtomNotAuthorizedException.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License. For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package com.sun.syndication.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 {
+ /** Construct new exception */
+ public AtomNotAuthorizedException() {
+ super();
+ }
+ /** Construct new exception with message */
+ public AtomNotAuthorizedException(String msg) {
+ super(msg);
+ }
+ /** Construct new exception with message and root cause */
+ public AtomNotAuthorizedException(String msg, Throwable t) {
+ super(msg, t);
+ }
+ /** Construct new exception to wrap root cause*/
+ public AtomNotAuthorizedException(Throwable t) {
+ super(t);
+ }
+ /** Get HTTP status code of exception (HTTP 403 unauthorized) */
+ public int getStatus() {
+ return HttpServletResponse.SC_UNAUTHORIZED;
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/server/AtomNotFoundException.java b/src/main/java/com/sun/syndication/propono/atom/server/AtomNotFoundException.java
new file mode 100644
index 0000000..ad71a68
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/server/AtomNotFoundException.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License. For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package com.sun.syndication.propono.atom.server;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Exception thrown by AtomHandler in that case a resource is not found.
+ */
+public class AtomNotFoundException extends AtomException {
+ /** Construct new exception */
+ public AtomNotFoundException() {
+ super();
+ }
+ /** Construct new exception with message */
+ public AtomNotFoundException(String msg) {
+ super(msg);
+ }
+ /** Construct new exception with message and root cause */
+ public AtomNotFoundException(String msg, Throwable t) {
+ super(msg, t);
+ }
+ /** Construct new exception with root cause */
+ public AtomNotFoundException(Throwable t) {
+ super(t);
+ }
+ /** Get HTTP status code associated with exception (404 not found) */
+ public int getStatus() {
+ return HttpServletResponse.SC_NOT_FOUND;
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/server/AtomRequest.java b/src/main/java/com/sun/syndication/propono/atom/server/AtomRequest.java
new file mode 100644
index 0000000..dc1e70d
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/server/AtomRequest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License. For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package com.sun.syndication.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/sun/syndication/propono/atom/server/AtomRequestImpl.java b/src/main/java/com/sun/syndication/propono/atom/server/AtomRequestImpl.java
new file mode 100644
index 0000000..8be7833
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/server/AtomRequestImpl.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License. For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package com.sun.syndication.propono.atom.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.Principal;
+import java.util.Enumeration;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Default request implementation.
+ */
+public class AtomRequestImpl implements AtomRequest {
+ private HttpServletRequest wrapped = null;
+
+ public AtomRequestImpl(HttpServletRequest wrapped) {
+ this.wrapped = wrapped;
+ }
+
+ public String getPathInfo() {
+ return wrapped.getPathInfo() != null ? wrapped.getPathInfo() : "";
+ }
+
+ public String getQueryString() {
+ return wrapped.getQueryString();
+ }
+
+ public String getRemoteUser() {
+ return wrapped.getRemoteUser();
+ }
+
+ public boolean isUserInRole(String arg0) {
+ return wrapped.isUserInRole(arg0);
+ }
+
+ public Principal getUserPrincipal() {
+ return wrapped.getUserPrincipal();
+ }
+
+ public String getRequestURI() {
+ return wrapped.getRequestURI();
+ }
+
+ public StringBuffer getRequestURL() {
+ return wrapped.getRequestURL();
+ }
+
+ public int getContentLength() {
+ return wrapped.getContentLength();
+ }
+
+ public String getContentType() {
+ return wrapped.getContentType();
+ }
+
+ public String getParameter(String arg0) {
+ return wrapped.getParameter(arg0);
+ }
+
+ public Enumeration getParameterNames() {
+ return wrapped.getParameterNames();
+ }
+
+ public String[] getParameterValues(String arg0) {
+ return wrapped.getParameterValues(arg0);
+ }
+
+ public Map getParameterMap() {
+ return wrapped.getParameterMap();
+ }
+
+ public InputStream getInputStream() throws IOException {
+ return wrapped.getInputStream();
+ }
+
+ public long getDateHeader(String arg0) {
+ return wrapped.getDateHeader(arg0);
+ }
+
+ public String getHeader(String arg0) {
+ return wrapped.getHeader(arg0);
+ }
+
+ public Enumeration getHeaders(String arg0) {
+ return wrapped.getHeaders(arg0);
+ }
+
+ public Enumeration getHeaderNames() {
+ return wrapped.getHeaderNames();
+ }
+
+ public int getIntHeader(String arg0) {
+ return wrapped.getIntHeader(arg0);
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/server/AtomServlet.java b/src/main/java/com/sun/syndication/propono/atom/server/AtomServlet.java
new file mode 100644
index 0000000..83815ea
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/server/AtomServlet.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright 2007 Apache Software Foundation
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. The ASF licenses this file to You
+ * under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License. For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+package com.sun.syndication.propono.atom.server;
+
+import com.sun.syndication.feed.atom.Content;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Writer;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jdom.Document;
+import org.jdom.output.Format;
+import org.jdom.output.XMLOutputter;
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.feed.atom.Feed;
+import com.sun.syndication.feed.atom.Link;
+import com.sun.syndication.io.WireFeedOutput;
+import com.sun.syndication.io.impl.Atom10Generator;
+import com.sun.syndication.io.impl.Atom10Parser;
+import com.sun.syndication.propono.atom.common.AtomService;
+import com.sun.syndication.propono.atom.common.Categories;
+import com.sun.syndication.propono.utils.Utilities;
+import java.io.BufferedReader;
+import java.util.Collections;
+import java.util.Iterator;
+import javax.servlet.ServletConfig;
+
+/**
+ * Atom Servlet implements Atom protocol by calling an
+ * {@link com.sun.syndication.propono.atom.server.AtomHandler}
+ * implementation. This servlet takes care of parsing incoming XML into ROME
+ * Atom {@link com.sun.syndication.feed.atom.Entry} objects, passing those to the handler and serializing
+ * to the response the entries and feeds returned by the handler.
+ */
+public class AtomServlet extends HttpServlet {
+
+ /**
+ * Get feed type support by Servlet, "atom_1.0"
+ */
+ public static final String FEED_TYPE = "atom_1.0";
+ private static String contextDirPath = null;
+
+ private static Log log =
+ LogFactory.getFactory().getInstance(AtomServlet.class);
+
+ static {
+ Atom10Parser.setResolveURIs(true);
+ }
+
+ //-----------------------------------------------------------------------------
+ /**
+ * Create an Atom request handler.
+ * TODO: make AtomRequestHandler implementation configurable.
+ */
+ private AtomHandler createAtomRequestHandler(
+ HttpServletRequest request, HttpServletResponse response)
+ throws ServletException {
+ AtomHandlerFactory ahf = AtomHandlerFactory.newInstance();
+ return ahf.newAtomHandler(request, response);
+ }
+
+ //-----------------------------------------------------------------------------
+ /**
+ * Handles an Atom GET by calling handler and writing results to response.
+ */
+ protected void doGet(HttpServletRequest req, HttpServletResponse res)
+ throws ServletException, IOException {
+ log.debug("Entering");
+ AtomHandler handler = createAtomRequestHandler(req, res);
+ String userName = handler.getAuthenticatedUsername();
+ if (userName != null) {
+ AtomRequest areq = new AtomRequestImpl(req);
+ try {
+ if (handler.isAtomServiceURI(areq)) {
+ // return an Atom Service document
+ AtomService service = handler.getAtomService(areq);
+ Document doc = service.serviceToDocument();
+ res.setContentType("application/atomsvc+xml; charset=utf-8");
+ Writer writer = res.getWriter();
+ XMLOutputter outputter = new XMLOutputter();
+ outputter.setFormat(Format.getPrettyFormat());
+ outputter.output(doc, writer);
+ writer.close();
+ res.setStatus(HttpServletResponse.SC_OK);
+ }
+ else if (handler.isCategoriesURI(areq)) {
+ Categories cats = handler.getCategories(areq);
+ res.setContentType("application/xml");
+ Writer writer = res.getWriter();
+ Document catsDoc = new Document();
+ catsDoc.setRootElement(cats.categoriesToElement());
+ XMLOutputter outputter = new XMLOutputter();
+ outputter.output(catsDoc, writer);
+ writer.close();
+ res.setStatus(HttpServletResponse.SC_OK);
+ }
+ else if (handler.isCollectionURI(areq)) {
+ // return a collection
+ Feed col = handler.getCollection(areq);
+ col.setFeedType(FEED_TYPE);
+ WireFeedOutput wireFeedOutput = new WireFeedOutput();
+ Document feedDoc = wireFeedOutput.outputJDom(col);
+ res.setContentType("application/atom+xml; charset=utf-8");
+ Writer writer = res.getWriter();
+ XMLOutputter outputter = new XMLOutputter();
+ outputter.setFormat(Format.getPrettyFormat());
+ outputter.output(feedDoc, writer);
+ writer.close();
+ res.setStatus(HttpServletResponse.SC_OK);
+ }
+ else if (handler.isEntryURI(areq)) {
+ // return an entry
+ Entry entry = handler.getEntry(areq);
+ if (entry != null) {
+ res.setContentType("application/atom+xml; type=entry; charset=utf-8");
+ Writer writer = res.getWriter();
+ Atom10Generator.serializeEntry(entry, writer);
+ writer.close();
+ } else {
+ res.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ }
+ }
+ else if (handler.isMediaEditURI(areq)) {
+ AtomMediaResource entry = handler.getMediaResource(areq);
+ res.setContentType(entry.getContentType());
+ res.setContentLength((int)entry.getContentLength());
+ Utilities.copyInputToOutput(entry.getInputStream(), res.getOutputStream());
+ res.getOutputStream().flush();
+ res.getOutputStream().close();
+ }
+ else {
+ res.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ }
+ } catch (AtomException ae) {
+ res.sendError(ae.getStatus(), ae.getMessage());
+ log.debug("ERROR processing GET", ae);
+ } catch (Exception e) {
+ res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+ log.debug("ERROR processing GET", e);
+ }
+ } else {
+ res.setHeader("WWW-Authenticate", "BASIC realm=\"AtomPub\"");
+ res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ }
+ log.debug("Exiting");
+ }
+
+ //-----------------------------------------------------------------------------
+ /**
+ * Handles an Atom POST by calling handler to identify URI, reading/parsing
+ * data, calling handler and writing results to response.
+ */
+ protected void doPost(HttpServletRequest req, HttpServletResponse res)
+ throws ServletException, IOException {
+ log.debug("Entering");
+ AtomHandler handler = createAtomRequestHandler(req, res);
+ String userName = handler.getAuthenticatedUsername();
+ if (userName != null) {
+ AtomRequest areq = new AtomRequestImpl(req);
+ try {
+ if (handler.isCollectionURI(areq)) {
+
+ if (req.getContentType().startsWith("application/atom+xml")) {
+
+ // parse incoming entry
+ Entry entry = Atom10Parser.parseEntry(new BufferedReader(
+ new InputStreamReader(req.getInputStream(), "UTF-8")), null);
+
+ // call handler to post it
+ Entry newEntry = handler.postEntry(areq, entry);
+
+ // set Location and Content-Location headers
+ for (Iterator it = newEntry.getOtherLinks().iterator(); it.hasNext();) {
+ Link link = (Link) it.next();
+ if ("edit".equals(link.getRel())) {
+ res.addHeader("Location", link.getHrefResolved());
+ break;
+ }
+ }
+ for (Iterator it = newEntry.getAlternateLinks().iterator(); it.hasNext();) {
+ Link link = (Link) it.next();
+ if ("alternate".equals(link.getRel())) {
+ res.addHeader("Content-Location", link.getHrefResolved());
+ break;
+ }
+ }
+
+ // write entry back out to response
+ res.setStatus(HttpServletResponse.SC_CREATED);
+ res.setContentType("application/atom+xml; type=entry; charset=utf-8");
+
+ Writer writer = res.getWriter();
+ Atom10Generator.serializeEntry(newEntry, writer);
+ writer.close();
+
+ } else if (req.getContentType() != null) {
+
+ // get incoming title and slug from HTTP header
+ String title = areq.getHeader("Title");
+
+ // create new entry for resource, set title and type
+ Entry resource = new Entry();
+ resource.setTitle(title);
+ Content content = new Content();
+ content.setType(areq.getContentType());
+ resource.setContents(Collections.singletonList(content));
+
+ // hand input stream off to hander to post file
+ Entry newEntry = handler.postMedia(areq, resource);
+
+ // set Location and Content-Location headers
+ for (Iterator it = newEntry.getOtherLinks().iterator(); it.hasNext();) {
+ Link link = (Link) it.next();
+ if ("edit".equals(link.getRel())) {
+ res.addHeader("Location", link.getHrefResolved());
+ break;
+ }
+ }
+ for (Iterator it = newEntry.getAlternateLinks().iterator(); it.hasNext();) {
+ Link link = (Link) it.next();
+ if ("alternate".equals(link.getRel())) {
+ res.addHeader("Content-Location", link.getHrefResolved());
+ break;
+ }
+ }
+
+ res.setStatus(HttpServletResponse.SC_CREATED);
+ res.setContentType("application/atom+xml; type=entry; charset=utf-8");
+
+ Writer writer = res.getWriter();
+ Atom10Generator.serializeEntry(newEntry, writer);
+ writer.close();
+
+ } else {
+ res.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE,
+ "No content-type specified in request");
+ }
+
+ } else {
+ res.sendError(HttpServletResponse.SC_NOT_FOUND,
+ "Invalid collection specified in request");
+ }
+ } catch (AtomException ae) {
+ res.sendError(ae.getStatus(), ae.getMessage());
+ log.debug("ERROR processing POST", ae);
+ } catch (Exception e) {
+ res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+ log.debug("ERROR processing POST", e);
+ }
+ } else {
+ res.setHeader("WWW-Authenticate", "BASIC realm=\"AtomPub\"");
+ res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ }
+ log.debug("Exiting");
+ }
+
+ //-----------------------------------------------------------------------------
+ /**
+ * Handles an Atom PUT by calling handler to identify URI, reading/parsing
+ * data, calling handler and writing results to response.
+ */
+ protected void doPut(HttpServletRequest req, HttpServletResponse res)
+ throws ServletException, IOException {
+ log.debug("Entering");
+ AtomHandler handler = createAtomRequestHandler(req, res);
+ String userName = handler.getAuthenticatedUsername();
+ if (userName != null) {
+ AtomRequest areq = new AtomRequestImpl(req);
+ try {
+ if (handler.isEntryURI(areq)) {
+
+ // parse incoming entry
+ Entry unsavedEntry = Atom10Parser.parseEntry(new BufferedReader(
+ new InputStreamReader(req.getInputStream(), "UTF-8")), null);
+
+ // call handler to put entry
+ handler.putEntry(areq, unsavedEntry);
+
+ res.setStatus(HttpServletResponse.SC_OK);
+
+ } else if (handler.isMediaEditURI(areq)) {
+
+ // hand input stream to handler
+ handler.putMedia(areq);
+
+ res.setStatus(HttpServletResponse.SC_OK);
+
+ } else {
+ res.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ }
+ } catch (AtomException ae) {
+ res.sendError(ae.getStatus(), ae.getMessage());
+ log.debug("ERROR processing PUT", ae);
+ } catch (Exception e) {
+ res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+ log.debug("ERROR processing PUT", e);
+ }
+ } else {
+ res.setHeader("WWW-Authenticate", "BASIC realm=\"AtomPub\"");
+ // Wanted to use sendError() here but Tomcat sends 403 forbidden
+ // when I do that, so sticking with setStatus() for time being.
+ res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ }
+ log.debug("Exiting");
+ }
+
+ //-----------------------------------------------------------------------------
+ /**
+ * Handle Atom DELETE by calling appropriate handler.
+ */
+ protected void doDelete(HttpServletRequest req, HttpServletResponse res)
+ throws ServletException, IOException {
+ log.debug("Entering");
+ AtomHandler handler = createAtomRequestHandler(req, res);
+ String userName = handler.getAuthenticatedUsername();
+ if (userName != null) {
+ AtomRequest areq = new AtomRequestImpl(req);
+ try {
+ if (handler.isEntryURI(areq)) {
+ handler.deleteEntry(areq);
+ res.setStatus(HttpServletResponse.SC_OK);
+ }
+ else {
+ res.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ }
+ } catch (AtomException ae) {
+ res.sendError(ae.getStatus(), ae.getMessage());
+ log.debug("ERROR processing DELETE", ae);
+ } catch (Exception e) {
+ res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+ log.debug("ERROR processing DELETE", e);
+ }
+ } else {
+ res.setHeader("WWW-Authenticate", "BASIC realm=\"AtomPub\"");
+ // Wanted to use sendError() here but Tomcat sends 403 forbidden
+ // when I do that, so sticking with setStatus() for time being.
+ res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ }
+ log.debug("Exiting");
+ }
+
+ /**
+ * Initialize servlet.
+ */
+ public void init( ServletConfig config ) throws ServletException {
+ super.init( config );
+ contextDirPath = getServletContext().getRealPath("/");
+
+ }
+
+ /**
+ * Get absolute path to Servlet context directory.
+ */
+ public static String getContextDirPath() {
+ return contextDirPath;
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/server/FactoryConfigurationError.java b/src/main/java/com/sun/syndication/propono/atom/server/FactoryConfigurationError.java
new file mode 100644
index 0000000..b35e0ff
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/server/FactoryConfigurationError.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.server;
+
+/**
+ * Thrown when a problem with configuration with the
+ * {@link com.sun.syndication.propono.atom.server.AtomHandlerFactory} exists.
+ * This error will typically be thrown when the class of a parser factory
+ * specified in the system properties cannot be found or instantiated.
+ */
+public class FactoryConfigurationError extends Error {
+
+ /**
+ * Exception that represents the error.
+ */
+ private Exception exception;
+
+ /**
+ * Create a new FactoryConfigurationError with no
+ * detail mesage.
+ */
+ public FactoryConfigurationError() {
+ super();
+ this.exception = null;
+ }
+
+ /**
+ * Create a new FactoryConfigurationError with
+ * the String specified as an error message.
+ *
+ * @param msg The error message for the exception.
+ */
+ public FactoryConfigurationError(String msg) {
+ super(msg);
+ this.exception = null;
+ }
+
+
+ /**
+ * Create a new FactoryConfigurationError with a
+ * given Exception base cause of the error.
+ *
+ * @param e The exception to be encapsulated in a
+ * FactoryConfigurationError.
+ */
+ public FactoryConfigurationError(Exception e) {
+ super(e.toString());
+ this.exception = e;
+ }
+
+ /**
+ * Create a new 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(Exception e, String msg) {
+ super(msg);
+ this.exception = e;
+ }
+
+
+ /**
+ * Return the message (if any) for this error . If there is no
+ * message for the exception and there is an encapsulated
+ * exception then the message of that exception, if it exists will be
+ * returned. Else the name of the encapsulated exception will be
+ * returned.
+ *
+ * @return The error message.
+ */
+ public String getMessage() {
+ String message = super.getMessage();
+
+ if (message == null && exception != null) {
+ return exception.getMessage();
+ }
+
+ return message;
+ }
+
+ /**
+ * Return the actual exception (if any) that caused this exception to
+ * be raised.
+ *
+ * @return The encapsulated exception, or null if there is none.
+ */
+ public Exception getException() {
+ return exception;
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/server/FactoryFinder.java b/src/main/java/com/sun/syndication/propono/atom/server/FactoryFinder.java
new file mode 100644
index 0000000..67fbc6e
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/server/FactoryFinder.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.server;
+
+import java.io.File;
+import java.io.FileInputStream;
+
+import java.util.Properties;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+
+/**
+ * Find {@link com.sun.syndication.propono.atom.server.AtomHandlerFactory} based on properties files.
+ */
+class FactoryFinder {
+ private static boolean debug = false;
+ static Properties cacheProps= new Properties();
+ static SecuritySupport ss = new SecuritySupport() ;
+ static boolean firstTime = true;
+
+ private static void dPrint(String msg) {
+ if (debug) {
+ System.err.println("Propono: " + msg);
+ }
+ }
+
+ /**
+ * Create an instance of a class using the specified ClassLoader and
+ * optionally fall back to the current ClassLoader if not found.
+ *
+ * @param className Name of the concrete class corresponding to the
+ * service provider
+ *
+ * @param cl ClassLoader to use to load the class, null means to use
+ * the bootstrap ClassLoader
+ *
+ * @param doFallback true if the current ClassLoader should be tried as
+ * a fallback if the class is not found using cl
+ */
+ private static Object newInstance(
+ String className, ClassLoader cl, boolean doFallback)
+ throws ConfigurationError {
+
+ try {
+ Class providerClass;
+ if (cl == null) {
+ // If classloader is null Use the bootstrap ClassLoader.
+ // Thus Class.forName(String) will use the current
+ // ClassLoader which will be the bootstrap ClassLoader.
+ providerClass = Class.forName(className);
+ } else {
+ try {
+ providerClass = cl.loadClass(className);
+ } catch (ClassNotFoundException x) {
+ if (doFallback) {
+ // Fall back to current classloader
+ cl = FactoryFinder.class.getClassLoader();
+ providerClass = cl.loadClass(className);
+ } else {
+ throw x;
+ }
+ }
+ }
+
+ Object instance = providerClass.newInstance();
+ dPrint("created new instance of " + providerClass +
+ " using ClassLoader: " + cl);
+ return instance;
+ } catch (ClassNotFoundException x) {
+ throw new ConfigurationError(
+ "Provider " + className + " not found", x);
+ } catch (Exception x) {
+ throw new ConfigurationError(
+ "Provider " + className + " could not be instantiated: " + x, x);
+ }
+ }
+
+ /**
+ * Finds the implementation Class object in the specified order. Main
+ * entry point.
+ * @return Class object of factory, never null
+ *
+ * @param factoryId Name of the factory to find, same as
+ * a property name
+ * @param fallbackClassName Implementation class name, if nothing else
+ * is found. Use null to mean no fallback.
+ *
+ * Package private so this code can be shared.
+ */
+ static Object find(String factoryId, String fallbackClassName)
+ throws ConfigurationError {
+
+ // Figure out which ClassLoader to use for loading the provider
+ // class. If there is a Context ClassLoader then use it.
+
+ ClassLoader classLoader = ss.getContextClassLoader();
+
+ if (classLoader == null) {
+ // if we have no Context ClassLoader
+ // so use the current ClassLoader
+ classLoader = FactoryFinder.class.getClassLoader();
+ }
+
+ dPrint("find factoryId =" + factoryId);
+
+ // Use the system property first
+ try {
+ String systemProp = ss.getSystemProperty(factoryId);
+ if( systemProp!=null) {
+ dPrint("found system property, value=" + systemProp);
+ return newInstance(systemProp, classLoader, true );
+ }
+ } catch (SecurityException se) {
+ //if first option fails due to any reason we should try next option in the
+ //look up algorithm.
+ }
+
+ // try to read from /propono.properties
+ try {
+ String javah = ss.getSystemProperty("java.home");
+ String configFile = "/propono.properties";
+ String factoryClassName = null;
+ if(firstTime){
+ synchronized(cacheProps){
+ if (firstTime) {
+ try {
+ InputStream is = FactoryFinder.class.getResourceAsStream(configFile);
+ firstTime = false;
+ if (is != null) {
+ dPrint("Read properties file: " + configFile);
+ cacheProps.load(is);
+ }
+ } catch (Exception intentionallyIgnored) {}
+ }
+ }
+ }
+ factoryClassName = cacheProps.getProperty(factoryId);
+
+ if(factoryClassName != null){
+ dPrint("found in $java.home/propono.properties, value=" + factoryClassName);
+ return newInstance(factoryClassName, classLoader, true);
+ }
+ } catch(Exception ex) {
+ if( debug ) ex.printStackTrace();
+ }
+
+ // Try Jar Service Provider Mechanism
+ Object provider = findJarServiceProvider(factoryId);
+ if (provider != null) {
+ return provider;
+ }
+ if (fallbackClassName == null) {
+ throw new ConfigurationError(
+ "Provider for " + factoryId + " cannot be found", null);
+ }
+
+ dPrint("loaded from fallback value: " + fallbackClassName);
+ return newInstance(fallbackClassName, classLoader, true);
+ }
+
+ /*
+ * Try to find provider using Jar Service Provider Mechanism
+ *
+ * @return instance of provider class if found or null
+ */
+ private static Object findJarServiceProvider(String factoryId)
+ throws ConfigurationError {
+
+ String serviceId = "META-INF/services/" + factoryId;
+ InputStream is = null;
+
+ // First try the Context ClassLoader
+ ClassLoader cl = ss.getContextClassLoader();
+ if (cl != null) {
+ is = ss.getResourceAsStream(cl, serviceId);
+
+ // If no provider found then try the current ClassLoader
+ if (is == null) {
+ cl = FactoryFinder.class.getClassLoader();
+ is = ss.getResourceAsStream(cl, serviceId);
+ }
+ } else {
+ // No Context ClassLoader, try the current
+ // ClassLoader
+ cl = FactoryFinder.class.getClassLoader();
+ is = ss.getResourceAsStream(cl, serviceId);
+ }
+
+ if (is == null) {
+ // No provider found
+ return null;
+ }
+
+ dPrint("found jar resource=" + serviceId +
+ " using ClassLoader: " + cl);
+
+ // Read the service provider name in UTF-8 as specified in
+ // the jar spec. Unfortunately this fails in Microsoft
+ // VJ++, which does not implement the UTF-8
+ // encoding. Theoretically, we should simply let it fail in
+ // that case, since the JVM is obviously broken if it
+ // doesn't support such a basic standard. But since there
+ // are still some users attempting to use VJ++ for
+ // development, we have dropped in a fallback which makes a
+ // second attempt using the platform's default encoding. In
+ // VJ++ this is apparently ASCII, which is a subset of
+ // UTF-8... and since the strings we'll be reading here are
+ // also primarily limited to the 7-bit ASCII range (at
+ // least, in English versions), this should work well
+ // enough to keep us on the air until we're ready to
+ // officially decommit from VJ++. [Edited comment from
+ // jkesselm]
+ BufferedReader rd;
+ try {
+ rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+ } catch (java.io.UnsupportedEncodingException e) {
+ rd = new BufferedReader(new InputStreamReader(is));
+ }
+
+ String factoryClassName = null;
+ try {
+ // XXX Does not handle all possible input as specified by the
+ // Jar Service Provider specification
+ factoryClassName = rd.readLine();
+ rd.close();
+ } catch (IOException x) {
+ // No provider found
+ return null;
+ }
+
+ if (factoryClassName != null &&
+ ! "".equals(factoryClassName)) {
+ dPrint("found in resource, value="
+ + factoryClassName);
+
+ // Note: here we do not want to fall back to the current
+ // ClassLoader because we want to avoid the case where the
+ // resource file was found using one ClassLoader and the
+ // provider class was instantiated using a different one.
+ return newInstance(factoryClassName, cl, false);
+ }
+
+ // No provider found
+ return null;
+ }
+
+ static class ConfigurationError extends Error {
+ private Exception exception;
+
+ /**
+ * Construct a new instance with the specified detail string and
+ * exception.
+ */
+ ConfigurationError(String msg, Exception x) {
+ super(msg);
+ this.exception = x;
+ }
+
+ Exception getException() {
+ return exception;
+ }
+ }
+
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/server/SecuritySupport.java b/src/main/java/com/sun/syndication/propono/atom/server/SecuritySupport.java
new file mode 100644
index 0000000..ab64b57
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/server/SecuritySupport.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.server;
+
+import java.security.*;
+import java.net.*;
+import java.io.*;
+import java.util.*;
+
+/**
+ * This class is duplicated for each subpackage, it is package private and
+ * therefore is not exposed as part of the public API.
+ */
+class SecuritySupport {
+
+ ClassLoader getContextClassLoader() {
+ return (ClassLoader)
+ AccessController.doPrivileged(new PrivilegedAction() {
+ public Object run() {
+ ClassLoader cl = null;
+ try {
+ cl = Thread.currentThread().getContextClassLoader();
+ } catch (SecurityException ex) { }
+ return cl;
+ }
+ });
+ }
+
+ String getSystemProperty(final String propName) {
+ return (String)
+ AccessController.doPrivileged(new PrivilegedAction() {
+ public Object run() {
+ return System.getProperty(propName);
+ }
+ });
+ }
+
+ FileInputStream getFileInputStream(final File file)
+ throws FileNotFoundException {
+ try {
+ return (FileInputStream)
+ AccessController.doPrivileged(new PrivilegedExceptionAction() {
+ public Object run() throws FileNotFoundException {
+ return new FileInputStream(file);
+ }
+ });
+ } catch (PrivilegedActionException e) {
+ throw (FileNotFoundException)e.getException();
+ }
+ }
+
+ InputStream getResourceAsStream(final ClassLoader cl,
+ final String name) {
+ return (InputStream)
+ AccessController.doPrivileged(new PrivilegedAction() {
+ public Object run() {
+ InputStream ris;
+ if (cl == null) {
+ ris = ClassLoader.getSystemResourceAsStream(name);
+ } else {
+ ris = cl.getResourceAsStream(name);
+ }
+ return ris;
+ }
+ });
+ }
+
+ boolean doesFileExist(final File f) {
+ return ((Boolean)
+ AccessController.doPrivileged(new PrivilegedAction() {
+ public Object run() {
+ return new Boolean(f.exists());
+ }
+ })).booleanValue();
+ }
+
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/server/impl/FileBasedAtomHandler.java b/src/main/java/com/sun/syndication/propono/atom/server/impl/FileBasedAtomHandler.java
new file mode 100644
index 0000000..018b8b2
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/server/impl/FileBasedAtomHandler.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.server.impl;
+
+import com.sun.syndication.propono.atom.server.AtomMediaResource;
+import org.apache.commons.codec.binary.Base64;
+import com.sun.syndication.propono.atom.server.AtomHandler;
+import com.sun.syndication.propono.atom.server.AtomException;
+import com.sun.syndication.propono.atom.server.AtomServlet;
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.feed.atom.Feed;
+import com.sun.syndication.propono.atom.common.AtomService;
+import com.sun.syndication.propono.atom.common.Categories;
+import com.sun.syndication.propono.atom.server.AtomRequest;
+import java.io.File;
+import java.util.StringTokenizer;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import javax.servlet.http.HttpServletRequest;
+import org.apache.commons.lang.StringUtils;
+
+
+/**
+ * File-based {@link com.sun.syndication.propono.atom.server.AtomHandler}
+ * implementation that stores entries and media-entries to disk. Implemented
+ * using {@link com.sun.syndication.propono.atom.server.impl.FileBasedAtomService}.
+ */
+public class FileBasedAtomHandler implements AtomHandler {
+
+ private static Log log =
+ LogFactory.getFactory().getInstance(FileBasedAtomHandler.class);
+
+ private static String fileStoreDir = null;
+
+ private String userName = null;
+ private String atomProtocolURL = null;
+ private String contextURI = null;
+ private String uploadurl = null;
+
+ private FileBasedAtomService service = null;
+
+ /**
+ * Construct handler to handle one request.
+ * @param req Request to be handled.
+ */
+ public FileBasedAtomHandler( HttpServletRequest req ) {
+ this(req, AtomServlet.getContextDirPath());
+ }
+
+ /**
+ * Contruct handler for one request, using specified file storage directory.
+ * @param req Request to be handled.
+ * @param uploaddir File storage upload dir.
+ */
+ public FileBasedAtomHandler(HttpServletRequest req, String uploaddir) {
+ log.debug("ctor");
+
+ userName = authenticateBASIC(req);
+
+ atomProtocolURL = req.getScheme() + "://" + req.getServerName() + ":"
+ + req.getServerPort() + req.getContextPath() + req.getServletPath();
+
+ contextURI = req.getScheme() + "://" + req.getServerName() + ":"
+ + req.getServerPort() + req.getContextPath();
+
+ try {
+ service = new FileBasedAtomService(userName, uploaddir,
+ contextURI, req.getContextPath(), req.getServletPath());
+ } catch (Throwable t) {
+ throw new RuntimeException("ERROR creating FileBasedAtomService", t);
+ }
+ }
+
+ /**
+ * Method used for validating user. Developers can overwrite this method
+ * and use credentials stored in Database or LDAP to confirm if the user is
+ * allowed to access this service.
+ * @param login user submitted login id
+ * @param password user submitted password
+ */
+ public boolean validateUser(String login, String password) {
+ return true;
+ }
+
+ /**
+ * Get username of authenticated user
+ * @return User name.
+ */
+ public String getAuthenticatedUsername() {
+ // For now return userName as the login id entered for authorization
+ return userName;
+ }
+
+ /**
+ * Get base URI of Atom protocol implementation.
+ * @return Base URI of Atom protocol implemenation.
+ */
+ public String getAtomProtocolURL( ) {
+ if ( atomProtocolURL == null ) {
+ return "app";
+ } else {
+ return atomProtocolURL;
+ }
+ }
+
+ /**
+ * Return introspection document
+ * @throws com.sun.syndication.propono.atom.server.AtomException Unexpected exception.
+ * @return AtomService object with workspaces and collections.
+ */
+ public AtomService getAtomService(AtomRequest areq) throws AtomException {
+ return service;
+ }
+
+ /**
+ * Returns null because we use in-line categories.
+ * @throws com.sun.syndication.propono.atom.server.AtomException Unexpected exception.
+ * @return Categories object
+ */
+ public Categories getCategories(AtomRequest areq) throws AtomException {
+ log.debug("getCollection");
+ String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+ String handle = pathInfo[0];
+ String collection = pathInfo[1];
+ FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ return (Categories)col.getCategories(true).get(0);
+ }
+
+ /**
+ * Get collection specified by pathinfo.
+ * @param areq Details of HTTP request
+ * @return ROME feed representing collection.
+ * @throws com.sun.syndication.propono.atom.server.AtomException Invalid collection or other exception.
+ */
+ public Feed getCollection(AtomRequest areq) throws AtomException {
+ log.debug("getCollection");
+ String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+ String handle = pathInfo[0];
+ String collection = pathInfo[1];
+ FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ return col.getFeedDocument();
+ }
+
+ /**
+ * Create a new entry specified by pathInfo and posted entry. We save the
+ * submitted Atom entry verbatim, but we do set the id and reset the update
+ * time.
+ *
+ * @param entry Entry to be added to collection.
+ * @param areq Details of HTTP request
+ * @throws com.sun.syndication.propono.atom.server.AtomException On invalid collection or other error.
+ * @return Entry as represented on server.
+ */
+ public Entry postEntry(AtomRequest areq, Entry entry) throws AtomException {
+ log.debug("postEntry");
+
+ String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+ String handle = pathInfo[0];
+ String collection = pathInfo[1];
+ FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ try {
+ return col.addEntry(entry);
+
+ } catch (Exception fe) {
+ fe.printStackTrace();
+ throw new AtomException( fe );
+ }
+ }
+
+ /**
+ * Get entry specified by pathInfo.
+ * @param areq Details of HTTP request
+ * @throws com.sun.syndication.propono.atom.server.AtomException On invalid pathinfo or other error.
+ * @return ROME Entry object.
+ */
+ public Entry getEntry(AtomRequest areq) throws AtomException {
+ log.debug("getEntry");
+ String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+ String handle = pathInfo[0];
+ String collection = pathInfo[1];
+ String fileName = pathInfo[2];
+ FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ try {
+ return col.getEntry(fileName);
+
+ } catch (Exception re) {
+ if (re instanceof AtomException) throw (AtomException)re;
+ throw new AtomException("ERROR: getting entry", re);
+ }
+ }
+
+ /**
+ * Update entry specified by pathInfo and posted entry.
+ *
+ * @param entry
+ * @param areq Details of HTTP request
+ * @throws com.sun.syndication.propono.atom.server.AtomException
+ */
+ public void putEntry(AtomRequest areq, Entry entry) throws AtomException {
+ log.debug("putEntry");
+ String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+ String handle = pathInfo[0];
+ String collection = pathInfo[1];
+ String fileName = pathInfo[2];
+ FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ try {
+ col.updateEntry(entry, fileName);
+
+ } catch ( Exception fe ) {
+ throw new AtomException( fe );
+ }
+ }
+
+
+ /**
+ * Delete entry specified by pathInfo.
+ * @param areq Details of HTTP request
+ */
+ public void deleteEntry(AtomRequest areq) throws AtomException {
+ log.debug("deleteEntry");
+ String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+ String handle = pathInfo[0];
+ String collection = pathInfo[1];
+ String fileName = pathInfo[2];
+ FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ try {
+ col.deleteEntry(fileName);
+
+ } catch (Exception e) {
+ String msg = "ERROR in atom.deleteResource";
+ log.error(msg,e);
+ throw new AtomException(msg);
+ }
+ }
+
+
+ /**
+ * Store media data in collection specified by pathInfo, create an Atom
+ * media-link entry to store metadata for the new media file and return
+ * that entry to the caller.
+ * @param areq Details of HTTP request
+ * @param entry New entry initialzied with only title and content type
+ * @return Location URL of new media entry
+ */
+ public Entry postMedia(AtomRequest areq, Entry entry) throws AtomException {
+
+ // get incoming slug from HTTP header
+ String slug = areq.getHeader("Slug");
+
+ if (log.isDebugEnabled()) {
+ log.debug("postMedia - title: "+entry.getTitle()+" slug:"+slug);
+ }
+
+ try {
+ File tempFile = null;
+ String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+ String handle = pathInfo[0];
+ String collection = pathInfo[1];
+ FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ try {
+ col.addMediaEntry(entry, slug, areq.getInputStream());
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ String msg = "ERROR reading posted file";
+ log.error(msg,e);
+ throw new AtomException(msg, e);
+ } finally {
+ if (tempFile != null) tempFile.delete();
+ }
+
+ } catch (Exception re) {
+ throw new AtomException("ERROR: posting media");
+ }
+ return entry;
+ }
+
+ /**
+ * Update the media file part of a media-link entry.
+ * @param areq Details of HTTP request
+ * Assuming pathInfo of form /user-name/resource/name
+ */
+ public void putMedia(AtomRequest areq) throws AtomException {
+
+ log.debug("putMedia");
+ String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+ String handle = pathInfo[0];
+ String collection = pathInfo[1];
+ String fileName = pathInfo[3];
+ FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ try {
+ col.updateMediaEntry(fileName, areq.getContentType(), areq.getInputStream());
+
+ } catch (Exception re) {
+ throw new AtomException("ERROR: posting media");
+ }
+ }
+
+ public AtomMediaResource getMediaResource(AtomRequest areq) throws AtomException {
+ log.debug("putMedia");
+ String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+ String handle = pathInfo[0];
+ String collection = pathInfo[1];
+ String fileName = pathInfo[3];
+ FileBasedCollection col = service.findCollectionByHandle(handle, collection);
+ try {
+ return col.getMediaResource(fileName);
+
+ } catch (Exception re) {
+ throw new AtomException("ERROR: posting media");
+ }
+ }
+
+ /**
+ * Return true if specified pathinfo represents URI of service doc.
+ */
+ public boolean isAtomServiceURI(AtomRequest areq) {
+ String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+ if (pathInfo.length==0) return true;
+ return false;
+ }
+
+ /**
+ * Return true if specified pathinfo represents URI of category doc.
+ */
+ public boolean isCategoriesURI(AtomRequest areq) {
+ log.debug("isCategoriesDocumentURI");
+ String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+ if (pathInfo.length == 3 && "categories".equals(pathInfo[2])) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Return true if specified pathinfo represents URI of a collection.
+ */
+ public boolean isCollectionURI(AtomRequest areq) {
+ log.debug("isCollectionURI");
+ // workspace/collection-plural
+ // if length is 2 and points to a valid collection then YES
+ String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+ if (pathInfo.length == 2) {
+ String handle = pathInfo[0];
+ String collection = pathInfo[1];
+ if (service.findCollectionByHandle(handle, collection) != null) {
+ return true;
+ }
+ }
+ return false;
+
+ }
+
+ /**
+ * Return true if specified pathinfo represents URI of an Atom entry.
+ */
+ public boolean isEntryURI(AtomRequest areq) {
+ log.debug("isEntryURI");
+ // workspace/collection-singular/fsid
+ // if length is 3 and points to a valid collection then YES
+ String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+ if (pathInfo.length == 3) {
+ String handle = pathInfo[0];
+ String collection = pathInfo[1];
+ if (service.findCollectionByHandle(handle, collection) != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return true if specified pathinfo represents media-edit URI.
+ */
+ public boolean isMediaEditURI(AtomRequest areq) {
+ log.debug("isMediaEditURI");
+ // workspace/collection-singular/fsid/media/fsid
+ // if length is 4, points to a valid collection and fsid is mentioned twice then YES
+ String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
+ if (pathInfo.length == 4) {
+ String handle = pathInfo[0];
+ String collection = pathInfo[1];
+ String media = pathInfo[2];
+ String fsid = pathInfo[3];
+ if (service.findCollectionByHandle(handle, collection) != null && media.equals("media")) {
+ return true;
+ }
+ }
+ return false;
+
+ }
+
+ /**
+ * BASIC authentication.
+ */
+ public String authenticateBASIC(HttpServletRequest request) {
+ log.debug("authenticateBASIC");
+ boolean valid = false;
+ String userID = null;
+ String password = null;
+ try {
+ String authHeader = request.getHeader("Authorization");
+ if (authHeader != null) {
+ StringTokenizer st = new StringTokenizer(authHeader);
+ if (st.hasMoreTokens()) {
+ String basic = st.nextToken();
+ if (basic.equalsIgnoreCase("Basic")) {
+ String credentials = st.nextToken();
+ String userPass = new String(Base64.decodeBase64(credentials.getBytes()));
+ int p = userPass.indexOf(":");
+ if (p != -1) {
+ userID = userPass.substring(0, p);
+ password = userPass.substring(p+1);
+
+ // Validate the User.
+ valid = validateUser( userID, password );
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ log.debug(e);
+ }
+ if (valid) {
+ //For now assume userID as userName
+ return userID;
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/atom/server/impl/FileBasedAtomHandlerFactory.java b/src/main/java/com/sun/syndication/propono/atom/server/impl/FileBasedAtomHandlerFactory.java
new file mode 100644
index 0000000..74ba434
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/server/impl/FileBasedAtomHandlerFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.server.impl;
+
+import com.sun.syndication.propono.atom.server.AtomHandlerFactory;
+import com.sun.syndication.propono.atom.server.AtomHandler;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Extends {@link com.sun.syndication.propono.atom.server.AtomHandlerFactory} to create and return
+ * {@link com.sun.syndication.propono.atom.server.impl.FileBasedAtomHandler}.
+ */
+public class FileBasedAtomHandlerFactory extends AtomHandlerFactory {
+
+ /**
+ * Create new AtomHandler.
+ */
+ public AtomHandler newAtomHandler(
+ HttpServletRequest req, HttpServletResponse res ) {
+ return new FileBasedAtomHandler(req);
+ }
+}
+
diff --git a/src/main/java/com/sun/syndication/propono/atom/server/impl/FileBasedAtomService.java b/src/main/java/com/sun/syndication/propono/atom/server/impl/FileBasedAtomService.java
new file mode 100644
index 0000000..9615a74
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/atom/server/impl/FileBasedAtomService.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.atom.server.impl;
+
+import com.sun.syndication.propono.atom.common.AtomService;
+import com.sun.syndication.propono.utils.Utilities;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+
+/**
+ * File based Atom service.
+ * Supports one workspace per user.
+ * Collections in workspace are defined in /propono.properties, for example:
+ *
+ *
+ *
+ * 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 Map workspaceMap = new TreeMap();
+ private Map collectionMap = new TreeMap();
+ private static Properties cacheProps = new Properties();
+ private boolean firstTime = true;
+
+ /**
+ * Creates a new instance of FileBasedAtomService.
+ */
+ public FileBasedAtomService(
+ String userName, String baseDir, String contextURI, String contextPath, String servletPath) throws Exception {
+ String workspaceHandle = userName;
+
+ // One workspace per user
+ FileBasedWorkspace workspace = new FileBasedWorkspace(workspaceHandle, baseDir);
+ workspaceMap.put(userName, workspace);
+
+ if (firstTime) {
+ synchronized(cacheProps) {
+ InputStream is = getClass().getResourceAsStream("/propono.properties");
+ if (is != null) cacheProps.load(is);
+ firstTime = false;
+ }
+ }
+ // can't find propono.properties, so use system props instead
+ if (cacheProps == null) cacheProps = System.getProperties();
+
+ String relativeURIsString = cacheProps.getProperty(
+ "propono.atomserver.filebased.relativeURIs");
+ boolean relativeURIs = "true".equals(relativeURIsString);
+
+ String inlineCategoriesString = cacheProps.getProperty(
+ "propono.atomserver.filebased.inlineCategories");
+ boolean inlineCategories = "true".equals(inlineCategoriesString);
+
+ String colnames = cacheProps.getProperty("propono.atomserver.filebased.collections");
+ if (colnames != null) {
+
+ // collections specified in propono.properties, use those
+
+ String[] colarray = Utilities.stringToStringArray(colnames,",");
+ for (int i=0; i[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(Entry entry, String slug, InputStream is) throws Exception {
+ synchronized (FileStore.getFileStore()) {
+
+ // Save media file temp file
+ Content content = (Content)entry.getContents().get(0);
+ if (entry.getTitle() == null) {
+ entry.setTitle(slug);
+ }
+ String fileName = createFileName((slug != null) ? slug : entry.getTitle(), content.getType());
+ File tempFile = File.createTempFile(fileName, "tmp");
+ FileOutputStream fos = new FileOutputStream(tempFile);
+ Utilities.copyInputToOutput(is, fos);
+ fos.close();
+
+ // Save media file
+ FileInputStream fis = new FileInputStream(tempFile);
+ saveMediaFile(fileName, content.getType(), tempFile.length(), fis);
+ fis.close();
+ File resourceFile = new File(getEntryMediaPath(fileName));
+
+ // Create media-link entry
+ updateTimestamps(entry);
+
+ // Save media-link entry
+ String entryPath = getEntryPath(fileName);
+ OutputStream os = FileStore.getFileStore().getFileOutputStream(entryPath);
+ updateMediaEntryAppLinks(entry, resourceFile.getName(), true);
+ Atom10Generator.serializeEntry(entry, new OutputStreamWriter(os, "UTF-8"));
+ os.flush();
+ os.close();
+
+ // Update feed with new entry
+ Feed f = getFeedDocument();
+ updateMediaEntryAppLinks(entry, resourceFile.getName(), false);
+ updateFeedDocumentWithNewEntry(f, entry);
+
+ return getEntryEditURI(fileName, false, true);
+ }
+ }
+
+ /**
+ * Get an entry from the collection.
+ * @param fsid Internal ID of entry to be returned
+ * @throws java.lang.Exception On error
+ * @return Entry specified by fileName/ID
+ */
+ public Entry getEntry(String fsid) throws Exception {
+ if (fsid.endsWith(".media-link")) {
+ fsid = fsid.substring(0, fsid.length() - ".media-link".length());
+ }
+
+ String entryPath = getEntryPath(fsid);
+
+ checkExistence(entryPath);
+ InputStream in = FileStore.getFileStore().getFileInputStream(entryPath);
+
+ final Entry entry;
+ String filePath = getEntryMediaPath(fsid);
+ File resource = new File(fsid);
+ if (resource.exists()) {
+ entry = loadAtomResourceEntry(in, resource);
+ updateMediaEntryAppLinks(entry, fsid, true);
+ } else {
+ entry = loadAtomEntry(in);
+ updateEntryAppLinks(entry, fsid, true);
+ }
+ return entry;
+ }
+
+ /**
+ * Get media resource wrapping a file.
+ */
+ public AtomMediaResource getMediaResource(String fileName) throws Exception {
+ String filePath = getEntryMediaPath(fileName);
+ File resource = new File(filePath);
+ return new AtomMediaResource(resource);
+ }
+
+ /**
+ * Update an entry in the collection.
+ * @param entry Updated entry to be stored
+ * @param fsid Internal ID of entry
+ * @throws java.lang.Exception On error
+ */
+ public void updateEntry(Entry entry, String fsid) throws Exception {
+ synchronized (FileStore.getFileStore()) {
+
+ Feed f = getFeedDocument();
+
+ if (fsid.endsWith(".media-link")) {
+ fsid = fsid.substring(0, fsid.length() - ".media-link".length());
+ }
+
+ updateTimestamps(entry);
+
+ updateEntryAppLinks(entry, fsid, false);
+ updateFeedDocumentWithExistingEntry(f, entry);
+
+ String entryPath = getEntryPath(fsid);
+ OutputStream os = FileStore.getFileStore().getFileOutputStream(entryPath);
+ updateEntryAppLinks(entry, fsid, true);
+ Atom10Generator.serializeEntry(entry, new OutputStreamWriter(os, "UTF-8"));
+ os.flush();
+ os.close();
+ }
+ }
+
+ /**
+ * Update media associated with a media-link entry.
+ * @param fileName Internal ID of entry being updated
+ * @param contentType Content type of data
+ * @param is Source of updated data
+ * @throws java.lang.Exception On error
+ * @return Updated Entry as it exists on server
+ */
+ public Entry updateMediaEntry(String fileName, String contentType, InputStream is) throws Exception {
+ synchronized (FileStore.getFileStore()) {
+
+ File tempFile = File.createTempFile(fileName, "tmp");
+ FileOutputStream fos = new FileOutputStream(tempFile);
+ Utilities.copyInputToOutput(is, fos);
+ fos.close();
+
+ // Update media file
+ FileInputStream fis = new FileInputStream(tempFile);
+ saveMediaFile(fileName, contentType, tempFile.length(), fis);
+ fis.close();
+ File resourceFile = new File(getEntryMediaPath(fileName));
+
+ // Load media-link entry to return
+ String entryPath = getEntryPath(fileName);
+ InputStream in = FileStore.getFileStore().getFileInputStream(entryPath);
+ Entry atomEntry = loadAtomResourceEntry(in, resourceFile);
+
+ updateTimestamps(atomEntry);
+ updateMediaEntryAppLinks(atomEntry, fileName, false);
+
+ // Update feed with new entry
+ Feed f = getFeedDocument();
+ updateFeedDocumentWithExistingEntry(f, atomEntry);
+
+ // Save updated media-link entry
+ OutputStream os = FileStore.getFileStore().getFileOutputStream(entryPath);
+ updateMediaEntryAppLinks(atomEntry, fileName, true);
+ Atom10Generator.serializeEntry(atomEntry, new OutputStreamWriter(os, "UTF-8"));
+ os.flush();
+ os.close();
+
+ return atomEntry;
+ }
+ }
+
+ /**
+ * Delete an entry and any associated media file.
+ * @param fsid Internal ID of entry
+ * @throws java.lang.Exception On error
+ */
+ public void deleteEntry(String fsid) throws Exception {
+ synchronized (FileStore.getFileStore()) {
+
+ // Remove entry from Feed
+ Feed feed = getFeedDocument();
+ updateFeedDocumentRemovingEntry(feed, fsid);
+
+ String entryFilePath = this.getEntryPath(fsid);
+ FileStore.getFileStore().deleteFile(entryFilePath);
+
+ String entryMediaPath = this.getEntryMediaPath(fsid);
+ if (entryMediaPath != null) {
+ FileStore.getFileStore().deleteFile(entryMediaPath);
+ }
+
+ String entryDirPath = getEntryDirPath(fsid);
+ FileStore.getFileStore().deleteDirectory(entryDirPath);
+
+ try {Thread.sleep(500L);}catch(Exception ignored){}
+ }
+ }
+
+ private void updateFeedDocumentWithNewEntry(Feed f, Entry e) throws AtomException {
+ boolean inserted = false;
+ for (int i=0; iRepresents 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.sun.syndication.propono.blogclient.BlogClientException On error fetching the blog entry.
+ * @return Blog entry specified by token.
+ */
+ public BlogEntry getEntry(String token) throws BlogClientException;
+
+ /**
+ * Gets listing of entry and resource collections available in the blog,
+ * including the primary collections.
+ * @throws BlogClientException On error fetching collections.
+ * @return List of Blog.Collection objects.
+ */
+ public List getCollections() throws BlogClientException;
+
+ /**
+ * Get collection by token.
+ * @param token Token from a collection's getToken() method.
+ * @throws BlogClientException On error fetching collection.
+ * @return Blog.Collection object.
+ */
+ public Collection getCollection(String token) throws BlogClientException;
+
+ /**
+ * Represents an entry or resource collection on a blog server.
+ */
+ public interface Collection {
+
+ /**
+ * Get blog that contains this collection.
+ * @return Blog that contains this collection.
+ */
+ public Blog getBlog();
+
+ /**
+ * Title of collection.
+ * @return Title of collecton.
+ */
+ public String getTitle();
+
+ /**
+ * Token that can be used to fetch collection.
+ * @return Token that can be used to fetch collection.
+ */
+ public String getToken();
+
+ /**
+ * Content-types accepted by collection.
+ * @return Comma-separated list of content-types accepted.
+ */
+ public List getAccepts();
+
+ /**
+ * Determines if collection will accept a content-type.
+ * @param contentType Content-type to be considered.
+ * @return True of content type will be accepted, false otherwise.
+ */
+ public boolean accepts(String contentType);
+
+ /**
+ * Return categories allowed by colletion.
+ * @throws BlogClientException On error fetching categories.
+ * @return List of BlogEntry.Category objects for this collection.
+ */
+ public List getCategories() throws BlogClientException;
+
+ /**
+ * Create but do not save new entry in collection.
+ * To save entry, call its save() method.
+ * @throws BlogClientException On error creating entry.
+ * @return New BlogEntry object.
+ */
+ public BlogEntry newEntry() throws BlogClientException;
+
+ /**
+ * Create but do not save new resource in collection.
+ * To save resource, call its save() method.
+ * @param name Name of new resource.
+ * @param contentType MIME content-type of new resource.
+ * @param bytes Data for new resource.
+ * @throws BlogClientException On error creating entry.
+ * @return New BlogResource object,
+ */
+ public BlogResource newResource(String name, String contentType, byte[] bytes) throws BlogClientException;
+
+ /**
+ * Get iterator over entries/resources in this collection.
+ * @return List of BlogEntry objects, some may be BlogResources.
+ * @throws BlogClientException On error fetching entries/resources.
+ */
+ public Iterator getEntries() throws BlogClientException;
+
+ /**
+ * Save or update a BlogEntry in this collection by adding it to this
+ * collection and then calling it's entry.save() method.
+ * @param entry BlogEntry to be saved.
+ * @throws BlogClientException On error saving entry.
+ * @return URI of entry.
+ */
+ public String saveEntry(BlogEntry entry) throws BlogClientException;
+
+ /**
+ * Save or update resource in this collection
+ * @param resource BlogResource to be saved.
+ * @throws BlogClientException On error saving resource.
+ * @return URI of resource.
+ */
+ public String saveResource(BlogResource resource) throws BlogClientException;
+ }
+
+
+ // Deprecated primary collection methods, instead use collections directly.
+
+ /**
+ * Get iterator over entries in primary entries collection (the first
+ * collection that accepts entries). Note that entries may be partial,
+ * so don't try to update and save them: to update and entry, first fetch
+ * it with getEntry(), change fields, then call entry.save();
+ * @return To iterate over all entries in collection.
+ * @throws BlogClientException On failure or if there is no primary entries collection.
+ *
+ * @deprecated Instead use collections directly.
+ */
+ public Iterator getEntries() throws BlogClientException;
+
+ /**
+ * Get entries in primary resources collection (the first
+ * collection that accepts anything other than entries).
+ * @throws BlogClientException On failure or if there is no primary resources collection.
+ * @return To iterate over all resojrces in collection.
+ *
+ * @deprecated Instead use collections directly.
+ */
+ public Iterator getResources() throws BlogClientException;
+
+
+ /**
+ * Create but do not save it to server new BlogEntry in primary entries collection
+ * (the first collection found that accepts entries). To save the entry to the
+ * server to a collection, use the entry's save() method.
+ * @throws BlogClientException On error or if there is no primary entries collection.
+ * @return Unsaved BlogEntry in primary entries collection.
+ *
+ * @deprecated Instead use collections directly.
+ */
+ public BlogEntry newEntry() throws BlogClientException;
+
+ /**
+ * Create but do not save it to server new BlogResource in primary resources collection
+ * (the first collection found that accepts resources). To save the resource to the
+ * server to a collection, use the resource's save() method.
+ * @param name Name of resource to be saved.
+ * @param type MIME content type of resource data.
+ * @param bytes Bytes of resource data.
+ * @throws BlogClientException On error or if there is no primary respurces collection.
+ * @return Unsaved BlogEntry in primary resources collection.
+ *
+ * @deprecated Instead use collections directly.
+ */
+ public BlogResource newResource(String name, String type, byte[] bytes)
+ throws BlogClientException;
+
+ /**
+ * Returns list of available BlogEntry.Category in primary entries collection.
+ * @throws BlogClientException On error or if there is no primary entries collection.
+ * @return List of BlogEntry.Category objects.
+ *
+ * @deprecated Instead use collections directly.
+ */
+ public List getCategories() throws BlogClientException;
+}
diff --git a/src/main/java/com/sun/syndication/propono/blogclient/BlogClientException.java b/src/main/java/com/sun/syndication/propono/blogclient/BlogClientException.java
new file mode 100644
index 0000000..b982a5a
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/blogclient/BlogClientException.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2007 Dave Johnson (Blogapps project)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.blogclient;
+
+/**
+ * Represents a Blog Client exception, the library throws these instead of
+ * implementation specific exceptions.
+ */
+public class BlogClientException extends Exception {
+
+ /**
+ * Construct a new exception
+ * @param msg Text message that explains exception
+ */
+ public BlogClientException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Construct a new exception which wraps a throwable.
+ * @param msg Text message that explains exception
+ * @param t Throwable to be wrapped by exception
+ */
+ public BlogClientException(String msg, Throwable t) {
+ super(msg, t);
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/blogclient/BlogConnection.java b/src/main/java/com/sun/syndication/propono/blogclient/BlogConnection.java
new file mode 100644
index 0000000..e889f9b
--- /dev/null
+++ b/src/main/java/com/sun/syndication/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.sun.syndication.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/sun/syndication/propono/blogclient/BlogConnectionFactory.java b/src/main/java/com/sun/syndication/propono/blogclient/BlogConnectionFactory.java
new file mode 100644
index 0000000..0f9945f
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/blogclient/BlogConnectionFactory.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2007 Dave Johnson (Blogapps project)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.blogclient;
+
+import java.lang.reflect.Constructor;
+
+/**
+ * Entry point to the Blogapps blog client library.
+ */
+public class BlogConnectionFactory {
+
+ // BlogConnection implementations must:
+ // 1) implement BlogConnection
+ // 2) privide contructor that accepts three strings args: url, username and password.
+
+ // TODO: make implementations configurable
+ private static String ATOMPROTOCOL_IMPL_CLASS =
+ "com.sun.syndication.propono.blogclient.atomprotocol.AtomConnection";
+
+ private static String METAWEBLOG_IMPL_CLASS =
+ "com.sun.syndication.propono.blogclient.metaweblog.MetaWeblogConnection";
+
+ /**
+ * Create a connection to a blog server.
+ * @param type Connection type, must be "atom" or "metaweblog"
+ * @param url End-point URL to connect to
+ * @param username Username for login to blog server
+ * @param password Password for login to blog server
+ */
+ public static BlogConnection getBlogConnection(
+ String type, String url, String username, String password)
+ throws BlogClientException {
+ BlogConnection blogConnection = null;
+ if (type == null || type.equals("metaweblog")) {
+ blogConnection = createBlogConnection(
+ METAWEBLOG_IMPL_CLASS, url, username, password);
+ } else if (type.equals("atom")) {
+ blogConnection = createBlogConnection(
+ ATOMPROTOCOL_IMPL_CLASS, url, username, password);
+ } else {
+ throw new BlogClientException("Type must be 'atom' or 'metaweblog'");
+ }
+ return blogConnection;
+ }
+
+ private static BlogConnection createBlogConnection(
+ String className, String url, String username, String password)
+ throws BlogClientException {
+ Class conClass;
+ try {
+ conClass = Class.forName(className);
+ } catch (ClassNotFoundException ex) {
+ throw new BlogClientException(
+ "BlogConnection impl. class not found: "+className, ex);
+ }
+ Class[] args = new Class[] {String.class, String.class, String.class};
+ Constructor ctor;
+ try {
+ ctor = conClass.getConstructor(args);
+ return (BlogConnection)
+ ctor.newInstance(new Object[] {url, username, password});
+ } catch (Throwable t) {
+ throw new BlogClientException(
+ "ERROR instantiating BlogConnection impl.", t);
+ }
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/blogclient/BlogEntry.java b/src/main/java/com/sun/syndication/propono/blogclient/BlogEntry.java
new file mode 100644
index 0000000..be79697
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/blogclient/BlogEntry.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2007 Dave Johnson (Blogapps project)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.blogclient;
+
+import java.util.Date;
+import java.util.List;
+import java.io.InputStream;
+
+/**
+ * Represents a single blog entry.
+ */
+public interface BlogEntry {
+
+ /** Get token, which can be used to fetch the blog entry */
+ public String getToken();
+
+ /**
+ * Save this entry to it's collection. If this is a new entry and does not
+ * have a collection yet, then save() will save it to the primary collection.
+ */
+ public void save() throws BlogClientException;
+
+ /** Delete this entry from blog server */
+ public void delete() throws BlogClientException;
+
+ /** Permanent link to this entry (assigned by server) */
+ public String getPermalink();
+
+ /** Blog is associated with a blog */
+ public Blog getBlog();
+
+ /** Get categories, a list of BlogEntry.Category objects */
+ public List getCategories();
+
+ /** Set categories, a list of BlogEntry.Category objects */
+ public void setCategories(List categories);
+
+ /** Get globally unique ID of this blog entry */
+ public String getId();
+
+ /** Get title of this blog entry */
+ public String getTitle();
+
+ /** Set title of this blog entry */
+ public void setTitle(String title);
+
+ /** Get summary of this blog entry */
+ public String getSummary();
+
+ /** Set summary of this blog entry */
+ public void setSummary(String summary);
+
+ /** Get content of this blog entry */
+ public Content getContent();
+
+ /** Set content of this blog entry */
+ public void setContent(Content content);
+
+ /** Get draft status of this entry */
+ public boolean getDraft();
+
+ /** Set draft status of this entry */
+ public void setDraft(boolean draft);
+
+ /** Get author of this entry */
+ public Person getAuthor();
+
+ /** Set author of this entry */
+ public void setAuthor(Person author);
+
+ /** Set publish date of this entry */
+ public Date getPublicationDate();
+
+ /** Get publish date of this entry */
+ public void setPublicationDate(Date date);
+
+ /** Get update date of this entry */
+ public Date getModificationDate();
+
+ /** Set update date of this entry */
+ public void setModificationDate(Date date);
+
+ /** Represents blog entry content */
+ public class Content {
+ String type = "html";
+ String value = null;
+ String src = null;
+
+ /** Construct content */
+ public Content() {}
+
+ /** Construct content with value (and type="html") */
+ public Content(String value) {
+ this.value = value;
+ }
+ /** Get value of content if in-line */
+ public String getValue() {
+ return value;
+ }
+ /** Set value of content if in-line */
+ public void setValue(String value) {
+ this.value = value;
+ }
+ /**
+ * Get type of content, either "text", "html", "xhtml" or a MIME content-type.
+ * Defaults to HTML.
+ */
+ public String getType() {
+ return type;
+ }
+ /**
+ * Set type of content, either "text", "html", "xhtml" or a MIME content-type.
+ * Defaults to HTML.
+ */
+ public void setType(String type) {
+ this.type = type;
+ }
+ /** Get URI of content if out-of-line */
+ public String getSrc() {
+ return src;
+ }
+ /** Set URI of content if out-of-line */
+ public void setSrc(String src) {
+ this.src = src;
+ }
+ }
+
+ /** Represents a blog author or contributor */
+ public class Person {
+ String name;
+ String email;
+ String url;
+ /** Get person's email */
+ public String getEmail() {
+ return email;
+ }
+ /** Set person's email */
+ public void setEmail(String email) {
+ this.email = email;
+ }
+ /** Get person's name */
+ public String getName() {
+ return name;
+ }
+ /** Set person's name */
+ public void setName(String name) {
+ this.name = name;
+ }
+ /** Get person's URL */
+ public String getUrl() {
+ return url;
+ }
+ /** Set person's URL */
+ public void setUrl(String url) {
+ this.url = url;
+ }
+ /** Returns person's name */
+ public String toString() {
+ return name;
+ }
+ }
+
+ /** Represents a weblog category */
+ public class Category {
+ String id;
+ String name;
+ String url;
+ /**
+ * Create new Catetory
+ */
+ public Category() {}
+ /**
+ * Create new category with name.
+ */
+ public Category(String id) {
+ this.id = id;
+ this.name = id;
+ }
+ /**
+ * Determines if categories are equal based on id.
+ */
+ public boolean equals(Object obj) {
+ Category other = (Category)obj;
+ if (obj == null) return false;
+ if (getId() != null && other.getId() != null
+ && getId().equals(other.getId())) return true;
+ return false;
+ }
+ /** Get category id */
+ public String getId() {
+ return id;
+ }
+ /** Set category id */
+ public void setId(String id) {
+ this.id = id;
+ }
+ /** Get category display name */
+ public String getName() {
+ return name;
+ }
+ /** Set category display name */
+ public void setName(String name) {
+ this.name = name;
+ }
+ /** Get URL of category domain */
+ public String getUrl() {
+ return url;
+ }
+ /** Set URL of category domain */
+ public void setUrl(String url) {
+ this.url = url;
+ }
+ /** Return category's name or id for display */
+ public String toString() {
+ return name!=null ? name : id;
+ }
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/blogclient/BlogResource.java b/src/main/java/com/sun/syndication/propono/blogclient/BlogResource.java
new file mode 100644
index 0000000..5e7043e
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/blogclient/BlogResource.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2007 Dave Johnson (Blogapps project)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.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/sun/syndication/propono/blogclient/atomprotocol/AtomBlog.java b/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomBlog.java
new file mode 100644
index 0000000..9da2b20
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomBlog.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2007 Dave Johnson (Blogapps project)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.blogclient.atomprotocol;
+
+import com.sun.syndication.propono.utils.ProponoException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import com.sun.syndication.propono.blogclient.Blog;
+import com.sun.syndication.propono.blogclient.BlogClientException;
+import com.sun.syndication.propono.blogclient.BlogEntry;
+import com.sun.syndication.propono.blogclient.BlogResource;
+import com.sun.syndication.propono.atom.client.ClientAtomService;
+import com.sun.syndication.propono.atom.client.ClientCollection;
+import com.sun.syndication.propono.atom.client.ClientEntry;
+import com.sun.syndication.propono.atom.client.ClientMediaEntry;
+import com.sun.syndication.propono.atom.client.ClientWorkspace;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Atom protocol implementation of the BlogClient Blog interface.
+ */
+public class AtomBlog implements Blog {
+ static final Log logger = LogFactory.getLog(AtomBlog.class);
+ private HttpClient httpClient = null;
+ private String name = null;
+ private ClientAtomService service;
+ private ClientWorkspace workspace = null;
+ private AtomCollection entriesCollection = null;
+ private AtomCollection resourcesCollection = null;
+ private Map collections = new TreeMap();
+
+ /**
+ * Create AtomBlog using specified HTTPClient, user account and workspace,
+ * called by AtomConnection. Fetches Atom Service document and creates
+ * an AtomCollection object for each collection found. The first entry
+ * collection is considered the primary entry collection. And the first
+ * resource collection is considered the primary resource collection.
+ */
+ AtomBlog(ClientAtomService service, ClientWorkspace workspace) {
+ this.setService(service);
+ this.setWorkspace(workspace);
+ this.name = workspace.getTitle();
+ Iterator members = workspace.getCollections().iterator();
+
+ while (members.hasNext()) {
+ ClientCollection col = (ClientCollection) members.next();
+ if (col.accepts("entry") && entriesCollection == null) {
+ // first entry collection is primary entry collection
+ entriesCollection = new AtomCollection(this, col);
+ }
+ else if (!col.accepts("entry") && resourcesCollection == null) {
+ // first non-entry collection is primary resource collection
+ resourcesCollection = new AtomCollection(this, col);
+ }
+ collections.put(col.getHrefResolved(), new AtomCollection(this, col));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getName() { return name; }
+
+ /**
+ * String display of blog, returns name.
+ */
+ public String toString() { return getName(); }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getToken() { return entriesCollection.getToken(); }
+
+ /**
+ * {@inheritDoc}
+ */
+ public BlogEntry newEntry() throws BlogClientException {
+ if (entriesCollection == null) throw new BlogClientException("No entry collection");
+ return entriesCollection.newEntry();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public BlogEntry getEntry(String token) throws BlogClientException {
+ ClientEntry clientEntry = null;
+ AtomEntry atomEntry = null;
+ try {
+ clientEntry = getService().getEntry(token);
+ } catch (ProponoException ex) {
+ throw new BlogClientException("ERROR: fetching entry", ex);
+ }
+ if (clientEntry != null && clientEntry instanceof ClientMediaEntry) {
+ return new AtomResource(this, (ClientMediaEntry)clientEntry);
+ } else if (clientEntry != null && clientEntry instanceof ClientEntry) {
+ return new AtomEntry(this, clientEntry);
+ } else {
+ throw new BlogClientException("ERROR: unknown object type returned");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Iterator getEntries() throws BlogClientException {
+ if (entriesCollection == null) throw new BlogClientException("No primary entry collection");
+ return new AtomEntryIterator(entriesCollection);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Iterator getResources() throws BlogClientException {
+ if (resourcesCollection == null) throw new BlogClientException("No primary entry collection");
+ return new AtomEntryIterator(resourcesCollection);
+ }
+
+ String saveEntry(BlogEntry entry) throws BlogClientException {
+ if (entriesCollection == null) throw new BlogClientException("No primary entry collection");
+ return entriesCollection.saveEntry(entry);
+ }
+
+ void deleteEntry(BlogEntry entry) throws BlogClientException {
+ if (entriesCollection == null) throw new BlogClientException("No primary entry collection");
+ entriesCollection.deleteEntry(entry);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public List getCategories() throws BlogClientException {
+ if (entriesCollection == null) throw new BlogClientException("No primary entry collection");
+ return entriesCollection.getCategories();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public BlogResource newResource(
+ String name, String contentType, byte[] bytes) throws BlogClientException {
+ if (resourcesCollection == null) {
+ throw new BlogClientException("No resource collection");
+ }
+ return resourcesCollection.newResource(name, contentType, bytes);
+ }
+
+
+ String saveResource(BlogResource res) throws BlogClientException {
+ if (resourcesCollection == null) throw new BlogClientException("No primary resource collection");
+ return resourcesCollection.saveResource(res);
+ }
+
+ void deleteResource(BlogResource resource) throws BlogClientException {
+ deleteEntry((BlogEntry)resource);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public List getCollections() throws BlogClientException {
+ return new ArrayList(collections.values());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Blog.Collection getCollection(String token) throws BlogClientException {
+ return (Blog.Collection)collections.get(token);
+ }
+
+ ClientAtomService getService() {
+ return service;
+ }
+
+ void setService(ClientAtomService service) {
+ this.service = service;
+ }
+
+ ClientWorkspace getWorkspace() {
+ return workspace;
+ }
+
+ void setWorkspace(ClientWorkspace workspace) {
+ this.workspace = workspace;
+ }
+
+}
diff --git a/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomCollection.java b/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomCollection.java
new file mode 100644
index 0000000..1216317
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomCollection.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2007 Dave Johnson (Blogapps project)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.blogclient.atomprotocol;
+
+import com.sun.syndication.feed.atom.Category;
+import com.sun.syndication.propono.atom.client.ClientAtomService;
+import com.sun.syndication.propono.atom.common.Categories;
+import com.sun.syndication.propono.atom.client.ClientCollection;
+import com.sun.syndication.propono.atom.client.ClientEntry;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import com.sun.syndication.propono.blogclient.Blog;
+import com.sun.syndication.propono.blogclient.BlogClientException;
+import com.sun.syndication.propono.blogclient.BlogEntry;
+import com.sun.syndication.propono.blogclient.BlogResource;
+
+/**
+ * Atom protocol implementation of BlogClient Blog.Collection.
+ */
+public class AtomCollection implements Blog.Collection {
+ static final Log logger = LogFactory.getLog(AtomCollection.class);
+
+ private Blog blog = null;
+ private List categories = new ArrayList();
+
+ private ClientCollection clientCollection = null;
+
+
+ AtomCollection(AtomBlog blog, ClientCollection col) {
+ this.blog = blog;
+ this.clientCollection = col;
+ for (Iterator catsIter = col.getCategories().iterator(); catsIter.hasNext();) {
+ Categories cats = (Categories)catsIter.next();
+ for (Iterator catIter = cats.getCategories().iterator(); catIter.hasNext();) {
+ Category cat = (Category)catIter.next();
+ BlogEntry.Category blogCat = new BlogEntry.Category(cat.getTerm());
+ blogCat.setName(cat.getLabel());
+ blogCat.setUrl(cat.getScheme());
+ getCategories().add(blogCat);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getTitle() {
+ return getClientCollection().getTitle();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getToken() {
+ return getClientCollection().getHrefResolved();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public List getAccepts() {
+ return getClientCollection().getAccepts();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean accepts(String ct) {
+ return getClientCollection().accepts(ct);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Iterator getEntries() throws BlogClientException {
+ return new AtomEntryIterator(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public BlogEntry newEntry() throws BlogClientException {
+ AtomBlog atomBlog = (AtomBlog)getBlog();
+ BlogEntry entry = new AtomEntry(atomBlog, this);
+ return entry;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public BlogResource newResource(String name, String contentType, byte[] bytes) throws BlogClientException {
+ return new AtomResource(this, name, contentType, bytes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String saveResource(BlogResource res) throws BlogClientException {
+ ((AtomResource)res).setCollection(this);
+ res.save();
+ return res.getContent().getSrc();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String saveEntry(BlogEntry entry) throws BlogClientException {
+ ((AtomEntry)entry).setCollection(this);
+ entry.save();
+ return entry.getPermalink();
+ }
+
+ void deleteEntry(BlogEntry entry) throws BlogClientException {
+ try {
+ ClientAtomService service = ((AtomBlog)getBlog()).getService();
+ ClientEntry clientEntry = service.getEntry(entry.getToken());
+ clientEntry.remove();
+
+ } catch (Exception e) {
+ throw new BlogClientException("ERROR deleting entry", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Blog getBlog() {
+ return blog;
+ }
+
+ void setBlog(AtomBlog blog) {
+ this.blog = blog;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public List getCategories() {
+ return categories;
+ }
+
+ void setCategories(List categories) {
+ this.categories = categories;
+ }
+
+ ClientCollection getClientCollection() {
+ return clientCollection;
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomConnection.java b/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomConnection.java
new file mode 100644
index 0000000..dc483a4
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomConnection.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2007 Dave Johnson (Blogapps project)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.blogclient.atomprotocol;
+
+import com.sun.syndication.propono.atom.client.AtomClientFactory;
+import com.sun.syndication.propono.atom.client.BasicAuthStrategy;
+import com.sun.syndication.propono.atom.client.ClientAtomService;
+import com.sun.syndication.propono.atom.client.ClientWorkspace;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.jdom.Document;
+
+import com.sun.syndication.propono.blogclient.BlogConnection;
+import com.sun.syndication.propono.blogclient.Blog;
+import com.sun.syndication.propono.blogclient.BlogClientException;
+
+
+/**
+ * Atom protocol of BlogConnection. Connects to Atom server, creates AtomBlog
+ * object for each Atom workspace found and within each blog a collection for each
+ * Atom collection found.
+ */
+public class AtomConnection implements BlogConnection {
+ private static Log logger = LogFactory.getLog(AtomConnection.class);
+ private HttpClient httpClient = null;
+ private Map blogs = new HashMap();
+
+ /**
+ * Create Atom blog client instance for specified URL and user account.
+ * @param uri End-point URL of Atom service
+ * @param username Username of account
+ * @param password Password of account
+ */
+ public AtomConnection(String uri, String username, String password)
+ throws BlogClientException {
+
+ Document doc = null;
+ try {
+ ClientAtomService service = (ClientAtomService)
+ AtomClientFactory.getAtomService(uri, new BasicAuthStrategy(username, password));
+ Iterator iter = service.getWorkspaces().iterator();
+ int count = 0;
+ while (iter.hasNext()) {
+ ClientWorkspace workspace = (ClientWorkspace)iter.next();
+ Blog blog = new AtomBlog(service, workspace);
+ blogs.put(blog.getToken(), blog);
+ }
+ } catch (Throwable t) {
+ throw new BlogClientException("Error connecting to blog server", t);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public List getBlogs() {
+ return new ArrayList(blogs.values());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Blog getBlog(String token) {
+ return (AtomBlog)blogs.get(token);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void setAppkey(String appkey) {
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomEntry.java b/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomEntry.java
new file mode 100644
index 0000000..59deaef
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomEntry.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2007 Dave Johnson (Blogapps project)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.blogclient.atomprotocol;
+
+import com.sun.syndication.propono.utils.ProponoException;
+import com.sun.syndication.propono.atom.common.rome.AppModule;
+import com.sun.syndication.propono.atom.common.rome.AppModuleImpl;
+import com.sun.syndication.propono.blogclient.BlogClientException;
+import com.sun.syndication.propono.blogclient.BlogEntry;
+import com.sun.syndication.propono.blogclient.BaseBlogEntry;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import com.sun.syndication.feed.atom.Entry;
+import com.sun.syndication.feed.atom.Link;
+import com.sun.syndication.propono.atom.client.ClientEntry;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import com.sun.syndication.propono.blogclient.BlogEntry.Person;
+
+/**
+ * Atom protocol implementation of BlogEntry.
+ */
+public class AtomEntry extends BaseBlogEntry implements BlogEntry {
+ static final Log logger = LogFactory.getLog(AtomCollection.class);
+
+ String editURI = null;
+ AtomCollection collection = null;
+
+ AtomEntry(AtomBlog blog, AtomCollection collection) throws BlogClientException {
+ super(blog);
+ this.collection = collection;
+ }
+
+ AtomEntry(AtomCollection collection, ClientEntry entry) throws BlogClientException {
+ this((AtomBlog)collection.getBlog(), collection);
+ //clientEntry = entry;
+ copyFromRomeEntry(entry);
+ }
+
+ AtomEntry(AtomBlog blog, ClientEntry entry) throws BlogClientException {
+ super(blog);
+ //clientEntry = entry;
+ copyFromRomeEntry(entry);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getToken() {
+ return editURI;
+ }
+
+ AtomCollection getCollection() {
+ return collection;
+ }
+
+ void setCollection(AtomCollection collection) {
+ this.collection = collection;
+ }
+
+ /**
+ * True if entry's token's are equal.
+ */
+ public boolean equals(Object o) {
+ if (o instanceof AtomEntry) {
+ AtomEntry other = (AtomEntry)o;
+ if (other.getToken() != null && getToken() != null) {
+ return other.getToken().equals(getToken());
+ }
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void save() throws BlogClientException {
+ boolean create = (getToken() == null);
+ if (create && getCollection() == null) {
+ throw new BlogClientException("Cannot save entry, no collection");
+ } else if (create) {
+ try {
+ ClientEntry clientEntry = collection.getClientCollection().createEntry();
+ copyToRomeEntry(clientEntry);
+ collection.getClientCollection().addEntry(clientEntry);
+ copyFromRomeEntry(clientEntry);
+ } catch (ProponoException ex) {
+ throw new BlogClientException("Error saving entry", ex);
+ }
+ } else {
+ try {
+ ClientEntry clientEntry = ((AtomBlog)getBlog()).getService().getEntry(getToken());
+ copyToRomeEntry(clientEntry);
+ clientEntry.update();
+ copyFromRomeEntry(clientEntry);
+ } catch (ProponoException ex) {
+ throw new BlogClientException("Error updating entry", ex);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void delete() throws BlogClientException {
+ if (getToken() == null) {
+ throw new BlogClientException("Cannot delete unsaved entry");
+ }
+ try {
+ ClientEntry clientEntry = ((AtomBlog)getBlog()).getService().getEntry(editURI);
+ clientEntry.remove();
+ } catch (ProponoException ex) {
+ throw new BlogClientException("Error removing entry", ex);
+ }
+ }
+
+ void copyFromRomeEntry(ClientEntry entry) {
+ id = entry.getId();
+ title = entry.getTitle();
+ editURI = entry.getEditURI();
+ List altlinks = entry.getAlternateLinks();
+ if (altlinks != null) {
+ for (Iterator iter = altlinks.iterator(); iter.hasNext();) {
+ Link link = (Link)iter.next();
+ if ("alternate".equals(link.getRel()) || link.getRel()==null) {
+ permalink = link.getHrefResolved();
+ break;
+ }
+ }
+ }
+ List contents = entry.getContents();
+ com.sun.syndication.feed.atom.Content romeContent = null;
+ if (contents != null && contents.size() > 0) {
+ romeContent = (com.sun.syndication.feed.atom.Content)contents.get(0);
+ }
+ if (romeContent != null) {
+ content = new BlogEntry.Content(romeContent.getValue());
+ content.setType(romeContent.getType());
+ content.setSrc(romeContent.getSrc());
+ }
+ if (entry.getCategories() != null) {
+ List cats = new ArrayList();
+ List romeCats = entry.getCategories();
+ for (Iterator iter=romeCats.iterator(); iter.hasNext();) {
+ com.sun.syndication.feed.atom.Category romeCat =
+ (com.sun.syndication.feed.atom.Category)iter.next();
+ BlogEntry.Category cat = new BlogEntry.Category();
+ cat.setId(romeCat.getTerm());
+ cat.setUrl(romeCat.getScheme());
+ cat.setName(romeCat.getLabel());
+ cats.add(cat);
+ }
+ categories = cats;
+ }
+ List authors = entry.getAuthors();
+ if (authors!=null && authors.size() > 0) {
+ com.sun.syndication.feed.atom.Person romeAuthor =
+ (com.sun.syndication.feed.atom.Person)authors.get(0);
+ if (romeAuthor != null) {
+ author = new Person();
+ author.setName(romeAuthor.getName());
+ author.setEmail(romeAuthor.getEmail());
+ author.setUrl(romeAuthor.getUrl());
+ }
+ }
+ publicationDate = entry.getPublished();
+ modificationDate = entry.getModified();
+
+ AppModule control = (AppModule)entry.getModule(AppModule.URI);
+ if (control != null && control.getDraft() != null) {
+ draft = control.getDraft().booleanValue();
+ } else {
+ draft = false;
+ }
+ }
+ Entry copyToRomeEntry(ClientEntry entry) {
+ if (id != null) {
+ entry.setId(id);
+ }
+ entry.setTitle(title);
+ if (author != null) {
+ com.sun.syndication.feed.atom.Person person =
+ new com.sun.syndication.feed.atom.Person();
+ person.setName(author.getName());
+ person.setEmail(author.getEmail());
+ person.setUrl(author.getUrl());
+ List authors = new ArrayList();
+ authors.add(person);
+ entry.setAuthors(authors);
+ }
+ if (content != null) {
+ com.sun.syndication.feed.atom.Content romeContent =
+ new com.sun.syndication.feed.atom.Content();
+ romeContent.setValue(content.getValue());
+ romeContent.setType(content.getType());
+ List contents = new ArrayList();
+ contents.add(romeContent);
+ entry.setContents(contents);
+ }
+ if (categories != null) {
+ List romeCats = new ArrayList();
+ for (Iterator iter=categories.iterator(); iter.hasNext();) {
+ BlogEntry.Category cat = (BlogEntry.Category)iter.next();
+ com.sun.syndication.feed.atom.Category romeCategory =
+ new com.sun.syndication.feed.atom.Category();
+ romeCategory.setTerm(cat.getId());
+ romeCategory.setScheme(cat.getUrl());
+ romeCategory.setLabel(cat.getName());
+ romeCats.add(romeCategory);
+ }
+ entry.setCategories(romeCats);
+ }
+ entry.setPublished((publicationDate == null) ? new Date() : publicationDate);
+ entry.setModified((modificationDate == null) ? new Date() : modificationDate);
+
+ List modules = new ArrayList();
+ AppModule control = new AppModuleImpl();
+ control.setDraft(new Boolean(draft));
+ modules.add(control);
+ entry.setModules(modules);
+
+ return entry;
+ }
+
+}
diff --git a/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomEntryIterator.java b/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomEntryIterator.java
new file mode 100644
index 0000000..9191c15
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomEntryIterator.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2007 Dave Johnson (Blogapps project)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.blogclient.atomprotocol;
+
+import com.sun.syndication.propono.atom.client.ClientEntry;
+import com.sun.syndication.propono.atom.client.ClientMediaEntry;
+import java.util.Iterator;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import com.sun.syndication.propono.blogclient.BlogClientException;
+
+/**
+ * Atom protocol implementation of BlogClient entry iterator.
+ */
+public class AtomEntryIterator implements Iterator {
+ static final Log logger = LogFactory.getLog(AtomEntryIterator.class);
+ private Iterator iterator = null;
+ private AtomCollection collection = null;
+
+ AtomEntryIterator(AtomCollection collection) throws BlogClientException {
+ try {
+ this.collection = collection;
+ iterator = collection.getClientCollection().getEntries();
+ } catch (Exception e) {
+ throw new BlogClientException("ERROR fetching collection", e);
+ }
+ }
+
+ /**
+ * True if more entries are available.
+ */
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ /**
+ * Get next entry.
+ */
+ public Object next() {
+ try {
+ ClientEntry entry = (ClientEntry)iterator.next();
+ if (entry instanceof ClientMediaEntry) {
+ return new AtomResource(collection, (ClientMediaEntry)entry);
+ } else {
+ return new AtomEntry(collection, entry);
+ }
+ } catch (Exception e) {
+ logger.error("ERROR fetching entry", e);
+ }
+ return null;
+ }
+
+ /**
+ * Remove is not supported.
+ */
+ public void remove() {
+ // optional method, not implemented
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomResource.java b/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomResource.java
new file mode 100644
index 0000000..455cb9e
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/blogclient/atomprotocol/AtomResource.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2007 Dave Johnson (Blogapps project)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.blogclient.atomprotocol;
+
+import java.io.InputStream;
+
+import com.sun.syndication.propono.blogclient.BlogClientException;
+import com.sun.syndication.propono.blogclient.BlogEntry;
+import com.sun.syndication.propono.blogclient.BlogResource;
+import com.sun.syndication.feed.atom.Link;
+import com.sun.syndication.propono.atom.client.ClientAtomService;
+import com.sun.syndication.propono.atom.client.ClientCollection;
+import com.sun.syndication.propono.atom.client.ClientEntry;
+import com.sun.syndication.propono.atom.client.ClientMediaEntry;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Atom protocol implementation of BlogResource.
+ */
+public class AtomResource extends AtomEntry implements BlogResource {
+ private AtomCollection collection;
+ private byte[] bytes;
+
+ AtomResource(AtomCollection collection, String name, String contentType, byte[] bytes)
+ throws BlogClientException {
+ super((AtomBlog)collection.getBlog(), collection);
+ this.collection = collection;
+ this.bytes = bytes;
+ BlogEntry.Content rcontent = new BlogEntry.Content();
+ rcontent.setType(contentType);
+ setContent(rcontent);
+ }
+
+ AtomResource(AtomCollection collection, ClientMediaEntry entry)
+ throws BlogClientException {
+ super(collection, entry);
+ }
+
+ AtomResource(AtomBlog blog, ClientMediaEntry entry) throws BlogClientException {
+ super(blog, entry);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getName() {
+ return getTitle();
+ }
+
+ byte[] getBytes() {
+ return bytes;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public InputStream getAsStream() throws BlogClientException {
+ try {
+ return null; //((ClientMediaEntry)clientEntry).getAsStream();
+ } catch (Exception e) {
+ throw new BlogClientException("Error creating entry", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void save() throws BlogClientException {
+ try {
+ if (getToken() == null) {
+ ClientAtomService clientService = ((AtomBlog)getBlog()).getService();
+ ClientCollection clientCollection = collection.getClientCollection();
+
+ ClientMediaEntry clientEntry =
+ new ClientMediaEntry(clientService, clientCollection, getTitle(),
+ "", getContent().getType(), getBytes());
+
+ copyToRomeEntry(clientEntry);
+ collection.getClientCollection().addEntry(clientEntry);
+ this.editURI = clientEntry.getEditURI();
+
+ } else {
+ ClientAtomService clientService = ((AtomBlog)getBlog()).getService();
+ ClientMediaEntry clientEntry = (ClientMediaEntry)clientService.getEntry(editURI);
+ clientEntry.update();
+ }
+ } catch (Exception e) {
+ throw new BlogClientException("Error creating entry", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void update(byte[] newBytes) throws BlogClientException {
+ try {
+ //((ClientMediaEntry)clientEntry).setBytes(newBytes);
+ //clientEntry.update();
+ } catch (Exception e) {
+ throw new BlogClientException("Error creating entry", e);
+ }
+ }
+
+ void copyFromRomeEntry(ClientEntry entry) {
+ super.copyFromRomeEntry(entry);
+
+ List links = entry.getOtherLinks();
+ if (links != null) {
+ for (Iterator iter = links.iterator(); iter.hasNext();) {
+ Link link = (Link)iter.next();
+ if ("edit-media".equals(link.getRel())) {
+ id = link.getHrefResolved();
+ break;
+ }
+ }
+ }
+
+
+ }
+}
diff --git a/src/main/java/com/sun/syndication/propono/blogclient/blogclient-diagram.gif b/src/main/java/com/sun/syndication/propono/blogclient/blogclient-diagram.gif
new file mode 100644
index 0000000..35dac52
Binary files /dev/null and b/src/main/java/com/sun/syndication/propono/blogclient/blogclient-diagram.gif differ
diff --git a/src/main/java/com/sun/syndication/propono/blogclient/metaweblog/MetaWeblogBlog.java b/src/main/java/com/sun/syndication/propono/blogclient/metaweblog/MetaWeblogBlog.java
new file mode 100644
index 0000000..5c531b3
--- /dev/null
+++ b/src/main/java/com/sun/syndication/propono/blogclient/metaweblog/MetaWeblogBlog.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright 2007 Dave Johnson (Blogapps project)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.syndication.propono.blogclient.metaweblog;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Iterator;
+import com.sun.syndication.propono.blogclient.BlogEntry;
+import com.sun.syndication.propono.blogclient.Blog;
+import com.sun.syndication.propono.blogclient.BlogClientException;
+import com.sun.syndication.propono.blogclient.BlogResource;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+import org.apache.xmlrpc.client.XmlRpcClient;
+import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
+
+/**
+ * Blog implementation that uses a mix of Blogger and MetaWeblog API methods.
+ */
+public class MetaWeblogBlog implements Blog {
+ private String blogid;
+ private String name;
+ private URL url;
+ private String userName;
+ private String password;
+ private String appkey = "dummy";
+ private Map collections;
+
+ private XmlRpcClient xmlRpcClient = null;
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getName() { return name; }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getToken() { return blogid; }
+
+ /**
+ * String representation of blog, returns the name.
+ */
+ public String toString() { return getName(); }
+
+ private XmlRpcClient getXmlRpcClient() {
+
+ if (xmlRpcClient == null) {
+ XmlRpcClientConfigImpl xmlrpcConfig = new XmlRpcClientConfigImpl();
+ xmlrpcConfig.setServerURL(url);
+ xmlRpcClient = new XmlRpcClient();
+ xmlRpcClient.setConfig(xmlrpcConfig);
+ }
+ return xmlRpcClient;
+ }
+
+ MetaWeblogBlog(String blogid, String name,
+ URL url, String userName, String password) {
+ this.blogid = blogid;
+ this.name = name;
+ this.url = url;
+ this.userName = userName;
+ this.password = password;
+ this.collections = new TreeMap();
+ collections.put("entries",
+ new MetaWeblogBlogCollection(this, "entries", "Entries", "entry"));
+ collections.put("resources",
+ new MetaWeblogBlogCollection(this, "resources", "Resources", "*"));
+ }
+
+ MetaWeblogBlog(String blogId, String name,
+ URL url, String userName, String password, String appkey) {
+ this(blogId, name, url, userName, password);
+ this.appkey = appkey;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public BlogEntry newEntry() {
+ return new MetaWeblogEntry(this, new HashMap());
+ }
+
+ String saveEntry(BlogEntry entry) throws BlogClientException {
+ Blog.Collection col = (Blog.Collection)collections.get("entries");
+ return col.saveEntry(entry);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public BlogEntry getEntry(String id) throws BlogClientException {
+ try {
+ Map result = (Map)
+ getXmlRpcClient().execute("metaWeblog.getPost", new Object[] {id, userName, password});
+ return new MetaWeblogEntry(this, result);
+ } catch (Exception e) {
+ throw new BlogClientException("ERROR: XML-RPC error getting entry", e);
+ }
+ }
+
+ void deleteEntry(String id) throws BlogClientException {
+ try {
+ getXmlRpcClient().execute("blogger.deletePost",
+ new Object[] {appkey, id, userName, password, Boolean.FALSE});
+ } catch (Exception e) {
+ throw new BlogClientException("ERROR: XML-RPC error getting entry", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Iterator getEntries() throws BlogClientException {
+ return new EntryIterator();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public BlogResource newResource(String name, String contentType, byte[] bytes) throws BlogClientException {
+ return new MetaWeblogResource(this, name, contentType, bytes);
+ }
+
+ String saveResource(MetaWeblogResource resource) throws BlogClientException {
+ Blog.Collection col = (Blog.Collection)collections.get("resources");
+ return col.saveResource(resource);
+ }
+
+ BlogResource getResource(String token) throws BlogClientException {
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Iterator getResources() throws BlogClientException {
+ return new NoOpIterator();
+ }
+
+ void deleteResource(BlogResource resource) throws BlogClientException {
+ // no-op
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public List getCategories() throws BlogClientException {
+
+ ArrayList ret = new ArrayList();
+ try {
+ Object result =
+ getXmlRpcClient().execute ("metaWeblog.getCategories",
+ new Object[] {blogid, userName, password});
+ if (result != null && result instanceof HashMap) {
+ // Standard MetaWeblog API style: struct of struts
+ Map catsmap = (Map)result;
+ Iterator keys = catsmap.keySet().iterator();
+ while (keys.hasNext()) {
+ String key = (String)keys.next();
+ Map catmap = (Map)catsmap.get(key);
+ BlogEntry.Category category = new BlogEntry.Category(key);
+ category.setName((String)catmap.get("description"));
+ // catmap.get("htmlUrl");
+ // catmap.get("rssUrl");
+ ret.add(category);
+ }
+ } else if (result != null && result instanceof Object[]) {
+ // Wordpress style: array of structs
+ Object[] resultArray = (Object[])result;
+ for (int i=0; i 0) {
+ List catArray = new ArrayList();
+ List cats = getCategories();
+ for (int i=0; i Integer.MAX_VALUE) {
+ // File is too large
+ }
+
+ // Create the byte array to hold the data
+ byte[] bytes = new byte[(int)length];
+
+ // Read in the bytes
+ int offset = 0;
+ int numRead = 0;
+ while (offset < bytes.length
+ && (numRead=is.read(bytes, offset, bytes.length-offset)) >= 0) {
+ offset += numRead;
+ }
+
+ // Ensure all the bytes have been read in
+ if (offset < bytes.length) {
+ throw new IOException("Could not completely read file "+file.getName());
+ }
+
+ // Close the input stream and return bytes
+ is.close();
+ return bytes;
+ }
+
+ /**
+ * Read input from stream and into string.
+ */
+ public static String streamToString(InputStream is) throws IOException {
+ StringBuffer sb = new StringBuffer();
+ BufferedReader in = new BufferedReader(new InputStreamReader(is));
+ String line;
+ while ((line = in.readLine()) != null) {
+ sb.append(line);
+ sb.append(LS);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Copy input stream to output stream using 8K buffer.
+ */
+ public static void copyInputToOutput(
+ InputStream input,
+ OutputStream output)
+ throws IOException {
+ BufferedInputStream in = new BufferedInputStream(input);
+ BufferedOutputStream out = new BufferedOutputStream(output);
+ byte buffer[] = new byte[8192];
+ for (int count = 0; count != -1;) {
+ count = in.read(buffer, 0, 8192);
+ if (count != -1)
+ out.write(buffer, 0, count);
+ }
+
+ try {
+ in.close();
+ out.close();
+ } catch (IOException ex) {
+ throw new IOException("Closing file streams, " + ex.getMessage());
+ }
+ }
+
+
+ /**
+ * Replaces occurences of non-alphanumeric characters with a supplied char.
+ */
+ public static String replaceNonAlphanumeric(String str, char subst) {
+ StringBuffer ret = new StringBuffer(str.length());
+ char[] testChars = str.toCharArray();
+ for (int i = 0; i < testChars.length; i++) {
+ if (Character.isLetterOrDigit(testChars[i])) {
+ ret.append(testChars[i]);
+ } else {
+ ret.append( subst );
+ }
+ }
+ return ret.toString();
+ }
+
+ /**
+ * Convert string to string array.
+ */
+ public static String[] stringToStringArray(String instr, String delim)
+ throws NoSuchElementException, NumberFormatException {
+ StringTokenizer toker = new StringTokenizer(instr, delim);
+ String stringArray[] = new String[toker.countTokens()];
+ int i = 0;
+ while (toker.hasMoreTokens()) {
+ stringArray[i++] = toker.nextToken();
+ }
+ return stringArray;
+ }
+
+ /**
+ * Convert string array to string.
+ */
+ public static String stringArrayToString(String[] stringArray, String delim) {
+ String ret = "";
+ for (int i = 0; i < stringArray.length; i++) {
+ if (ret.length() > 0)
+ ret = ret + delim + stringArray[i];
+ else
+ ret = stringArray[i];
+ }
+ return ret;
+ }
+
+
+ static Pattern absoluteURIPattern = Pattern.compile("^[a-z0-9]*:.*$");
+
+ private static boolean isAbsoluteURI(String uri) {
+ return absoluteURIPattern.matcher(uri).find();
+ }
+
+ private static boolean isRelativeURI(String uri) {
+ return !isAbsoluteURI(uri);
+ }
+
+ /**
+ * }
+ * Resolve URI based considering xml:base and baseURI.
+ * @param baseURI Base URI of feed
+ * @param parent Parent from which to consider xml:base
+ * @param url URL to be resolved
+ */
+ private static String resolveURI(String baseURI, Parent parent, String url) {
+ if (isRelativeURI(url)) {
+ url = (!".".equals(url) && !"./".equals(url)) ? url : "";
+
+ // Relative URI with parent
+ if (parent != null && parent instanceof Element) {
+
+ // Do we have an xml:base?
+ String xmlbase = ((Element)parent).getAttributeValue(
+ "base", Namespace.XML_NAMESPACE);
+ if (xmlbase != null && xmlbase.trim().length() > 0) {
+ if (isAbsoluteURI(xmlbase)) {
+ // Absolute xml:base, so form URI right now
+ if (url.startsWith("/")) {
+ // Host relative URI
+ int slashslash = xmlbase.indexOf("//");
+ int nextslash = xmlbase.indexOf("/", slashslash + 2);
+ if (nextslash != -1) xmlbase = xmlbase.substring(0, nextslash);
+ return formURI(xmlbase, url);
+ }
+ if (!xmlbase.endsWith("/")) {
+ // Base URI is filename, strip it off
+ xmlbase = xmlbase.substring(0, xmlbase.lastIndexOf("/"));
+ }
+ return formURI(xmlbase, url);
+ } else {
+ // Relative xml:base, so walk up tree
+ return resolveURI(baseURI, parent.getParent(),
+ stripTrailingSlash(xmlbase) + "/"+ stripStartingSlash(url));
+ }
+ }
+ // No xml:base so walk up tree
+ return resolveURI(baseURI, parent.getParent(), url);
+
+ // Relative URI with no parent (i.e. top of tree), so form URI right now
+ } else if (parent == null || parent instanceof Document) {
+ return formURI(baseURI, url);
+ }
+ }
+ return url;
+ }
+
+ /**
+ * Form URI by combining base with append portion and giving
+ * special consideration to append portions that begin with ".."
+ * @param base Base of URI, may end with trailing slash
+ * @param append String to append, may begin with slash or ".."
+ */
+ private static String formURI(String base, String append) {
+ base = stripTrailingSlash(base);
+ append = stripStartingSlash(append);
+ if (append.startsWith("..")) {
+ String ret = null;
+ String[] parts = append.split("/");
+ for (int i=0; i 0);
+ for (Iterator it = service.getWorkspaces().iterator(); it.hasNext();) {
+ ClientWorkspace space = (ClientWorkspace) it.next();
+ assertNotNull(space.getTitle());
+ log.debug("Workspace: " + space.getTitle());
+ for (Iterator colit = space.getCollections().iterator(); colit.hasNext();) {
+ ClientCollection col = (ClientCollection) colit.next();
+ log.debug(" Collection: " + col.getTitle() + " Accepts: " + col.getAccepts());
+ log.debug(" href: " + col.getHrefResolved());
+ assertNotNull(col.getTitle());
+ }
+ }
+ }
+
+ /**
+ * Tests that entries can be posted and removed in all collections that
+ * accept entries. Fails if no collections found that accept entries.
+ */
+ public void testSimpleEntryPostAndRemove() throws Exception {
+ assertNotNull(service);
+ assertTrue(service.getWorkspaces().size() > 0);
+ int count = 0;
+ for (Iterator it = service.getWorkspaces().iterator(); it.hasNext();) {
+ ClientWorkspace space = (ClientWorkspace) it.next();
+ assertNotNull(space.getTitle());
+
+ for (Iterator colit = space.getCollections().iterator(); colit.hasNext();) {
+ ClientCollection col = (ClientCollection) colit.next();
+ if (col.accepts(Collection.ENTRY_TYPE)) {
+
+ // we found a collection that accepts entries, so post one
+ ClientEntry m1 = col.createEntry();
+ m1.setTitle("Test post");
+ Content c = new Content();
+ c.setValue("This is a test post");
+ c.setType("html");
+ m1.setContent(c);
+
+ col.addEntry(m1);
+
+ // entry should now exist on server
+ ClientEntry m2 = col.getEntry(m1.getEditURI());
+ assertNotNull(m2);
+
+ // remove entry
+ m2.remove();
+
+ // fetching entry now should result in exception
+ boolean failed = false;
+ try {
+ col.getEntry(m1.getEditURI());
+ } catch (ProponoException e) {
+ failed = true;
+ }
+ assertTrue(failed);
+ count++;
+ }
+ }
+ }
+ assertTrue(count > 0);
+ }
+
+ /**
+ * Tests that entries can be posted, updated and removed in all collections that
+ * accept entries. Fails if no collections found that accept entries.
+ */
+ public void testSimpleEntryPostUpdateAndRemove() throws Exception {
+ assertNotNull(service);
+ assertTrue(service.getWorkspaces().size() > 0);
+ int count = 0;
+ for (Iterator it = service.getWorkspaces().iterator(); it.hasNext();) {
+ ClientWorkspace space = (ClientWorkspace) it.next();
+ assertNotNull(space.getTitle());
+
+ for (Iterator colit = space.getCollections().iterator(); colit.hasNext();) {
+ ClientCollection col = (ClientCollection) colit.next();
+ if (col.accepts(Collection.ENTRY_TYPE)) {
+
+ // we found a collection that accepts entries, so post one
+ ClientEntry m1 = col.createEntry();
+ m1.setTitle(col.getTitle() + ": Test post");
+ Content c = new Content();
+ c.setValue("This is a test post");
+ c.setType("html");
+ m1.setContent(c);
+
+ col.addEntry(m1);
+
+ // entry should now exist on server
+ ClientEntry m2 = (ClientEntry)col.getEntry(m1.getEditURI());
+ assertNotNull(m2);
+
+ m2.setTitle(col.getTitle() + ": Updated title");
+ m2.update();
+
+ // entry should now be updated on server
+ ClientEntry m3 = (ClientEntry)col.getEntry(m1.getEditURI());
+ assertEquals(col.getTitle() + ": Updated title", m3.getTitle());
+
+ // remove entry
+ m3.remove();
+
+ // fetching entry now should result in exception
+ boolean failed = false;
+ try {
+ col.getEntry(m1.getEditURI());
+ } catch (ProponoException e) {
+ failed = true;
+ }
+ assertTrue(failed);
+ count++;
+ }
+ }
+ }
+ assertTrue(count > 0);
+ }
+
+ public void testFindWorkspace() throws Exception {
+ assertNotNull(service);
+ ClientWorkspace ws = (ClientWorkspace)service.findWorkspace("adminblog");
+ if (ws != null) {
+ ClientCollection col = (ClientCollection)ws.findCollection(null, "entry");
+ ClientEntry entry = col.createEntry();
+ entry.setTitle("NPE on submitting order query");
+ entry.setContent("This is a bad one!", Content.HTML);
+ col.addEntry(entry);
+
+ // entry should now exist on server
+ ClientEntry saved = (ClientEntry)col.getEntry(entry.getEditURI());
+ assertNotNull(saved);
+
+ // remove entry
+ saved.remove();
+
+ // fetching entry now should result in exception
+ boolean failed = false;
+ try {
+ col.getEntry(saved.getEditURI());
+ } catch (ProponoException e) {
+ failed = true;
+ }
+ assertTrue(failed);
+ }
+ }
+
+ /**
+ * Test posting an entry to every available collection with a fixed and
+ * an unfixed category if server support allows, then cleanup.
+ */
+ public void testEntryPostWithCategories() throws Exception {
+ assertNotNull(service);
+ assertTrue(service.getWorkspaces().size() > 0);
+ int count = 0;
+ for (Iterator it = service.getWorkspaces().iterator(); it.hasNext();) {
+ ClientWorkspace space = (ClientWorkspace) it.next();
+ assertNotNull(space.getTitle());
+
+ for (Iterator colit = space.getCollections().iterator(); colit.hasNext();) {
+ ClientCollection col = (ClientCollection) colit.next();
+ if (col.accepts(Collection.ENTRY_TYPE)) {
+
+ // we found a collection that accepts GIF, so post one
+ ClientEntry m1 = col.createEntry();
+ m1.setTitle("Test post");
+ Content c = new Content();
+ c.setValue("This is a test post");
+ c.setType("html");
+ m1.setContent(c);
+
+ // if possible, pick one fixed an un unfixed category
+ Category fixedCat = null;
+ Category unfixedCat = null;
+ List entryCats = new ArrayList();
+ for (int i=0; i 0);
+ }
+
+ /**
+ * Post media entry to every media colletion avialable on server, then cleanup.
+ */
+ public void testMediaPost() throws Exception {
+ assertNotNull(service);
+ assertTrue(service.getWorkspaces().size() > 0);
+ int count = 0;
+ for (Iterator it = service.getWorkspaces().iterator(); it.hasNext();) {
+ ClientWorkspace space = (ClientWorkspace) it.next();
+ assertNotNull(space.getTitle());
+
+ for (Iterator colit = space.getCollections().iterator(); colit.hasNext();) {
+ ClientCollection col = (ClientCollection) colit.next();
+ if (col.accepts("image/gif")) {
+
+ // we found a collection that accepts GIF, so post one
+ ClientMediaEntry m1 = col.createMediaEntry("duke"+count, "duke"+count, "image/gif",
+ new FileInputStream("test/testdata/duke-wave-shadow.gif"));
+ col.addEntry(m1);
+
+ // entry should now exist on server
+ ClientMediaEntry m2 = (ClientMediaEntry)col.getEntry(m1.getEditURI());
+ assertNotNull(m2);
+
+ // remove entry
+ m2.remove();
+
+ // fetching entry now should result in exception
+ boolean failed = false;
+ try {
+ col.getEntry(m1.getEditURI());
+ } catch (ProponoException e) {
+ failed = true;
+ }
+ assertTrue(failed);
+ count++;
+ }
+ }
+ }
+ assertTrue(count > 0);
+ }
+
+ /**
+ * Post X media entries each media collection found, test paging, then cleanup.
+ *
+ public void testMediaPaging() throws Exception {
+ ClientAtomService service = getClientAtomService();
+ assertNotNull(service);
+ assertTrue(service.getWorkspaces().size() > 0);
+ int count = 0;
+ for (Iterator it = service.getWorkspaces().iterator(); it.hasNext();) {
+ ClientWorkspace space = (ClientWorkspace) it.next();
+ assertNotNull(space.getTitle());
+
+ for (Iterator colit = space.getCollections().iterator(); colit.hasNext();) {
+ ClientCollection col = (ClientCollection) colit.next();
+ if (col.accepts("image/gif")) {
+
+ // we found a collection that accepts GIF, so post 100 of them
+ List posted = new ArrayList();
+ for (int i=0; i= maxPagingEntries);
+ count++;
+ break;
+ }
+ }
+ }
+ assertTrue(count > 0);
+ }*/
+}
+
+
diff --git a/src/test/java/com/sun/syndication/propono/atom/client/BloggerDotComTest.java b/src/test/java/com/sun/syndication/propono/atom/client/BloggerDotComTest.java
new file mode 100644
index 0000000..42b7ef1
--- /dev/null
+++ b/src/test/java/com/sun/syndication/propono/atom/client/BloggerDotComTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.sun.syndication.propono.atom.client;
+
+import com.sun.syndication.feed.atom.Content;
+import java.util.Iterator;
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+/**
+ * Simple APP test designed to run against Blogger.com.
+ */
+public class BloggerDotComTest extends TestCase {
+
+ private String collectionURI = "http://www.blogger.com/feeds/BLOGID/posts/default";
+ private String atomServiceURI= "http://www.blogger.com/feeds/default/blogs?alt=atom-service";
+ private String email = "EMAIL";
+ private String password = "PASSWORD";
+
+ public BloggerDotComTest(String testName) {
+ super(testName);
+ }
+
+ protected void setUp() throws Exception {
+ }
+
+ protected void tearDown() throws Exception {
+ }
+
+ public static Test suite() {
+ TestSuite suite = new TestSuite(BloggerDotComTest.class);
+ return suite;
+ }
+
+ /**
+ * Verify that server returns service document containing workspaces containing collections.
+ */
+ public void testGetEntries() throws Exception {
+
+ // no auth necessary for iterating through entries
+ ClientCollection col = AtomClientFactory.getCollection(collectionURI,
+ new GDataAuthStrategy(email, password, "blogger"));
+ assertNotNull(col);
+ int count = 0;
+ for (Iterator it = col.getEntries(); it.hasNext();) {
+ ClientEntry entry = (ClientEntry) it.next();
+ assertNotNull(entry);
+ count++;
+ }
+ assertTrue(count > 0);
+
+ col = AtomClientFactory.getCollection(collectionURI,
+ new GDataAuthStrategy(email, password, "blogger"));
+ ClientEntry p1 = col.createEntry();
+ p1.setTitle("Propono post");
+ Content c = new Content();
+ c.setValue("This is content from ROME Propono");
+ p1.setContent(c);
+ col.addEntry(p1);
+
+ ClientEntry p2 = col.getEntry(p1.getEditURI());
+ assertNotNull(p2);
+
+
+ ClientAtomService atomService = AtomClientFactory.getAtomService(
+ collectionURI, new GDataAuthStrategy(email, password, "blogger"));
+ assertNotNull(atomService);
+
+ }
+}
+
+
diff --git a/src/test/java/com/sun/syndication/propono/atom/common/AtomServiceTest.java b/src/test/java/com/sun/syndication/propono/atom/common/AtomServiceTest.java
new file mode 100644
index 0000000..ef9d54a
--- /dev/null
+++ b/src/test/java/com/sun/syndication/propono/atom/common/AtomServiceTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.sun.syndication.propono.atom.common;
+
+import com.sun.syndication.feed.atom.Category;
+import java.io.FileInputStream;
+import java.util.Iterator;
+import junit.framework.*;
+import org.jdom.Document;
+import org.jdom.Element;
+import org.jdom.input.SAXBuilder;
+
+/**
+ * Tests reading and writing of service document, no server needed.
+ */
+public class AtomServiceTest extends TestCase {
+
+ public AtomServiceTest(String testName) {
+ super(testName);
+ }
+
+ protected void setUp() throws Exception {
+ }
+
+ protected void tearDown() throws Exception {
+ }
+
+ public static Test suite() {
+ TestSuite suite = new TestSuite(AtomServiceTest.class);
+
+ return suite;
+ }
+
+ /**
+ * Test of documentToService method, of class AtomService.
+ */
+ public void testDocumentToService() {
+ try {
+ // Load service document from disk
+ SAXBuilder builder = new SAXBuilder();
+ Document document = builder.build(new FileInputStream("test/testdata/servicedoc1.xml"));
+ assertNotNull(document);
+ AtomService service = AtomService.documentToService(document);
+
+ int workspaceCount = 0;
+
+ // Verify that service contains expected workspaces, collections and categories
+ for (Iterator it = service.getWorkspaces().iterator(); it.hasNext();) {
+ Workspace space = (Workspace)it.next();
+ assertNotNull(space.getTitle());
+ workspaceCount++;
+ int collectionCount = 0;
+
+ for (Iterator colit = space.getCollections().iterator(); colit.hasNext();) {
+ Collection col = (Collection)colit.next();
+ assertNotNull(col.getTitle());
+ assertNotNull(col.getHrefResolved());
+ collectionCount++;
+ int catCount = 0;
+ if (col.getCategories().size() > 0) {
+ for (Iterator catsit = col.getCategories().iterator(); catsit.hasNext();) {
+ Categories cats = (Categories) catsit.next();
+ for (Iterator catit = cats.getCategories().iterator(); catit.hasNext();) {
+ Category cat = (Category) catit.next();
+ catCount++;
+ }
+ assertTrue(catCount > 0);
+ }
+ }
+ }
+ }
+
+ assertTrue(workspaceCount > 0);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail();
+ }
+ }
+
+ /**
+ * Test of documentToService method, of class AtomService.
+ */
+ public void testServiceToDocument() {
+ try {
+ // Create service with workspace and collections
+ AtomService service = new AtomService();
+
+ Workspace workspace1 = new Workspace("workspace1", null);
+ Workspace workspace2 = new Workspace("workspace1", null);
+ service.addWorkspace(workspace1);
+ service.addWorkspace(workspace2);
+
+ Collection collection11 =
+ new Collection("collection11", null, "http://example.com/app/col11");
+ Collection collection12 =
+ new Collection("collection12", null, "http://example.com/app/col12");
+ workspace1.addCollection(collection11);
+ workspace1.addCollection(collection12);
+
+ Collection collection21 =
+ new Collection("collection21", null, "http://example.com/app/col21");
+ Collection collection22 =
+ new Collection("collection22", null, "http://example.com/app/col22");
+ workspace2.addCollection(collection21);
+ workspace2.addCollection(collection22);
+
+ // TODO: add categories at collection level
+
+ // Convert to JDOM document
+ Document document = service.serviceToDocument();
+
+ // verify that JDOM document contains service, workspace and collection
+ assertEquals("service", document.getRootElement().getName());
+ int workspaceCount = 0;
+ for (Iterator spaceit = document.getRootElement().getChildren().iterator(); spaceit.hasNext();) {
+ Element elem = (Element) spaceit.next();
+ if ("workspace".equals(elem.getName())) {
+ workspaceCount++;
+ }
+ boolean workspaceTitle = false;
+ int collectionCount = 0;
+ for (Iterator colit = elem.getChildren().iterator(); colit.hasNext();) {
+ Element colelem = (Element) colit.next();
+ if ("title".equals(colelem.getName())) {
+ workspaceTitle = true;
+ } else if ("collection".equals(colelem.getName())){
+ collectionCount++;
+ }
+
+ // TODO: test for categories at the collection level
+ }
+ assertTrue(workspaceTitle);
+ assertTrue(collectionCount > 0);
+ }
+ assertTrue(workspaceCount > 0);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail();
+ }
+ }
+}
diff --git a/src/test/java/com/sun/syndication/propono/atom/common/CollectionTest.java b/src/test/java/com/sun/syndication/propono/atom/common/CollectionTest.java
new file mode 100644
index 0000000..969731f
--- /dev/null
+++ b/src/test/java/com/sun/syndication/propono/atom/common/CollectionTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.sun.syndication.propono.atom.common;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import junit.framework.*;
+
+/**
+ * Tests Collection class, no server needed.
+ */
+public class CollectionTest extends TestCase {
+
+ public CollectionTest(String testName) {
+ super(testName);
+ }
+
+ protected void setUp() throws Exception {
+ }
+
+ protected void tearDown() throws Exception {
+ }
+
+ /**
+ * Test of accepts method, of class com.sun.syndication.propono.atom.common.Collection.
+ */
+ public void testAccepts() {
+
+ Collection col =
+ new Collection("dummy_title","dummy_titletype","dummy_href");
+
+ col.setAccepts(Collections.singletonList("image/*"));
+ assertTrue(col.accepts("image/gif"));
+ assertTrue(col.accepts("image/jpg"));
+ assertTrue(col.accepts("image/png"));
+ assertFalse(col.accepts("test/html"));
+
+ List accepts = new ArrayList();
+ accepts.add("image/*");
+ accepts.add("text/*");
+ col.setAccepts(accepts);
+ assertTrue(col.accepts("image/gif"));
+ assertTrue(col.accepts("image/jpg"));
+ assertTrue(col.accepts("image/png"));
+ assertTrue(col.accepts("text/html"));
+
+ col.setAccepts(Collections.singletonList("*/*"));
+ assertTrue(col.accepts("image/gif"));
+ assertTrue(col.accepts("image/jpg"));
+ assertTrue(col.accepts("image/png"));
+ assertTrue(col.accepts("text/html"));
+ }
+}
diff --git a/src/test/java/com/sun/syndication/propono/atom/server/AtomClientServerTest.java b/src/test/java/com/sun/syndication/propono/atom/server/AtomClientServerTest.java
new file mode 100644
index 0000000..4179cb8
--- /dev/null
+++ b/src/test/java/com/sun/syndication/propono/atom/server/AtomClientServerTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.sun.syndication.propono.atom.server;
+
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import junit.framework.Test;
+import junit.framework.TestSuite;
+import org.mortbay.http.HttpContext;
+import org.mortbay.http.HttpServer;
+import org.mortbay.http.SocketListener;
+import org.mortbay.jetty.servlet.ServletHandler;
+
+
+/**
+ * Test Propono Atom Client against Atom Server via Jetty. Extends
+ * AtomClientTest to start Jetty server, run tests and then stop
+ * the Jetty server.
+ */
+public class AtomClientServerTest { // extends AtomClientTest {
+
+ private HttpServer server;
+ public static final int TESTPORT = 8283;
+ public static final String ENDPOINT = "http://localhost:" + TESTPORT + "/rome/app";
+ public static final String USERNAME = "admin";
+ public static final String PASSWORD = "admin";
+
+ public AtomClientServerTest(String s) {
+ //super(s);
+ }
+
+ public String getEndpoint() {
+ return ENDPOINT;
+ }
+
+ public String getUsername() {
+ return USERNAME;
+ }
+
+ public String getPassword() {
+ return PASSWORD;
+ }
+
+ public static Test suite() {
+ TestSuite suite = new TestSuite(AtomClientServerTest.class);
+ return suite;
+ }
+
+ protected HttpServer getServer() {
+ return server;
+ }
+
+ protected void setUp() throws Exception {
+ ConsoleHandler handler = new ConsoleHandler();
+ Logger logger = Logger.getLogger("com.sun.syndication.propono");
+ logger.setLevel(Level.FINEST);
+ logger.addHandler(handler);
+
+ setupServer();
+ HttpContext context = createContext();
+ ServletHandler servlets = createServletHandler();
+ context.addHandler(servlets);
+ server.addContext(context);
+ server.start();
+ }
+
+ private void setupServer() throws InterruptedException {
+ // Create the server
+ if (server != null) {
+ server.stop();
+ server = null;
+ }
+ server = new HttpServer();
+
+ // Create a port listener
+ SocketListener listener = new SocketListener();
+ listener.setPort(TESTPORT);
+ server.addListener(listener);
+ }
+
+ private ServletHandler createServletHandler() {
+ System.setProperty(
+ "com.sun.syndication.propono.atom.server.AtomHandlerFactory",
+ "com.sun.syndication.propono.atom.server.TestAtomHandlerFactory");
+ ServletHandler servlets = new ServletHandler();
+ servlets.addServlet(
+ "app", "/app/*",
+ "com.sun.syndication.propono.atom.server.AtomServlet");
+ return servlets;
+ }
+
+ private HttpContext createContext() {
+ HttpContext context = new HttpContext();
+ context.setContextPath("/rome/*");
+ return context;
+ }
+
+ protected void tearDown() throws Exception {
+ if (server != null) {
+ server.stop();
+ server.destroy();
+ server = null;
+ }
+ }
+}
+
+
diff --git a/src/test/java/com/sun/syndication/propono/atom/server/TestAtomHandlerFactory.java b/src/test/java/com/sun/syndication/propono/atom/server/TestAtomHandlerFactory.java
new file mode 100644
index 0000000..ddf665e
--- /dev/null
+++ b/src/test/java/com/sun/syndication/propono/atom/server/TestAtomHandlerFactory.java
@@ -0,0 +1,27 @@
+/*
+ * 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.sun.syndication.propono.atom.server;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class TestAtomHandlerFactory extends AtomHandlerFactory {
+
+ public AtomHandler newAtomHandler(HttpServletRequest req, HttpServletResponse res) {
+ return new TestAtomHandlerImpl(req, "build/testuploaddir");
+ }
+}
diff --git a/src/test/java/com/sun/syndication/propono/atom/server/TestAtomHandlerImpl.java b/src/test/java/com/sun/syndication/propono/atom/server/TestAtomHandlerImpl.java
new file mode 100644
index 0000000..0fdec0e
--- /dev/null
+++ b/src/test/java/com/sun/syndication/propono/atom/server/TestAtomHandlerImpl.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2007 Sun Microsystems, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.sun.syndication.propono.atom.server;
+
+import com.sun.syndication.propono.atom.server.impl.FileBasedAtomHandler;
+import javax.servlet.http.HttpServletRequest;
+
+public class TestAtomHandlerImpl extends FileBasedAtomHandler {
+
+ public TestAtomHandlerImpl(HttpServletRequest req, String uploaddir) {
+ super(req, uploaddir);
+ }
+ public boolean validateUser( String loginId, String password ) {
+ return AtomClientServerTest.USERNAME.equals(loginId)
+ && AtomClientServerTest.PASSWORD.equals(password);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/sun/syndication/propono/blogclient/SimpleBlogClientTest.java b/src/test/java/com/sun/syndication/propono/blogclient/SimpleBlogClientTest.java
new file mode 100644
index 0000000..910dcaf
--- /dev/null
+++ b/src/test/java/com/sun/syndication/propono/blogclient/SimpleBlogClientTest.java
@@ -0,0 +1,215 @@
+/*
+ * 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.sun.syndication.propono.blogclient;
+
+import com.sun.syndication.io.impl.Atom10Parser;
+import com.sun.syndication.propono.utils.Utilities;
+import java.io.File;
+import java.util.Iterator;
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+
+
+/**
+ * Tests Atom and MetaWeblog API CRUD via BlogClient.
+ * Exclude this from automated tests because it requires a live blog server.
+ */
+public class SimpleBlogClientTest extends TestCase {
+
+ private String metaweblogEndpoint = "http://localhost:8080/roller/roller-services/xmlrpc";
+ //private String atomEndpoint = "http://localhost:8080/roller/roller-services/app";
+ private String atomEndpoint = "http://localhost:8080/sample-atomserver/app";
+
+ private String endpoint = "http://localhost:8080/atom-fileserver/app";
+ private String username = "admin";
+ private String password = "admin";
+
+ public SimpleBlogClientTest(String testName) {
+ super(testName);
+ }
+
+ protected void setUp() throws Exception {
+ }
+
+ protected void tearDown() throws Exception {
+ }
+
+ public void testBlogClientAtom() throws Exception {
+ testBlogClient("atom", atomEndpoint);
+ }
+
+ public void testBlogClientMetaWeblog() throws Exception{
+ testBlogClient("metaweblog", metaweblogEndpoint);
+ }
+
+ public void testBlogClient(String type, String endpoint) throws Exception {
+ BlogConnection conn = BlogConnectionFactory
+ .getBlogConnection(type, endpoint, username, password);
+
+ int blogCount = 0;
+ for (Iterator it = conn.getBlogs().iterator(); it.hasNext();) {
+ Blog blog = (Blog) it.next();
+ System.out.println(blog.getName());
+ blogCount++;
+ }
+ assertTrue(blogCount > 0);
+ }
+
+ public void testPostAndDeleteAtom() throws Exception {
+ testPostAndDelete("atom", atomEndpoint);
+ }
+
+ public void testPostAndDeleteMetaWeblog() throws Exception {
+ testPostAndDelete("metaweblog", metaweblogEndpoint);
+ }
+
+ public void testMediaPostAtom() throws Exception {
+ testMediaPost("atom", atomEndpoint);
+ }
+
+ public void testMediaPostMetaWeblog() throws Exception {
+ testMediaPost("metaweblog", metaweblogEndpoint);
+ }
+
+ public void testPostAndDelete(String type, String endpoint) throws Exception {
+ BlogConnection conn = BlogConnectionFactory
+ .getBlogConnection(type, endpoint, username, password);
+ assertNotNull(conn);
+
+ String title1 = "Test content";
+ String content1 = "Test content";
+
+ Blog blog = (Blog)conn.getBlogs().get(0);
+ BlogEntry entry = blog.newEntry();
+ entry.setTitle(title1);
+ entry.setContent(new BlogEntry.Content(content1));
+ entry.save();
+ String token = entry.getToken();
+ assertNotNull(token);
+
+ entry = blog.getEntry(token);
+
+ assertEquals(title1, entry.getTitle());
+ assertEquals(content1, entry.getContent().getValue());
+
+ assertNotNull(entry);
+ entry.delete();
+ entry = null;
+
+ boolean notFound = false;
+ try {
+ entry = blog.getEntry(token);
+ } catch (Exception e) {
+ notFound = true;
+ }
+ assertTrue(notFound);
+ }
+
+ /**
+ * Post media entry to every media colletion avialable on server, then cleanup.
+ */
+ public void testMediaPost(String type, String endpoint) throws Exception {
+ BlogConnection conn = BlogConnectionFactory
+ .getBlogConnection(type, endpoint, username, password);
+ assertNotNull(conn);
+
+ assertTrue(conn.getBlogs().size() > 0);
+ int count = 0;
+ for (Iterator it = conn.getBlogs().iterator(); it.hasNext();) {
+ Blog blog = (Blog) it.next();
+ assertNotNull(blog.getName());
+
+ for (Iterator colit = blog.getCollections().iterator(); colit.hasNext();) {
+ Blog.Collection col = (Blog.Collection) colit.next();
+ if (col.accepts("image/gif")) {
+
+ // we found a collection that accepts GIF, so post one
+ BlogResource m1 = col.newResource("duke"+count, "image/gif",
+ Utilities.getBytesFromFile(new File("test/testdata/duke-wave-shadow.gif")));
+ col.saveResource(m1);
+
+ if ("atom".equals(type)) { // additional tests for Atom
+
+ // entry should now exist on server
+ BlogResource m2 = (BlogResource)blog.getEntry(m1.getToken());
+ assertNotNull(m2);
+
+ // remove entry
+ m2.delete();
+
+ // fetching entry now should result in exception
+ boolean failed = false;
+ try {
+ blog.getEntry(m1.getToken());
+ } catch (Exception e) {
+ failed = true;
+ }
+ assertTrue(failed);
+ }
+ count++;
+ }
+ }
+ }
+ assertTrue(count > 0);
+ }
+
+
+ public void testEntryIterationAtom() throws Exception {
+ testEntryIteration("atom", atomEndpoint);
+ }
+
+ public void testEntryIterationMetaWeblog() throws Exception {
+ testEntryIteration("metaweblog", metaweblogEndpoint);
+ }
+
+ public void testEntryIteration(String type, String endpoint) throws Exception {
+ BlogConnection conn = BlogConnectionFactory
+ .getBlogConnection(type, endpoint, username, password);
+ assertNotNull(conn);
+
+ String title1 = "Test content";
+ String content1 = "Test content";
+
+ Blog blog = (Blog)conn.getBlogs().get(0);
+
+ for (int i=0; i<10; i++) {
+ BlogEntry entry = blog.newEntry();
+ entry.setTitle(title1);
+ entry.setContent(new BlogEntry.Content(content1));
+ entry.save();
+ String token = entry.getToken();
+ assertTrue(Atom10Parser.isAbsoluteURI(token));
+ assertNotNull(token);
+ }
+
+ for (Iterator it = blog.getEntries(); it.hasNext();) {
+ BlogEntry blogEntry = (BlogEntry)it.next();
+ assertTrue(Atom10Parser.isAbsoluteURI(blogEntry.getToken()));
+ blogEntry.delete();
+ }
+ }
+
+
+ public static Test suite() {
+ TestSuite suite = new TestSuite(SimpleBlogClientTest.class);
+ return suite;
+ }
+}
+
+
+
diff --git a/src/test/resources/commons-logging.properties b/src/test/resources/commons-logging.properties
new file mode 100644
index 0000000..a7bb501
--- /dev/null
+++ b/src/test/resources/commons-logging.properties
@@ -0,0 +1,16 @@
+#
+# 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.
+#
+org.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog
\ No newline at end of file
diff --git a/src/test/resources/duke-wave-shadow.gif b/src/test/resources/duke-wave-shadow.gif
new file mode 100644
index 0000000..1e5ac90
Binary files /dev/null and b/src/test/resources/duke-wave-shadow.gif differ
diff --git a/src/test/resources/servicedoc1.xml b/src/test/resources/servicedoc1.xml
new file mode 100644
index 0000000..57270ce
--- /dev/null
+++ b/src/test/resources/servicedoc1.xml
@@ -0,0 +1,18 @@
+
+
+
+ adminblog1
+
+ Weblog Entries
+
+
+
+
+ entry
+
+
+ Media Files
+ */*
+
+
+
\ No newline at end of file
diff --git a/src/test/resources/simplelog.properties b/src/test/resources/simplelog.properties
new file mode 100644
index 0000000..318d7fb
--- /dev/null
+++ b/src/test/resources/simplelog.properties
@@ -0,0 +1,19 @@
+# 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.
+
+org.apache.commons.logging.simplelog.log.com.sun.syndication.propono.atom.server.impl.FileBasedAtomHandler=debug
+
+org.apache.commons.logging.simplelog.log.com.sun.syndication.propono.atom.client.AtomClientTest=debug