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.

+ */ +public abstract class AtomHandlerFactory { + + private static Log log = + LogFactory.getFactory().getInstance(AtomHandlerFactory.class); + + private static final String DEFAULT_PROPERTY_NAME = + "com.sun.syndication.propono.atom.server.AtomHandlerFactory"; + + private static final String FALLBACK_IMPL_NAME = + "com.sun.syndication.propono.atom.server.impl.FileBasedAtomHandlerFactory"; + + /* + *

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. + *
  • + *
  • + * Platform default AtomHandlerFactory instance. + *
  • + *
+ * + * Once an application has obtained a reference to a AtomHandlerFactory + * it can use the factory to configure and obtain parser instances. + * + * @return New instance of a AtomHandlerFactory + * + * @throws FactoryConfigurationError if the implementation is not available + * or cannot be instantiated. + */ + public static AtomHandlerFactory newInstance() { + try { + return (AtomHandlerFactory) + FactoryFinder.find(DEFAULT_PROPERTY_NAME, FALLBACK_IMPL_NAME); + } catch (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: + * + *
+ *    # Define list of collections to be offered
+ *    propono.atomserver.filebased.collections=entries,gifimages
+ *
+ *    # Defines 'entries' collection, accepts entries
+ *    propono.atomserver.filebased.collection.entries.title=Entries
+ *    propono.atomserver.filebased.collection.entries.singular=entry
+ *    propono.atomserver.filebased.collection.entries.plural=entries
+ *    propono.atomserver.filebased.collection.entries.accept=application/atom+xml;type=entry
+ *    propono.atomserver.filebased.collection.entries.categories=general,category1,category2
+ *
+ *    # Defines 'gifimages' collection, accepts only GIF files
+ *    propono.atomserver.filebased.collection.gifimages.title=GIF Images
+ *    propono.atomserver.filebased.collection.gifimages.singular=gif
+ *    propono.atomserver.filebased.collection.gifimages.plural=gifs
+ *    propono.atomserver.filebased.collection.gifimages.accept=image/gif
+ *    propono.atomserver.filebased.collection.gifimages.categories=general,category1,category2
+ * 
+ * + * 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