diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e975ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.classpath +/.project +/.settings +/target \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6b0b127 --- /dev/null +++ b/LICENSE @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..fe019d6 --- /dev/null +++ b/NOTICE @@ -0,0 +1,26 @@ + +Portions of the Propono code are Copyright (C) 2007 Sun Microsystems, Inc. + +Sun Microsystems, Inc. +4150 Network Circle, Santa Clara, California 95054, U.S.A. +All rights reserved. + +U.S. Government Rights - Commercial software. Government users are subject to +the Sun Microsystems, Inc. standard license agreement and applicable provisions +of the FAR and its supplements. + +Use is subject to license terms. + +This distribution may include materials developed by third parties. + +Sun, Sun Microsystems, the Sun logo and Java are trademarks or registered +trademarks of Sun Microsystems, Inc. in the U.S. and other countries. + +This product is covered and controlled by U.S. Export Control laws and may be +subject to the export or import laws in other countries. Nuclear, missile, +chemical biological weapons or nuclear maritime end uses or end users, whether +direct or indirect, are strictly prohibited. Export or reexport to countries +subject to U.S. embargo or to entities identified on U.S. export exclusion +lists, including, but not limited to, the denied persons and specially +designated nationals lists is strictly prohibited. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..8a9f1a5 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +rome +==== + +ROME is a set of RSS and Atom Utilities for Java. It makes it easy to work in Java with most syndication formats: RSS 0.90, RSS 0.91 Netscape, RSS 0.91 Userland, RSS 0.92, RSS 0.93, RSS 0.94, RSS 1.0, RSS 2.0, Atom 0.3, Atom 1.0 + +More Information: http://rometools.github.io/rome-propono/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f4768e9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,122 @@ + + + 4.0.0 + + + com.rometools + rome-parent + 1.6.0-SNAPSHOT + + + rome-propono + 1.6.0-SNAPSHOT + jar + + rome-propono + + The ROME Propono subproject is a Java class library that + supports publishing protocols, specifically the Atom Publishing Protocol + and the legacy MetaWeblog API. Propono includes an Atom client library, + Atom server framework and a Blog client that supports both Atom protocol + and the MetaWeblog API. + + + http://rometools.github.io/rome-propono/ + + + scm:git:ssh://github.com/rometools/rome-propono.git + scm:git:ssh://git@github.com/rometools/rome-propono.git + https://github.com/rometools/rome-propono + + + + + Dave Johnson + http://rollerweblogger.org/roller + -5 + + + Robert Cooper + kebernet@gmail.com + http://www.screaming-penguin.com + -4 + + + + + + sonatype-nexus-snapshots + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + + + + + org.apache.maven.plugins + maven-scm-publish-plugin + + gh-pages + ${project.scm.developerConnection} + ${project.build.directory}/site + + + + + + + + com.rometools + rome + 1.6.0-SNAPSHOT + + + commons-httpclient + commons-httpclient + + + org.apache.commons + commons-lang3 + + + commons-beanutils + commons-beanutils + + + org.apache.xmlrpc + xmlrpc-client + + + net.oauth.core + oauth + + + javax.servlet + servlet-api + provided + + + ch.qos.logback + logback-classic + test + + + junit + junit + test + + + jetty + jetty + test + + + + diff --git a/src/main/java/com/rometools/propono/atom/client/AtomClientFactory.java b/src/main/java/com/rometools/propono/atom/client/AtomClientFactory.java new file mode 100644 index 0000000..281baaf --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/client/AtomClientFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.client; + +import com.rometools.propono.utils.ProponoException; +import com.rometools.rome.io.impl.Atom10Parser; + +/** + * Creates AtomService or ClientCollection based on username, password and end-point URI of Atom + * protocol service. + */ +public class AtomClientFactory { + + static { + Atom10Parser.setResolveURIs(true); + } + + /** + * Create AtomService by reading service doc from Atom Server. + */ + public static ClientAtomService getAtomService(final String uri, final AuthStrategy authStrategy) throws ProponoException { + return new ClientAtomService(uri, authStrategy); + } + + /** + * Create ClientCollection bound to URI. + */ + public static ClientCollection getCollection(final String uri, final AuthStrategy authStrategy) throws ProponoException { + return new ClientCollection(uri, authStrategy); + } +} diff --git a/src/main/java/com/rometools/propono/atom/client/AuthStrategy.java b/src/main/java/com/rometools/propono/atom/client/AuthStrategy.java new file mode 100644 index 0000000..c40ebd1 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/client/AuthStrategy.java @@ -0,0 +1,29 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.client; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpMethodBase; + +import com.rometools.propono.utils.ProponoException; + +public interface AuthStrategy { + + /** + * Add authentication credenticals, tokens, etc. to HTTP method + */ + void addAuthentication(HttpClient httpClient, HttpMethodBase method) throws ProponoException; +} diff --git a/src/main/java/com/rometools/propono/atom/client/BasicAuthStrategy.java b/src/main/java/com/rometools/propono/atom/client/BasicAuthStrategy.java new file mode 100644 index 0000000..8bf51e4 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/client/BasicAuthStrategy.java @@ -0,0 +1,42 @@ +/* + * Copyright 2009 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.client; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpMethodBase; + +import com.rometools.propono.utils.ProponoException; +import com.rometools.rome.io.impl.Base64; + +public class BasicAuthStrategy implements AuthStrategy { + private final String credentials; + + public BasicAuthStrategy(final String username, final String password) { + new Base64(); + credentials = new String(Base64.encode((username + ":" + password).getBytes())); + } + + public void init() throws ProponoException { + // op-op + } + + @Override + public void addAuthentication(final HttpClient httpClient, final HttpMethodBase method) throws ProponoException { + httpClient.getParams().setAuthenticationPreemptive(true); + final String header = "Basic " + credentials; + method.setRequestHeader("Authorization", header); + } +} diff --git a/src/main/java/com/rometools/propono/atom/client/ClientAtomService.java b/src/main/java/com/rometools/propono/atom/client/ClientAtomService.java new file mode 100644 index 0000000..77ca1ca --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/client/ClientAtomService.java @@ -0,0 +1,139 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.client; + +import java.io.InputStreamReader; +import java.util.List; +import java.util.Locale; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpMethodBase; +import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; +import org.apache.commons.httpclient.methods.GetMethod; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.input.SAXBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.rometools.propono.atom.common.AtomService; +import com.rometools.propono.utils.ProponoException; +import com.rometools.rome.feed.atom.Entry; +import com.rometools.rome.io.impl.Atom10Parser; + +/** + * This class models an Atom Publising Protocol Service Document. It extends the common + * {@link com.rometools.rome.propono.atom.common.Collection} class to add a getEntry() + * method and to return {@link com.rometools.rome.propono.atom.client.ClientWorkspace} objects + * instead of common {@link com.rometools.rome.propono.atom.common.Workspace}s. + */ +public class ClientAtomService extends AtomService { + + private static final Logger LOG = LoggerFactory.getLogger(ClientAtomService.class); + + private String uri = null; + private HttpClient httpClient = null; + private AuthStrategy authStrategy = null; + + /** + * Create Atom blog service instance for specified URL and user account. + * + * @param url End-point URL of Atom service + */ + ClientAtomService(final String uri, final AuthStrategy authStrategy) throws ProponoException { + this.uri = uri; + this.authStrategy = authStrategy; + final Document doc = getAtomServiceDocument(); + parseAtomServiceDocument(doc); + } + + /** + * Get full entry from service by entry edit URI. + */ + public ClientEntry getEntry(final String uri) throws ProponoException { + final GetMethod method = new GetMethod(uri); + authStrategy.addAuthentication(httpClient, method); + try { + httpClient.executeMethod(method); + if (method.getStatusCode() != 200) { + throw new ProponoException("ERROR HTTP status code=" + method.getStatusCode()); + } + final Entry romeEntry = Atom10Parser.parseEntry(new InputStreamReader(method.getResponseBodyAsStream()), uri, Locale.US); + if (!romeEntry.isMediaEntry()) { + return new ClientEntry(this, null, romeEntry, false); + } else { + return new ClientMediaEntry(this, null, romeEntry, false); + } + } catch (final Exception e) { + throw new ProponoException("ERROR: getting or parsing entry/media", e); + } finally { + method.releaseConnection(); + } + } + + void addAuthentication(final HttpMethodBase method) throws ProponoException { + authStrategy.addAuthentication(httpClient, method); + } + + AuthStrategy getAuthStrategy() { + return authStrategy; + } + + private Document getAtomServiceDocument() throws ProponoException { + GetMethod method = null; + final int code = -1; + try { + httpClient = new HttpClient(new MultiThreadedHttpConnectionManager()); + // TODO: make connection timeout configurable + httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(30000); + + method = new GetMethod(uri); + authStrategy.addAuthentication(httpClient, method); + httpClient.executeMethod(method); + + final SAXBuilder builder = new SAXBuilder(); + final String doc = method.getResponseBodyAsString(); + LOG.debug(doc); + return builder.build(method.getResponseBodyAsStream()); + + } catch (final Throwable t) { + final String msg = "ERROR retrieving Atom Service Document, code: " + code; + LOG.debug(msg, t); + throw new ProponoException(msg, t); + } finally { + if (method != null) { + method.releaseConnection(); + } + } + } + + /** Deserialize an Atom service XML document into an object */ + private void parseAtomServiceDocument(final Document document) throws ProponoException { + final Element root = document.getRootElement(); + final List spaces = root.getChildren("workspace", AtomService.ATOM_PROTOCOL); + for (final Element e : spaces) { + addWorkspace(new ClientWorkspace(e, this, uri)); + } + } + + /** + * Package access to httpClient. + */ + HttpClient getHttpClient() { + return httpClient; + } + +} diff --git a/src/main/java/com/rometools/propono/atom/client/ClientCategories.java b/src/main/java/com/rometools/propono/atom/client/ClientCategories.java new file mode 100644 index 0000000..229430d --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/client/ClientCategories.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package com.rometools.propono.atom.client; + +import java.io.IOException; +import java.io.InputStreamReader; + +import org.apache.commons.httpclient.methods.GetMethod; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.input.SAXBuilder; + +import com.rometools.propono.atom.common.Categories; +import com.rometools.propono.utils.ProponoException; + +/** + * Models an Atom protocol Categories element, which may contain ROME Atom + * {@link com.rometools.rome.feed.atom.Category} elements. + */ +public class ClientCategories extends Categories { + private ClientCollection clientCollection = null; + + /** Load select from XML element */ + public ClientCategories(final Element e, final ClientCollection clientCollection) throws ProponoException { + this.clientCollection = clientCollection; + parseCategoriesElement(e); + if (getHref() != null) { + fetchContents(); + } + } + + public void fetchContents() throws ProponoException { + final GetMethod method = new GetMethod(getHrefResolved()); + clientCollection.addAuthentication(method); + try { + clientCollection.getHttpClient().executeMethod(method); + if (method.getStatusCode() != 200) { + throw new ProponoException("ERROR HTTP status code=" + method.getStatusCode()); + } + final SAXBuilder builder = new SAXBuilder(); + final Document catsDoc = builder.build(new InputStreamReader(method.getResponseBodyAsStream())); + parseCategoriesElement(catsDoc.getRootElement()); + + } catch (final IOException ioe) { + throw new ProponoException("ERROR: reading out-of-line categories", ioe); + } catch (final JDOMException jde) { + throw new ProponoException("ERROR: parsing out-of-line categories", jde); + } finally { + method.releaseConnection(); + } + } +} diff --git a/src/main/java/com/rometools/propono/atom/client/ClientCollection.java b/src/main/java/com/rometools/propono/atom/client/ClientCollection.java new file mode 100644 index 0000000..a96f0b8 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/client/ClientCollection.java @@ -0,0 +1,225 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.client; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpMethodBase; +import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; +import org.apache.commons.httpclient.methods.GetMethod; +import org.jdom2.Element; + +import com.rometools.propono.atom.common.AtomService; +import com.rometools.propono.atom.common.Categories; +import com.rometools.propono.atom.common.Collection; +import com.rometools.propono.atom.common.Workspace; +import com.rometools.propono.utils.ProponoException; +import com.rometools.rome.feed.atom.Entry; +import com.rometools.rome.io.impl.Atom10Parser; + +/** + * Models an Atom collection, extends Collection and adds methods for adding, retrieving, updateing + * and deleting entries. + */ +public class ClientCollection extends Collection { + + private final boolean writable = true; + + private HttpClient httpClient = null; + private AuthStrategy authStrategy = null; + private ClientWorkspace workspace = null; + private ClientAtomService service = null; + + ClientCollection(final Element e, final ClientWorkspace workspace, final String baseURI) throws ProponoException { + super(e, baseURI); + this.workspace = workspace; + service = workspace.getAtomService(); + httpClient = workspace.getAtomService().getHttpClient(); + authStrategy = workspace.getAtomService().getAuthStrategy(); + parseCollectionElement(e); + } + + ClientCollection(final String href, final AuthStrategy authStrategy) throws ProponoException { + super("Standalone connection", "text", href); + this.authStrategy = authStrategy; + try { + httpClient = new HttpClient(new MultiThreadedHttpConnectionManager()); + // TODO: make connection timeout configurable + httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(30000); + } catch (final Throwable t) { + throw new ProponoException("ERROR creating HTTPClient", t); + } + } + + void addAuthentication(final HttpMethodBase method) throws ProponoException { + authStrategy.addAuthentication(httpClient, method); + } + + /** + * Package access to httpClient to allow use by ClientEntry and ClientMediaEntry. + */ + HttpClient getHttpClient() { + return httpClient; + } + + /** + * Get iterator over entries in this collection. Entries returned are considered to be partial + * entries cannot be saved/updated. + */ + public Iterator getEntries() throws ProponoException { + return new EntryIterator(this); + } + + /** + * Get full entry specified by entry edit URI. Note that entry may or may not be associated with + * this collection. + * + * @return ClientEntry or ClientMediaEntry specified by URI. + */ + public ClientEntry getEntry(final String uri) throws ProponoException { + final GetMethod method = new GetMethod(uri); + authStrategy.addAuthentication(httpClient, method); + try { + httpClient.executeMethod(method); + if (method.getStatusCode() != 200) { + throw new ProponoException("ERROR HTTP status code=" + method.getStatusCode()); + } + final Entry romeEntry = Atom10Parser.parseEntry(new InputStreamReader(method.getResponseBodyAsStream()), uri, Locale.US); + if (!romeEntry.isMediaEntry()) { + return new ClientEntry(service, this, romeEntry, false); + } else { + return new ClientMediaEntry(service, this, romeEntry, false); + } + } catch (final Exception e) { + throw new ProponoException("ERROR: getting or parsing entry/media, HTTP code: ", e); + } finally { + method.releaseConnection(); + } + } + + /** + * Get workspace or null if collection is not associated with a workspace. + */ + public Workspace getWorkspace() { + return workspace; + } + + /** + * Determines if collection is writable. + */ + public boolean isWritable() { + return writable; + } + + /** + * Create new entry associated with collection, but do not save to server. + * + * @throws ProponoException if collecton is not writable. + */ + public ClientEntry createEntry() throws ProponoException { + if (!isWritable()) { + throw new ProponoException("Collection is not writable"); + } + return new ClientEntry(service, this); + } + + /** + * Create new media entry assocaited with collection, but do not save. server. Depending on the + * Atom server, you may or may not be able to persist the properties of the entry that is + * returned. + * + * @param title Title to used for uploaded file. + * @param slug String to be used in file-name of stored file + * @param contentType MIME content-type of file. + * @param bytes Data to be uploaded as byte array. + * @throws ProponoException if collecton is not writable + */ + public ClientMediaEntry createMediaEntry(final String title, final String slug, final String contentType, final byte[] bytes) throws ProponoException { + if (!isWritable()) { + throw new ProponoException("Collection is not writable"); + } + return new ClientMediaEntry(service, this, title, slug, contentType, bytes); + } + + /** + * Create new media entry assocaited with collection, but do not save. server. Depending on the + * Atom server, you may or may not be able to. persist the properties of the entry that is + * returned. + * + * @param title Title to used for uploaded file. + * @param slug String to be used in file-name of stored file + * @param contentType MIME content-type of file. + * @param is Data to be uploaded as InputStream. + * @throws ProponoException if collecton is not writable + */ + public ClientMediaEntry createMediaEntry(final String title, final String slug, final String contentType, final InputStream is) throws ProponoException { + if (!isWritable()) { + throw new ProponoException("Collection is not writable"); + } + return new ClientMediaEntry(service, this, title, slug, contentType, is); + } + + /** + * Save to collection a new entry that was created by a createEntry() or createMediaEntry() and + * save it to the server. + * + * @param entry Entry to be saved. + * @throws ProponoException on error, if collection is not writable or if entry is partial. + */ + public void addEntry(final ClientEntry entry) throws ProponoException { + if (!isWritable()) { + throw new ProponoException("Collection is not writable"); + } + entry.addToCollection(this); + } + + @Override + protected void parseCollectionElement(final Element element) throws ProponoException { + if (workspace == null) { + return; + } + + setHref(element.getAttribute("href").getValue()); + + final Element titleElem = element.getChild("title", AtomService.ATOM_FORMAT); + if (titleElem != null) { + setTitle(titleElem.getText()); + if (titleElem.getAttribute("type", AtomService.ATOM_FORMAT) != null) { + setTitleType(titleElem.getAttribute("type", AtomService.ATOM_FORMAT).getValue()); + } + } + + final List acceptElems = element.getChildren("accept", AtomService.ATOM_PROTOCOL); + if (acceptElems != null && !acceptElems.isEmpty()) { + for (final Element acceptElem : acceptElems) { + addAccept(acceptElem.getTextTrim()); + } + } + + // Loop to parse element to Categories objects + final List catsElems = element.getChildren("categories", AtomService.ATOM_PROTOCOL); + for (final Element catsElem : catsElems) { + final Categories cats = new ClientCategories(catsElem, this); + addCategories(cats); + } + } + +} diff --git a/src/main/java/com/rometools/propono/atom/client/ClientEntry.java b/src/main/java/com/rometools/propono/atom/client/ClientEntry.java new file mode 100644 index 0000000..1af10cb --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/client/ClientEntry.java @@ -0,0 +1,267 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.client; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import org.apache.commons.beanutils.BeanUtils; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpMethodBase; +import org.apache.commons.httpclient.methods.DeleteMethod; +import org.apache.commons.httpclient.methods.EntityEnclosingMethod; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.methods.PutMethod; +import org.apache.commons.httpclient.methods.StringRequestEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.rometools.propono.utils.ProponoException; +import com.rometools.propono.utils.Utilities; +import com.rometools.rome.feed.atom.Content; +import com.rometools.rome.feed.atom.Entry; +import com.rometools.rome.feed.atom.Link; +import com.rometools.rome.io.impl.Atom10Generator; +import com.rometools.rome.io.impl.Atom10Parser; + +/** + * Client implementation of Atom entry, extends ROME Entry to add methods for easily getting/setting + * content, updating and removing the entry from the server. + */ +public class ClientEntry extends Entry { + + private static final long serialVersionUID = 1L; + + private static final Logger LOG = LoggerFactory.getLogger(ClientEntry.class); + + private ClientAtomService service = null; + private ClientCollection collection = null; + protected boolean partial = false; + + public ClientEntry(final ClientAtomService service, final ClientCollection collection) { + this.service = service; + this.collection = collection; + } + + public ClientEntry(final ClientAtomService service, final ClientCollection collection, final Entry entry, final boolean partial) throws ProponoException { + this.service = service; + this.collection = collection; + this.partial = partial; + try { + BeanUtils.copyProperties(this, entry); + } catch (final Exception e) { + throw new ProponoException("ERROR: copying fields from ROME entry", e); + } + } + + /** + * Set content of entry. + * + * @param contentString content string. + * @param type Must be "text" for plain text, "html" for escaped HTML, "xhtml" for XHTML or a + * valid MIME content-type. + */ + public void setContent(final String contentString, final String type) { + final Content newContent = new Content(); + newContent.setType(type == null ? Content.HTML : type); + newContent.setValue(contentString); + final ArrayList contents = new ArrayList(); + contents.add(newContent); + setContents(contents); + } + + /** + * Convenience method to set first content object in content collection. Atom 1.0 allows only + * one content element per entry. + */ + public void setContent(final Content c) { + final ArrayList contents = new ArrayList(); + contents.add(c); + setContents(contents); + } + + /** + * Convenience method to get first content object in content collection. Atom 1.0 allows only + * one content element per entry. + */ + public Content getContent() { + if (getContents() != null && !getContents().isEmpty()) { + final Content c = getContents().get(0); + return c; + } + return null; + } + + /** + * Determines if entries are equal based on edit URI. + */ + @Override + public boolean equals(final Object o) { + if (o instanceof ClientEntry) { + final ClientEntry other = (ClientEntry) o; + if (other.getEditURI() != null && getEditURI() != null) { + return other.getEditURI().equals(getEditURI()); + } + } + return false; + } + + /** + * Update entry by posting new representation of entry to server. Note that you should not + * attempt to update entries that you get from iterating over a collection they may be "partial" + * entries. If you want to update an entry, you must get it via one of the + * getEntry() methods in {@link com.rometools.rome.propono.atom.common.Collection} + * or {@link com.rometools.rome.propono.atom.common.AtomService}. + * + * @throws ProponoException If entry is a "partial" entry. + */ + public void update() throws ProponoException { + if (partial) { + throw new ProponoException("ERROR: attempt to update partial entry"); + } + final EntityEnclosingMethod method = new PutMethod(getEditURI()); + addAuthentication(method); + final StringWriter sw = new StringWriter(); + final int code = -1; + try { + Atom10Generator.serializeEntry(this, sw); + method.setRequestEntity(new StringRequestEntity(sw.toString(), null, null)); + method.setRequestHeader("Content-type", "application/atom+xml; charset=utf-8"); + getHttpClient().executeMethod(method); + final InputStream is = method.getResponseBodyAsStream(); + if (method.getStatusCode() != 200 && method.getStatusCode() != 201) { + throw new ProponoException("ERROR HTTP status=" + method.getStatusCode() + " : " + Utilities.streamToString(is)); + } + + } catch (final Exception e) { + final String msg = "ERROR: updating entry, HTTP code: " + code; + LOG.debug(msg, e); + throw new ProponoException(msg, e); + } finally { + method.releaseConnection(); + } + } + + /** + * Remove entry from server. + */ + public void remove() throws ProponoException { + if (getEditURI() == null) { + throw new ProponoException("ERROR: cannot delete unsaved entry"); + } + final DeleteMethod method = new DeleteMethod(getEditURI()); + addAuthentication(method); + try { + getHttpClient().executeMethod(method); + } catch (final IOException ex) { + throw new ProponoException("ERROR: removing entry, HTTP code", ex); + } finally { + method.releaseConnection(); + } + } + + void setCollection(final ClientCollection collection) { + this.collection = collection; + } + + ClientCollection getCollection() { + return collection; + } + + /** + * Get the URI that can be used to edit the entry via HTTP PUT or DELETE. + */ + public String getEditURI() { + for (int i = 0; i < getOtherLinks().size(); i++) { + final Link link = getOtherLinks().get(i); + if (link.getRel() != null && link.getRel().equals("edit")) { + return link.getHrefResolved(); + } + } + return null; + } + + void addToCollection(final ClientCollection col) throws ProponoException { + setCollection(col); + final EntityEnclosingMethod method = new PostMethod(getCollection().getHrefResolved()); + addAuthentication(method); + final StringWriter sw = new StringWriter(); + int code = -1; + try { + Atom10Generator.serializeEntry(this, sw); + method.setRequestEntity(new StringRequestEntity(sw.toString(), null, null)); + method.setRequestHeader("Content-type", "application/atom+xml; charset=utf-8"); + getHttpClient().executeMethod(method); + final InputStream is = method.getResponseBodyAsStream(); + code = method.getStatusCode(); + if (code != 200 && code != 201) { + throw new ProponoException("ERROR HTTP status=" + code + " : " + Utilities.streamToString(is)); + } + final Entry romeEntry = Atom10Parser.parseEntry(new InputStreamReader(is), getCollection().getHrefResolved(), Locale.US); + BeanUtils.copyProperties(this, romeEntry); + + } catch (final Exception e) { + final String msg = "ERROR: saving entry, HTTP code: " + code; + LOG.debug(msg, e); + throw new ProponoException(msg, e); + } finally { + method.releaseConnection(); + } + final Header locationHeader = method.getResponseHeader("Location"); + if (locationHeader == null) { + LOG.warn("WARNING added entry, but no location header returned"); + } else if (getEditURI() == null) { + final List links = getOtherLinks(); + final Link link = new Link(); + link.setHref(locationHeader.getValue()); + link.setRel("edit"); + links.add(link); + setOtherLinks(links); + } + } + + void addAuthentication(final HttpMethodBase method) throws ProponoException { + if (service != null) { + service.addAuthentication(method); + } else if (collection != null) { + collection.addAuthentication(method); + } + } + + HttpClient getHttpClient() { + if (service != null) { + return service.getHttpClient(); + } else if (collection != null) { + return collection.getHttpClient(); + } + return null; + } + + @Override + public void setCreated(final Date d) { + // protected against null created property (an old Atom 0.3 property) + if (d != null) { + super.setCreated(d); + } + } +} diff --git a/src/main/java/com/rometools/propono/atom/client/ClientMediaEntry.java b/src/main/java/com/rometools/propono/atom/client/ClientMediaEntry.java new file mode 100644 index 0000000..049f039 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/client/ClientMediaEntry.java @@ -0,0 +1,304 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.client; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.apache.commons.beanutils.BeanUtils; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.methods.EntityEnclosingMethod; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.methods.InputStreamRequestEntity; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.methods.PutMethod; +import org.apache.commons.httpclient.methods.StringRequestEntity; +import org.jdom2.JDOMException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.rometools.propono.utils.ProponoException; +import com.rometools.propono.utils.Utilities; +import com.rometools.rome.feed.atom.Content; +import com.rometools.rome.feed.atom.Entry; +import com.rometools.rome.feed.atom.Link; +import com.rometools.rome.io.FeedException; +import com.rometools.rome.io.impl.Atom10Generator; +import com.rometools.rome.io.impl.Atom10Parser; + +/** + * Client implementation of Atom media-link entry, an Atom entry that provides meta-data for a media + * file (e.g. uploaded image or audio file). + */ +public class ClientMediaEntry extends ClientEntry { + + private static final long serialVersionUID = 1L; + + private static final Logger LOG = LoggerFactory.getLogger(ClientMediaEntry.class); + + private String slug = null; + private byte[] bytes; + private InputStream inputStream; + + /** + * Create ClientMedieEntry for service and collection. + */ + public ClientMediaEntry(final ClientAtomService service, final ClientCollection collection) { + super(service, collection); + } + + public ClientMediaEntry(final ClientAtomService service, final ClientCollection collection, final Entry entry, final boolean partial) + throws ProponoException { + super(service, collection, entry, partial); + } + + public ClientMediaEntry(final ClientAtomService service, final ClientCollection collection, final String title, final String slug, + final String contentType, final InputStream is) { + this(service, collection); + inputStream = is; + setTitle(title); + setSlug(slug); + final Content content = new Content(); + content.setType(contentType); + final List contents = new ArrayList(); + contents.add(content); + setContents(contents); + } + + public ClientMediaEntry(final ClientAtomService service, final ClientCollection collection, final String title, final String slug, + final String contentType, final byte[] bytes) { + this(service, collection); + this.bytes = bytes; + setTitle(title); + setSlug(slug); + final Content content = new Content(); + content.setType(contentType); + final List contents = new ArrayList(); + contents.add(content); + setContents(contents); + } + + /** + * Get bytes of media resource associated with entry. + * + * @return Bytes or null if none available or if entry uses an InputStream instead. + */ + public byte[] getBytes() { + return bytes; + } + + /** + * Set media resource data as a byte array, don't try this if you have already set the data as + * an InputStream. + */ + public void setBytes(final byte[] bytes) { + if (inputStream != null) { + throw new IllegalStateException("ERROR: already has inputStream, cannot set both inputStream and bytes"); + } + this.bytes = bytes; + } + + /** + * Get input stream for media resource associated with this entry. + * + * @return InputStream or null if none available or if entry uses bytes instead. + */ + public InputStream getInputStream() { + return inputStream; + } + + /** + * Set media resource data as an input stream, don't try this if you have already set the data + * as a byte array. + */ + public void setInputStream(final InputStream inputStream) { + if (bytes != null) { + throw new IllegalStateException("ERROR: already has bytes, cannot set both bytes and inputStream"); + } + this.inputStream = inputStream; + } + + /** + * Get media link URI for editing the media resource associated with this entry via HTTP PUT or + * DELETE. + */ + public String getMediaLinkURI() { + for (int i = 0; i < getOtherLinks().size(); i++) { + final Link link = getOtherLinks().get(i); + if (link.getRel() != null && link.getRel().equals("edit-media")) { + return link.getHrefResolved(); + } + } + return null; + } + + /** + * Get media resource as an InputStream, should work regardless of whether you set the media + * resource data as an InputStream or as a byte array. + */ + public InputStream getAsStream() throws ProponoException { + if (getContents() != null && !getContents().isEmpty()) { + final Content c = getContents().get(0); + if (c.getSrc() != null) { + return getResourceAsStream(); + } else if (inputStream != null) { + return inputStream; + } else if (bytes != null) { + return new ByteArrayInputStream(bytes); + } else { + throw new ProponoException("ERROR: no src URI or binary data to return"); + } + } else { + throw new ProponoException("ERROR: no content found in entry"); + } + } + + private InputStream getResourceAsStream() throws ProponoException { + if (getEditURI() == null) { + throw new ProponoException("ERROR: not yet saved to server"); + } + final GetMethod method = new GetMethod(((Content) getContents()).getSrc()); + try { + getCollection().getHttpClient().executeMethod(method); + if (method.getStatusCode() != 200) { + throw new ProponoException("ERROR HTTP status=" + method.getStatusCode()); + } + return method.getResponseBodyAsStream(); + } catch (final IOException e) { + throw new ProponoException("ERROR: getting media entry", e); + } + } + + /** + * Update entry on server. + */ + @Override + public void update() throws ProponoException { + if (partial) { + throw new ProponoException("ERROR: attempt to update partial entry"); + } + EntityEnclosingMethod method = null; + final Content updateContent = getContents().get(0); + try { + if (getMediaLinkURI() != null && getBytes() != null) { + // existing media entry and new file, so PUT file to edit-media URI + method = new PutMethod(getMediaLinkURI()); + if (inputStream != null) { + method.setRequestEntity(new InputStreamRequestEntity(inputStream)); + } else { + method.setRequestEntity(new InputStreamRequestEntity(new ByteArrayInputStream(getBytes()))); + } + + method.setRequestHeader("Content-type", updateContent.getType()); + } else if (getEditURI() != null) { + // existing media entry and NO new file, so PUT entry to edit URI + method = new PutMethod(getEditURI()); + final StringWriter sw = new StringWriter(); + Atom10Generator.serializeEntry(this, sw); + method.setRequestEntity(new StringRequestEntity(sw.toString(), null, null)); + method.setRequestHeader("Content-type", "application/atom+xml; charset=utf8"); + } else { + throw new ProponoException("ERROR: media entry has no edit URI or media-link URI"); + } + getCollection().addAuthentication(method); + method.addRequestHeader("Title", getTitle()); + getCollection().getHttpClient().executeMethod(method); + if (inputStream != null) { + inputStream.close(); + } + final InputStream is = method.getResponseBodyAsStream(); + if (method.getStatusCode() != 200 && method.getStatusCode() != 201) { + throw new ProponoException("ERROR HTTP status=" + method.getStatusCode() + " : " + Utilities.streamToString(is)); + } + + } catch (final Exception e) { + throw new ProponoException("ERROR: saving media entry"); + } + if (method.getStatusCode() != 201) { + throw new ProponoException("ERROR HTTP status=" + method.getStatusCode()); + } + } + + /** Package access, to be called by DefaultClientCollection */ + @Override + void addToCollection(final ClientCollection col) throws ProponoException { + setCollection(col); + final EntityEnclosingMethod method = new PostMethod(col.getHrefResolved()); + getCollection().addAuthentication(method); + try { + final Content c = getContents().get(0); + if (inputStream != null) { + method.setRequestEntity(new InputStreamRequestEntity(inputStream)); + } else { + method.setRequestEntity(new InputStreamRequestEntity(new ByteArrayInputStream(getBytes()))); + } + method.setRequestHeader("Content-type", c.getType()); + method.setRequestHeader("Title", getTitle()); + method.setRequestHeader("Slug", getSlug()); + getCollection().getHttpClient().executeMethod(method); + if (inputStream != null) { + inputStream.close(); + } + final InputStream is = method.getResponseBodyAsStream(); + if (method.getStatusCode() == 200 || method.getStatusCode() == 201) { + final Entry romeEntry = Atom10Parser.parseEntry(new InputStreamReader(is), col.getHrefResolved(), Locale.US); + BeanUtils.copyProperties(this, romeEntry); + + } else { + throw new ProponoException("ERROR HTTP status-code=" + method.getStatusCode() + " status-line: " + method.getStatusLine()); + } + } catch (final IOException ie) { + throw new ProponoException("ERROR: saving media entry", ie); + } catch (final JDOMException je) { + throw new ProponoException("ERROR: saving media entry", je); + } catch (final FeedException fe) { + throw new ProponoException("ERROR: saving media entry", fe); + } catch (final IllegalAccessException ae) { + throw new ProponoException("ERROR: saving media entry", ae); + } catch (final InvocationTargetException te) { + throw new ProponoException("ERROR: saving media entry", te); + } + final Header locationHeader = method.getResponseHeader("Location"); + if (locationHeader == null) { + LOG.warn("WARNING added entry, but no location header returned"); + } else if (getEditURI() == null) { + final List links = getOtherLinks(); + final Link link = new Link(); + link.setHref(locationHeader.getValue()); + link.setRel("edit"); + links.add(link); + setOtherLinks(links); + } + } + + /** Set string to be used in file name of new media resource on server. */ + public String getSlug() { + return slug; + } + + /** Get string to be used in file name of new media resource on server. */ + public void setSlug(final String slug) { + this.slug = slug; + } + +} diff --git a/src/main/java/com/rometools/propono/atom/client/ClientWorkspace.java b/src/main/java/com/rometools/propono/atom/client/ClientWorkspace.java new file mode 100644 index 0000000..e06b7e0 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/client/ClientWorkspace.java @@ -0,0 +1,62 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.client; + +import java.util.List; + +import org.jdom2.Element; + +import com.rometools.propono.atom.common.AtomService; +import com.rometools.propono.atom.common.Workspace; +import com.rometools.propono.utils.ProponoException; + +/** + * Represents Atom protocol workspace on client-side. It extends the common + * {@link com.rometools.rome.propono.atom.common.Workspace} to return + * {@link com.rometools.rome.propono.atom.client.ClientCollection} objects instead of common + * {@link com.rometools.rome.propono.atom.common.Collection}s. + */ +public class ClientWorkspace extends Workspace { + + private ClientAtomService atomService = null; + + ClientWorkspace(final Element e, final ClientAtomService atomService, final String baseURI) throws ProponoException { + super("dummy", "dummy"); + this.atomService = atomService; + parseWorkspaceElement(e, baseURI); + } + + /** + * Package access to parent service. + */ + ClientAtomService getAtomService() { + return atomService; + } + + /** Deserialize a Atom workspace XML element into an object */ + protected void parseWorkspaceElement(final Element element, final String baseURI) throws ProponoException { + final Element titleElem = element.getChild("title", AtomService.ATOM_FORMAT); + setTitle(titleElem.getText()); + if (titleElem.getAttribute("type", AtomService.ATOM_FORMAT) != null) { + setTitleType(titleElem.getAttribute("type", AtomService.ATOM_FORMAT).getValue()); + } + final List collections = element.getChildren("collection", AtomService.ATOM_PROTOCOL); + for (final Element e : collections) { + addCollection(new ClientCollection(e, this, baseURI)); + } + } + +} diff --git a/src/main/java/com/rometools/propono/atom/client/EntryIterator.java b/src/main/java/com/rometools/propono/atom/client/EntryIterator.java new file mode 100644 index 0000000..a92d2ba --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/client/EntryIterator.java @@ -0,0 +1,129 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.client; + +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.apache.commons.httpclient.methods.GetMethod; +import org.jdom2.Document; +import org.jdom2.input.SAXBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.rometools.propono.utils.ProponoException; +import com.rometools.rome.feed.atom.Entry; +import com.rometools.rome.feed.atom.Feed; +import com.rometools.rome.feed.atom.Link; +import com.rometools.rome.io.WireFeedInput; + +/** + * Enables iteration over entries in Atom protocol collection. + */ +public class EntryIterator implements Iterator { + + private static final Logger LOG = LoggerFactory.getLogger(EntryIterator.class); + + private final ClientCollection collection; + + private Iterator members = null; + private Feed col = null; + private final String collectionURI; + private String nextURI; + + EntryIterator(final ClientCollection collection) throws ProponoException { + this.collection = collection; + collectionURI = collection.getHrefResolved(); + nextURI = collectionURI; + getNextEntries(); + } + + /** + * Returns true if more entries are available. + */ + @Override + public boolean hasNext() { + if (!members.hasNext()) { + try { + getNextEntries(); + } catch (final Exception ignored) { + LOG.error("An error occured while getting next entries", ignored); + } + } + return members.hasNext(); + } + + /** + * Get next entry in collection. + */ + @Override + public ClientEntry next() { + if (hasNext()) { + final Entry romeEntry = members.next(); + try { + if (!romeEntry.isMediaEntry()) { + return new ClientEntry(null, collection, romeEntry, true); + } else { + return new ClientMediaEntry(null, collection, romeEntry, true); + } + } catch (final ProponoException e) { + throw new RuntimeException("Unexpected exception creating ClientEntry or ClientMedia", e); + } + } + throw new NoSuchElementException(); + } + + /** + * Remove entry is not implemented. + */ + @Override + public void remove() { + // optional method, not implemented + } + + private void getNextEntries() throws ProponoException { + if (nextURI == null) { + return; + } + final GetMethod colGet = new GetMethod(collection.getHrefResolved(nextURI)); + collection.addAuthentication(colGet); + try { + collection.getHttpClient().executeMethod(colGet); + final SAXBuilder builder = new SAXBuilder(); + final Document doc = builder.build(colGet.getResponseBodyAsStream()); + final WireFeedInput feedInput = new WireFeedInput(); + col = (Feed) feedInput.build(doc); + } catch (final Exception e) { + throw new ProponoException("ERROR: fetching or parsing next entries, HTTP code: " + (colGet != null ? colGet.getStatusCode() : -1), e); + } finally { + colGet.releaseConnection(); + } + members = col.getEntries().iterator(); + col.getEntries().size(); + + nextURI = null; + final List altLinks = col.getOtherLinks(); + if (altLinks != null) { + for (final Link link : altLinks) { + if ("next".equals(link.getRel())) { + nextURI = link.getHref(); + } + } + } + } + +} diff --git a/src/main/java/com/rometools/propono/atom/client/GDataAuthStrategy.java b/src/main/java/com/rometools/propono/atom/client/GDataAuthStrategy.java new file mode 100644 index 0000000..0912397 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/client/GDataAuthStrategy.java @@ -0,0 +1,64 @@ +/* + * Copyright 2009 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.client; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpMethodBase; +import org.apache.commons.httpclient.NameValuePair; +import org.apache.commons.httpclient.methods.PostMethod; + +import com.rometools.propono.utils.ProponoException; + +public class GDataAuthStrategy implements AuthStrategy { + private final String email; + private final String password; + private final String service; + private String authToken; + + public GDataAuthStrategy(final String email, final String password, final String service) throws ProponoException { + this.email = email; + this.password = password; + this.service = service; + init(); + } + + private void init() throws ProponoException { + try { + final HttpClient httpClient = new HttpClient(); + final PostMethod method = new PostMethod("https://www.google.com/accounts/ClientLogin"); + final NameValuePair[] data = { new NameValuePair("Email", email), new NameValuePair("Passwd", password), + new NameValuePair("accountType", "GOOGLE"), new NameValuePair("service", service), + new NameValuePair("source", "ROME Propono Atompub Client") }; + method.setRequestBody(data); + httpClient.executeMethod(method); + + final String responseBody = method.getResponseBodyAsString(); + final int authIndex = responseBody.indexOf("Auth="); + + authToken = "GoogleLogin auth=" + responseBody.trim().substring(authIndex + 5); + + } catch (final Throwable t) { + t.printStackTrace(); + throw new ProponoException("ERROR obtaining Google authentication string", t); + } + } + + @Override + public void addAuthentication(final HttpClient httpClient, final HttpMethodBase method) throws ProponoException { + httpClient.getParams().setAuthenticationPreemptive(true); + method.setRequestHeader("Authorization", authToken); + } +} diff --git a/src/main/java/com/rometools/propono/atom/client/NoAuthStrategy.java b/src/main/java/com/rometools/propono/atom/client/NoAuthStrategy.java new file mode 100644 index 0000000..2d45687 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/client/NoAuthStrategy.java @@ -0,0 +1,33 @@ +/* + * Copyright 2009 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.client; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpMethodBase; + +import com.rometools.propono.utils.ProponoException; + +/** + * No authentication + */ +public class NoAuthStrategy implements AuthStrategy { + + @Override + public void addAuthentication(final HttpClient httpClient, final HttpMethodBase method) throws ProponoException { + // no-op + } + +} diff --git a/src/main/java/com/rometools/propono/atom/client/OAuthStrategy.java b/src/main/java/com/rometools/propono/atom/client/OAuthStrategy.java new file mode 100644 index 0000000..ae610f6 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/client/OAuthStrategy.java @@ -0,0 +1,295 @@ +/* + * Copyright 2009 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.client; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import net.oauth.OAuth; +import net.oauth.OAuthAccessor; +import net.oauth.OAuthConsumer; +import net.oauth.OAuthMessage; +import net.oauth.OAuthServiceProvider; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpMethodBase; +import org.apache.commons.httpclient.NameValuePair; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.util.ParameterParser; + +import com.rometools.propono.utils.ProponoException; + +/** + * Strategy for using OAuth. + */ +public class OAuthStrategy implements AuthStrategy { + + private State state = State.UNAUTHORIZED; + + private enum State { + UNAUTHORIZED, // have not sent any requests + REQUEST_TOKEN, // have a request token + AUTHORIZED, // are authorized + ACCESS_TOKEN // have access token, ready to make calls + } + + private final String username; + private final String consumerKey; + private final String consumerSecret; + private final String keyType; + + private final String reqUrl; + private final String authzUrl; + private final String accessUrl; + + private final String nonce; + private final long timestamp; + + private String requestToken = null; + private String accessToken = null; + private String tokenSecret = null; + + /** + * Create OAuth authentcation strategy and negotiate with services to obtain access token to be + * used in subsequent calls. + * + * @param username Username to be used in authentication + * @param key Consumer key + * @param secret Consumer secret + * @param keyType Key type (e.g. "HMAC-SHA1") + * @param reqUrl URL of request token service + * @param authzUrl URL of authorize service + * @param accessUrl URL of acess token service + * @throws ProponoException on any sort of initialization error + */ + public OAuthStrategy(final String username, final String key, final String secret, final String keyType, final String reqUrl, final String authzUrl, + final String accessUrl) throws ProponoException { + + this.username = username; + this.reqUrl = reqUrl; + this.authzUrl = authzUrl; + this.accessUrl = accessUrl; + consumerKey = key; + consumerSecret = secret; + this.keyType = keyType; + + nonce = UUID.randomUUID().toString(); + timestamp = new Date().getTime() / 1000L; + + init(); + } + + private void init() throws ProponoException { + callOAuthUri(reqUrl); + callOAuthUri(authzUrl); + callOAuthUri(accessUrl); + } + + @Override + public void addAuthentication(final HttpClient httpClient, final HttpMethodBase method) throws ProponoException { + + if (state != State.ACCESS_TOKEN) { + throw new ProponoException("ERROR: authentication strategy failed init"); + } + + // add OAuth name/values to request query string + + // wish we didn't have to parse them apart first, ugh + List originalqlist = null; + if (method.getQueryString() != null) { + String qstring = method.getQueryString().trim(); + qstring = qstring.startsWith("?") ? qstring.substring(1) : qstring; + @SuppressWarnings("unchecked") + final List parameters = new ParameterParser().parse(qstring, '&'); + originalqlist = parameters; + } else { + originalqlist = new ArrayList(); + } + + // put query string into hashmap form to please OAuth.net classes + final Map params = new HashMap(); + for (final Object element : originalqlist) { + final NameValuePair pair = (NameValuePair) element; + params.put(pair.getName(), pair.getValue()); + } + + // add OAuth params to query string + params.put("xoauth_requestor_id", username); + params.put("oauth_consumer_key", consumerKey); + params.put("oauth_signature_method", keyType); + params.put("oauth_timestamp", Long.toString(timestamp)); + params.put("oauth_nonce", nonce); + params.put("oauth_token", accessToken); + params.put("oauth_token_secret", tokenSecret); + + // sign complete URI + String finalUri = null; + final OAuthServiceProvider provider = new OAuthServiceProvider(reqUrl, authzUrl, accessUrl); + final OAuthConsumer consumer = new OAuthConsumer(null, consumerKey, consumerSecret, provider); + final OAuthAccessor accessor = new OAuthAccessor(consumer); + accessor.tokenSecret = tokenSecret; + OAuthMessage message; + try { + message = new OAuthMessage(method.getName(), method.getURI().toString(), params.entrySet()); + message.sign(accessor); + + finalUri = OAuth.addParameters(message.URL, message.getParameters()); + + } catch (final Exception ex) { + throw new ProponoException("ERROR: OAuth signing request", ex); + } + + // pull query string off and put it back onto method + method.setQueryString(finalUri.substring(finalUri.lastIndexOf("?"))); + } + + private void callOAuthUri(final String uri) throws ProponoException { + + final HttpClient httpClient = new HttpClient(); + + final HttpMethodBase method; + final String content; + + final Map params = new HashMap(); + params.put("oauth_version", "1.0"); + if (username != null) { + params.put("xoauth_requestor_id", username); + } + params.put("oauth_consumer_key", consumerKey); + params.put("oauth_signature_method", keyType); + params.put("oauth_timestamp", Long.toString(timestamp)); + params.put("oauth_nonce", nonce); + params.put("oauth_callback", "none"); + + final OAuthServiceProvider provider = new OAuthServiceProvider(reqUrl, authzUrl, accessUrl); + final OAuthConsumer consumer = new OAuthConsumer(null, consumerKey, consumerSecret, provider); + final OAuthAccessor accessor = new OAuthAccessor(consumer); + + if (state == State.UNAUTHORIZED) { + + try { + final OAuthMessage message = new OAuthMessage("GET", uri, params.entrySet()); + message.sign(accessor); + + final String finalUri = OAuth.addParameters(message.URL, message.getParameters()); + method = new GetMethod(finalUri); + httpClient.executeMethod(method); + content = method.getResponseBodyAsString(); + + } catch (final Exception e) { + throw new ProponoException("ERROR fetching request token", e); + } + + } else if (state == State.REQUEST_TOKEN) { + + try { + params.put("oauth_token", requestToken); + params.put("oauth_token_secret", tokenSecret); + accessor.tokenSecret = tokenSecret; + + final OAuthMessage message = new OAuthMessage("POST", uri, params.entrySet()); + message.sign(accessor); + + final String finalUri = OAuth.addParameters(message.URL, message.getParameters()); + method = new PostMethod(finalUri); + httpClient.executeMethod(method); + content = method.getResponseBodyAsString(); + + } catch (final Exception e) { + throw new ProponoException("ERROR fetching request token", e); + } + + } else if (state == State.AUTHORIZED) { + + try { + params.put("oauth_token", accessToken); + params.put("oauth_token_secret", tokenSecret); + accessor.tokenSecret = tokenSecret; + + final OAuthMessage message = new OAuthMessage("GET", uri, params.entrySet()); + message.sign(accessor); + + final String finalUri = OAuth.addParameters(message.URL, message.getParameters()); + method = new GetMethod(finalUri); + httpClient.executeMethod(method); + content = method.getResponseBodyAsString(); + + } catch (final Exception e) { + throw new ProponoException("ERROR fetching request token", e); + } + + } else { + method = null; + content = null; + return; + } + + String token = null; + String secret = null; + + if (content != null) { + final String[] settings = content.split("&"); + for (final String setting2 : settings) { + final String[] setting = setting2.split("="); + if (setting.length > 1) { + if ("oauth_token".equals(setting[0])) { + token = setting[1]; + } else if ("oauth_token_secret".equals(setting[0])) { + secret = setting[1]; + } + } + } + } + + // TODO review switch without 'default' + + switch (state) { + + case UNAUTHORIZED: + if (token != null && secret != null) { + requestToken = token; + tokenSecret = secret; + state = State.REQUEST_TOKEN; + } else { + throw new ProponoException("ERROR: requestToken or tokenSecret is null"); + } + break; + + case REQUEST_TOKEN: + if (method.getStatusCode() == 200) { + state = State.AUTHORIZED; + } else { + throw new ProponoException("ERROR: authorization returned code: " + method.getStatusCode()); + } + break; + + case AUTHORIZED: + if (token != null && secret != null) { + accessToken = token; + tokenSecret = secret; + state = State.ACCESS_TOKEN; + } else { + throw new ProponoException("ERROR: accessToken or tokenSecret is null"); + } + break; + } + } +} diff --git a/src/main/java/com/rometools/propono/atom/client/atomclient-diagram.gif b/src/main/java/com/rometools/propono/atom/client/atomclient-diagram.gif new file mode 100644 index 0000000..21b1094 Binary files /dev/null and b/src/main/java/com/rometools/propono/atom/client/atomclient-diagram.gif differ diff --git a/src/main/java/com/rometools/propono/atom/common/AtomService.java b/src/main/java/com/rometools/propono/atom/common/AtomService.java new file mode 100644 index 0000000..43d8a64 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/common/AtomService.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package com.rometools.propono.atom.common; + +import java.util.ArrayList; +import java.util.List; + +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.Namespace; + +import com.rometools.propono.utils.ProponoException; + +/** + * Models an Atom Publishing Protocol Service Document. Is able to read a Service document from a + * JDOM Document and to write Service document out as a JDOM Document. + */ +public class AtomService { + + private List workspaces = new ArrayList(); + + /** Namespace for Atom Syndication Format */ + public static Namespace ATOM_FORMAT = Namespace.getNamespace("atom", "http://www.w3.org/2005/Atom"); + + /** Namespace for Atom Publishing Protocol */ + public static Namespace ATOM_PROTOCOL = Namespace.getNamespace("app", "http://www.w3.org/2007/app"); + + /** + * Create new and empty Atom service + */ + public AtomService() { + } + + /** + * Add Workspace to service. + */ + public void addWorkspace(final Workspace workspace) { + workspaces.add(workspace); + } + + /** + * Get Workspaces available from service. + */ + public List getWorkspaces() { + return workspaces; + } + + /** + * Set Workspaces of service. + */ + public void setWorkspaces(final List workspaces) { + this.workspaces = workspaces; + } + + /** + * Find workspace by title. + * + * @param title Match this title + * @return Matching Workspace or null if none found. + */ + public Workspace findWorkspace(final String title) { + for (final Object element : workspaces) { + final Workspace ws = (Workspace) element; + if (title.equals(ws.getTitle())) { + return ws; + } + } + return null; + } + + /** + * Deserialize an Atom service XML document into an object + */ + public static AtomService documentToService(final Document document) throws ProponoException { + final AtomService service = new AtomService(); + final Element root = document.getRootElement(); + final List spaces = root.getChildren("workspace", ATOM_PROTOCOL); + for (final Element e : spaces) { + service.addWorkspace(Workspace.elementToWorkspace(e)); + } + return service; + } + + /** + * Serialize an AtomService object into an XML document + */ + public Document serviceToDocument() { + final AtomService service = this; + + final Document doc = new Document(); + final Element root = new Element("service", ATOM_PROTOCOL); + doc.setRootElement(root); + final List spaces = service.getWorkspaces(); + for (final Workspace space : spaces) { + root.addContent(space.workspaceToElement()); + } + return doc; + } + +} diff --git a/src/main/java/com/rometools/propono/atom/common/Categories.java b/src/main/java/com/rometools/propono/atom/common/Categories.java new file mode 100644 index 0000000..c3bde6e --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/common/Categories.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package com.rometools.propono.atom.common; + +import java.util.ArrayList; +import java.util.List; + +import org.jdom2.Element; + +import com.rometools.rome.feed.atom.Category; +import com.rometools.rome.io.impl.Atom10Parser; + +/** + * Models an Atom protocol Categories element, which may contain ROME Atom + * {@link com.rometools.rome.feed.atom.Category} elements. + */ +public class Categories { + + private final List categories = new ArrayList(); + private String baseURI = null; + private Element categoriesElement = null; + private String href = null; + private String scheme = null; + private boolean fixed = false; + + public Categories() { + } + + /** Load select from XML element */ + public Categories(final Element e, final String baseURI) { + categoriesElement = e; + this.baseURI = baseURI; + parseCategoriesElement(e); + } + + /** Add category list of those specified */ + public void addCategory(final Category cat) { + categories.add(cat); + } + + /** + * Iterate over Category objects + * + * @return List of ROME Atom {@link com.rometools.rome.feed.atom.Category} + */ + public List getCategories() { + return categories; + } + + /** True if clients MUST use one of the categories specified */ + public boolean isFixed() { + return fixed; + } + + /** True if clients MUST use one of the categories specified */ + public void setFixed(final boolean fixed) { + this.fixed = fixed; + } + + /** Category URI scheme to use for Categories without a scheme */ + public String getScheme() { + return scheme; + } + + /** Category URI scheme to use for Categories without a scheme */ + public void setScheme(final String scheme) { + this.scheme = scheme; + } + + /** URI of out-of-line categories */ + public String getHref() { + return href; + } + + /** URI of out-of-line categories */ + public void setHref(final String href) { + this.href = href; + } + + /** Get unresolved URI of the collection, or null if impossible to determine */ + public String getHrefResolved() { + if (Atom10Parser.isAbsoluteURI(href)) { + return href; + } else if (baseURI != null && categoriesElement != null) { + return Atom10Parser.resolveURI(baseURI, categoriesElement, href); + } + return null; + } + + public Element categoriesToElement() { + final Categories cats = this; + final Element catsElem = new Element("categories", AtomService.ATOM_PROTOCOL); + catsElem.setAttribute("fixed", cats.isFixed() ? "yes" : "no", AtomService.ATOM_PROTOCOL); + if (cats.getScheme() != null) { + catsElem.setAttribute("scheme", cats.getScheme(), AtomService.ATOM_PROTOCOL); + } + if (cats.getHref() != null) { + catsElem.setAttribute("href", cats.getHref(), AtomService.ATOM_PROTOCOL); + } else { + // Loop to create elements + for (final Object element : cats.getCategories()) { + final Category cat = (Category) element; + final Element catElem = new Element("category", AtomService.ATOM_FORMAT); + catElem.setAttribute("term", cat.getTerm(), AtomService.ATOM_FORMAT); + if (cat.getScheme() != null) { // optional + catElem.setAttribute("scheme", cat.getScheme(), AtomService.ATOM_FORMAT); + } + if (cat.getLabel() != null) { // optional + catElem.setAttribute("label", cat.getLabel(), AtomService.ATOM_FORMAT); + } + catsElem.addContent(catElem); + } + } + return catsElem; + } + + protected void parseCategoriesElement(final Element catsElem) { + + if (catsElem.getAttribute("href", AtomService.ATOM_PROTOCOL) != null) { + setHref(catsElem.getAttribute("href", AtomService.ATOM_PROTOCOL).getValue()); + } + + if (catsElem.getAttribute("fixed", AtomService.ATOM_PROTOCOL) != null) { + if ("yes".equals(catsElem.getAttribute("fixed", AtomService.ATOM_PROTOCOL).getValue())) { + setFixed(true); + } + } + + if (catsElem.getAttribute("scheme", AtomService.ATOM_PROTOCOL) != null) { + setScheme(catsElem.getAttribute("scheme", AtomService.ATOM_PROTOCOL).getValue()); + } + + // Loop to parse elemenents to Category objects + final List catElems = catsElem.getChildren("category", AtomService.ATOM_FORMAT); + for (final Element catElem : catElems) { + final Category cat = new Category(); + cat.setTerm(catElem.getAttributeValue("term", AtomService.ATOM_FORMAT)); + cat.setLabel(catElem.getAttributeValue("label", AtomService.ATOM_FORMAT)); + cat.setScheme(catElem.getAttributeValue("scheme", AtomService.ATOM_FORMAT)); + addCategory(cat); + } + } + +} diff --git a/src/main/java/com/rometools/propono/atom/common/Collection.java b/src/main/java/com/rometools/propono/atom/common/Collection.java new file mode 100644 index 0000000..9fb0834 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/common/Collection.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package com.rometools.propono.atom.common; + +import java.util.ArrayList; +import java.util.List; + +import org.jdom2.Element; + +import com.rometools.propono.utils.ProponoException; +import com.rometools.rome.io.impl.Atom10Parser; + +/** + * Models an Atom workspace collection. + */ +public class Collection { + + public static final String ENTRY_TYPE = "application/atom+xml;type=entry"; + + private final List categories = new ArrayList(); + + private Element collectionElement = null; + private String baseURI = null; + private String title = null; + private String titleType = null; // may be TEXT, HTML, XHTML + private List accepts = new ArrayList(); + private String href = null; + + /** + * Collection MUST have title and href. + * + * @param title Title for collection + * @param titleType Content type of title (null for plain text) + * @param href Collection URI. + */ + public Collection(final String title, final String titleType, final String href) { + this.title = title; + this.titleType = titleType; + this.href = href; + } + + /** Load self from XML element */ + public Collection(final Element e) throws ProponoException { + collectionElement = e; + parseCollectionElement(e); + } + + /** Load self from XML element and base URI for resolving relative URIs */ + public Collection(final Element e, final String baseURI) throws ProponoException { + collectionElement = e; + this.baseURI = baseURI; + parseCollectionElement(e); + } + + /** + * List of content-type ranges accepted by collection. + */ + public List getAccepts() { + return accepts; + } + + public void addAccept(final String accept) { + accepts.add(accept); + } + + public void setAccepts(final List accepts) { + this.accepts = accepts; + } + + /** The URI of the collection */ + public String getHref() { + return href; + } + + /** + * Set URI of collection + */ + public void setHref(final String href) { + this.href = href; + } + + /** Get resolved URI of the collection, or null if impossible to determine */ + public String getHrefResolved() { + if (Atom10Parser.isAbsoluteURI(href)) { + return href; + } else if (baseURI != null && collectionElement != null) { + final int lastslash = baseURI.lastIndexOf("/"); + return Atom10Parser.resolveURI(baseURI.substring(0, lastslash), collectionElement, href); + } + return null; + } + + /** Get resolved URI using collection's baseURI, or null if impossible to determine */ + public String getHrefResolved(final String relativeUri) { + if (Atom10Parser.isAbsoluteURI(relativeUri)) { + return relativeUri; + } else if (baseURI != null && collectionElement != null) { + final int lastslash = baseURI.lastIndexOf("/"); + return Atom10Parser.resolveURI(baseURI.substring(0, lastslash), collectionElement, relativeUri); + } + return null; + } + + /** Must have human readable title */ + public String getTitle() { + return title; + } + + /** + * Set title of collection. + */ + public void setTitle(final String title) { + this.title = title; + } + + /** + * Type of title ("text", "html" or "xhtml") + */ + public String getTitleType() { + return titleType; + } + + /** + * Type of title ("text", "html" or "xhtml") + */ + public void setTitleType(final String titleType) { + this.titleType = titleType; + } + + /** Workspace can have multiple Categories objects */ + public void addCategories(final Categories cats) { + categories.add(cats); + } + + /** + * Get categories allowed by collection. + * + * @return Collection of {@link com.rometools.rome.propono.atom.common.Categories} objects. + */ + public List getCategories() { + return categories; + } + + /** + * Returns true if contentType is accepted by collection. + */ + public boolean accepts(final String ct) { + for (final Object element : accepts) { + final String accept = (String) element; + if (accept != null && accept.trim().equals("*/*")) { + return true; + } + final String entryType = "application/atom+xml"; + final boolean entry = entryType.equals(ct); + if (entry && null == accept) { + return true; + } else if (entry && "entry".equals(accept)) { + return true; + } else if (entry && entryType.equals(accept)) { + return true; + } else { + final String[] rules = accepts.toArray(new String[accepts.size()]); + for (final String rule2 : rules) { + String rule = rule2.trim(); + if (rule.equals(ct)) { + return true; + } + final int slashstar = rule.indexOf("/*"); + if (slashstar > 0) { + rule = rule.substring(0, slashstar + 1); + if (ct.startsWith(rule)) { + return true; + } + } + } + } + } + return false; + } + + /** + * Serialize an AtomService.Collection into an XML element + */ + public Element collectionToElement() { + final Collection collection = this; + final Element element = new Element("collection", AtomService.ATOM_PROTOCOL); + element.setAttribute("href", collection.getHref()); + + final Element titleElem = new Element("title", AtomService.ATOM_FORMAT); + titleElem.setText(collection.getTitle()); + if (collection.getTitleType() != null && !collection.getTitleType().equals("TEXT")) { + titleElem.setAttribute("type", collection.getTitleType(), AtomService.ATOM_FORMAT); + } + element.addContent(titleElem); + + // Loop to create elements + for (final Object element2 : collection.getCategories()) { + final Categories cats = (Categories) element2; + element.addContent(cats.categoriesToElement()); + } + + for (final Object element2 : collection.getAccepts()) { + final String range = (String) element2; + final Element acceptElem = new Element("accept", AtomService.ATOM_PROTOCOL); + acceptElem.setText(range); + element.addContent(acceptElem); + } + + return element; + } + + /** Deserialize an Atom service collection XML element into an object */ + public Collection elementToCollection(final Element element) throws ProponoException { + return new Collection(element); + } + + protected void parseCollectionElement(final Element element) throws ProponoException { + setHref(element.getAttribute("href").getValue()); + + final Element titleElem = element.getChild("title", AtomService.ATOM_FORMAT); + if (titleElem != null) { + setTitle(titleElem.getText()); + if (titleElem.getAttribute("type", AtomService.ATOM_FORMAT) != null) { + setTitleType(titleElem.getAttribute("type", AtomService.ATOM_FORMAT).getValue()); + } + } + + final List acceptElems = element.getChildren("accept", AtomService.ATOM_PROTOCOL); + if (acceptElems != null && !acceptElems.isEmpty()) { + for (final Element acceptElem : acceptElems) { + addAccept(acceptElem.getTextTrim()); + } + } + + // Loop to parse element to Categories objects + final List catsElems = element.getChildren("categories", AtomService.ATOM_PROTOCOL); + for (final Element catsElem : catsElems) { + final Categories cats = new Categories(catsElem, baseURI); + addCategories(cats); + } + } + +} diff --git a/src/main/java/com/rometools/propono/atom/common/Workspace.java b/src/main/java/com/rometools/propono/atom/common/Workspace.java new file mode 100644 index 0000000..c7452c2 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/common/Workspace.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package com.rometools.propono.atom.common; + +import java.util.ArrayList; +import java.util.List; + +import org.jdom2.Element; + +import com.rometools.propono.utils.ProponoException; + +/** + * Models an Atom workspace. + */ +public class Workspace { + + private String title = null; + private String titleType = null; // may be TEXT, HTML, XHTML + private final List collections = new ArrayList(); + + /** + * Collection MUST have title. + * + * @param title Title for collection + * @param titleType Content type of title (null for plain text) + */ + public Workspace(final String title, final String titleType) { + this.title = title; + this.titleType = titleType; + } + + public Workspace(final Element elem) throws ProponoException { + parseWorkspaceElement(elem); + } + + /** Iterate over collections in workspace */ + public List getCollections() { + return collections; + } + + /** Add new collection to workspace */ + public void addCollection(final Collection col) { + collections.add(col); + } + + /** + * DefaultWorkspace must have a human readable title + */ + public String getTitle() { + return title; + } + + /** + * Set title of workspace. + */ + public void setTitle(final String title) { + this.title = title; + } + + /** + * Get title type ("text", "html", "xhtml" or MIME content-type) + */ + public String getTitleType() { + return titleType; + } + + /** + * Set title type ("text", "html", "xhtml" or MIME content-type) + */ + public void setTitleType(final String titleType) { + this.titleType = titleType; + } + + /** + * Find collection by title and/or content-type. + * + * @param title Title or null to match all titles. + * @param contentType Content-type or null to match all content-types. + * @return First Collection that matches title and/or content-type. + */ + public Collection findCollection(final String title, final String contentType) { + for (final Object element : collections) { + final Collection col = (Collection) element; + if (title != null && col.accepts(contentType)) { + return col; + } else if (col.accepts(contentType)) { + return col; + } + } + return null; + } + + /** Deserialize a Atom workspace XML element into an object */ + public static Workspace elementToWorkspace(final Element element) throws ProponoException { + return new Workspace(element); + } + + /** + * Serialize an AtomService.DefaultWorkspace object into an XML element + */ + public Element workspaceToElement() { + final Workspace space = this; + + final Element element = new Element("workspace", AtomService.ATOM_PROTOCOL); + + final Element titleElem = new Element("title", AtomService.ATOM_FORMAT); + titleElem.setText(space.getTitle()); + if (space.getTitleType() != null && !space.getTitleType().equals("TEXT")) { + titleElem.setAttribute("type", space.getTitleType(), AtomService.ATOM_FORMAT); + } + element.addContent(titleElem); + + for (final Collection col : space.getCollections()) { + element.addContent(col.collectionToElement()); + } + + return element; + } + + /** Deserialize a Atom workspace XML element into an object */ + protected void parseWorkspaceElement(final Element element) throws ProponoException { + final Element titleElem = element.getChild("title", AtomService.ATOM_FORMAT); + setTitle(titleElem.getText()); + if (titleElem.getAttribute("type", AtomService.ATOM_FORMAT) != null) { + setTitleType(titleElem.getAttribute("type", AtomService.ATOM_FORMAT).getValue()); + } + final List collections = element.getChildren("collection", AtomService.ATOM_PROTOCOL); + for (final Element e : collections) { + addCollection(new Collection(e)); + } + } + +} diff --git a/src/main/java/com/rometools/propono/atom/common/rome/AppModule.java b/src/main/java/com/rometools/propono/atom/common/rome/AppModule.java new file mode 100644 index 0000000..dde22be --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/common/rome/AppModule.java @@ -0,0 +1,44 @@ +/* + * Copyright 2007 Apache Software Foundation + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package com.rometools.propono.atom.common.rome; + +import java.util.Date; + +import com.rometools.rome.feed.module.Module; + +/** + * ROME Extension Module to Atom protocol extensions to Atom format. + */ +public interface AppModule extends Module { + + public static final String URI = "http://www.w3.org/2007/app"; + + /** True if entry is a draft */ + public Boolean getDraft(); + + /** Set to true if entry is a draft */ + public void setDraft(Boolean draft); + + /** Time of last edit */ + public Date getEdited(); + + /** Set time of last edit */ + public void setEdited(Date edited); +} diff --git a/src/main/java/com/rometools/propono/atom/common/rome/AppModuleGenerator.java b/src/main/java/com/rometools/propono/atom/common/rome/AppModuleGenerator.java new file mode 100644 index 0000000..7f57b6d --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/common/rome/AppModuleGenerator.java @@ -0,0 +1,87 @@ +/* + * Copyright 2007 Apache Software Foundation + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package com.rometools.propono.atom.common.rome; + +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import java.util.TimeZone; + +import org.jdom2.Element; +import org.jdom2.Namespace; + +import com.rometools.rome.feed.module.Module; +import com.rometools.rome.io.ModuleGenerator; + +/** + * Creates JDOM representation for APP Extension Module. + */ +public class AppModuleGenerator implements ModuleGenerator { + + private static final Namespace APP_NS = Namespace.getNamespace("app", AppModule.URI); + + @Override + public String getNamespaceUri() { + return AppModule.URI; + } + + private static final Set NAMESPACES; + + static { + final Set nss = new HashSet(); + nss.add(APP_NS); + NAMESPACES = Collections.unmodifiableSet(nss); + } + + /** Get namespaces associated with this module */ + @Override + public Set getNamespaces() { + return NAMESPACES; + } + + /** Generate JDOM element for module and add it to parent element */ + @Override + public void generate(final Module module, final Element parent) { + final AppModule m = (AppModule) module; + + if (m.getDraft() != null) { + final String draft = m.getDraft().booleanValue() ? "yes" : "no"; + final Element control = new Element("control", APP_NS); + control.addContent(generateSimpleElement("draft", draft)); + parent.addContent(control); + } + if (m.getEdited() != null) { + final Element edited = new Element("edited", APP_NS); + // Inclulde millis in date/time + final SimpleDateFormat dateFormater = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + dateFormater.setTimeZone(TimeZone.getTimeZone("GMT")); + edited.addContent(dateFormater.format(m.getEdited())); + parent.addContent(edited); + } + } + + private Element generateSimpleElement(final String name, final String value) { + final Element element = new Element(name, APP_NS); + element.addContent(value); + return element; + } +} diff --git a/src/main/java/com/rometools/propono/atom/common/rome/AppModuleImpl.java b/src/main/java/com/rometools/propono/atom/common/rome/AppModuleImpl.java new file mode 100644 index 0000000..c1a42e3 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/common/rome/AppModuleImpl.java @@ -0,0 +1,80 @@ +/* + * Copyright 2007 Apache Software Foundation + * Copyright 2011 The ROME Teams + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package com.rometools.propono.atom.common.rome; + +import java.util.Date; + +import com.rometools.rome.feed.CopyFrom; +import com.rometools.rome.feed.module.ModuleImpl; + +/** + * Bean representation of APP module. + */ +public class AppModuleImpl extends ModuleImpl implements AppModule { + + private static final long serialVersionUID = 1L; + + private boolean draft = false; + private Date edited = null; + + public AppModuleImpl() { + super(AppModule.class, AppModule.URI); + } + + /** True if entry is draft */ + @Override + public Boolean getDraft() { + return draft ? Boolean.TRUE : Boolean.FALSE; + } + + /** Set to true if entry is draft */ + @Override + public void setDraft(final Boolean draft) { + this.draft = draft.booleanValue(); + } + + /** Time of last edit */ + @Override + public Date getEdited() { + return edited; + } + + /** Set time of last edit */ + @Override + public void setEdited(final Date edited) { + this.edited = edited; + } + + /** Get interface class of module */ + @Override + public Class getInterface() { + return AppModule.class; + } + + /** Copy from other module */ + @Override + public void copyFrom(final CopyFrom obj) { + final AppModule m = (AppModule) obj; + setDraft(m.getDraft()); + setEdited(m.getEdited()); + } + +} diff --git a/src/main/java/com/rometools/propono/atom/common/rome/AppModuleParser.java b/src/main/java/com/rometools/propono/atom/common/rome/AppModuleParser.java new file mode 100644 index 0000000..a9d0e87 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/common/rome/AppModuleParser.java @@ -0,0 +1,72 @@ +/* + * Copyright 2007 Apache Software Foundation + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package com.rometools.propono.atom.common.rome; + +import java.util.Locale; + +import org.jdom2.Element; +import org.jdom2.Namespace; + +import com.rometools.rome.feed.module.Module; +import com.rometools.rome.io.ModuleParser; +import com.rometools.rome.io.impl.DateParser; + +/** + * Parses APP module information from a JDOM element and into AppModule form. + */ +public class AppModuleParser implements ModuleParser { + + /** Get URI of module namespace */ + @Override + public String getNamespaceUri() { + return AppModule.URI; + } + + /** Get namespace of module */ + public Namespace getContentNamespace() { + return Namespace.getNamespace(AppModule.URI); + } + + /** Parse JDOM element into module */ + @Override + public Module parse(final Element elem, final Locale locale) { + final AppModule m = new AppModuleImpl(); + final Element control = elem.getChild("control", getContentNamespace()); + if (control != null) { + final Element draftElem = control.getChild("draft", getContentNamespace()); + if (draftElem != null) { + if ("yes".equals(draftElem.getText())) { + m.setDraft(Boolean.TRUE); + } + if ("no".equals(draftElem.getText())) { + m.setDraft(Boolean.FALSE); + } + } + } + final Element edited = elem.getChild("edited", getContentNamespace()); + if (edited != null) { + try { + m.setEdited(DateParser.parseW3CDateTime(edited.getTextTrim(), locale)); + } catch (final Exception ignored) { + } + } + return m; + } +} diff --git a/src/main/java/com/rometools/propono/atom/server/AtomException.java b/src/main/java/com/rometools/propono/atom/server/AtomException.java new file mode 100644 index 0000000..c8d06e4 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/AtomException.java @@ -0,0 +1,59 @@ +/* + * Copyright 2007 Apache Software Foundation + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package com.rometools.propono.atom.server; + +import javax.servlet.http.HttpServletResponse; + +/** + * Exception thrown by {@link com.rometools.rome.propono.atom.server.AtomHandler} and extended by + * other Propono Atom exception classes. + */ +public class AtomException extends Exception { + + private static final long serialVersionUID = 1L; + + /** Construct new exception */ + public AtomException() { + super(); + } + + /** Construct new exception with message */ + public AtomException(final String msg) { + super(msg); + } + + /** Contruct new exception with message and wrapping existing exception */ + public AtomException(final String msg, final Throwable t) { + super(msg, t); + } + + /** Construct new exception to wrap existing one. */ + public AtomException(final Throwable t) { + super(t); + } + + /* Get HTTP status code associated with exception (HTTP 500 server error) */ + /** + * Get HTTP status associated with exception. + */ + public int getStatus() { + return HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + } +} diff --git a/src/main/java/com/rometools/propono/atom/server/AtomHandler.java b/src/main/java/com/rometools/propono/atom/server/AtomHandler.java new file mode 100644 index 0000000..f08762b --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/AtomHandler.java @@ -0,0 +1,150 @@ +/* + * Copyright 2007 Apache Software Foundation + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package com.rometools.propono.atom.server; + +import com.rometools.propono.atom.common.AtomService; +import com.rometools.propono.atom.common.Categories; +import com.rometools.rome.feed.atom.Entry; +import com.rometools.rome.feed.atom.Feed; + +/** + * Interface for handling single Atom protocol requests. + * + *

+ * To create your own Atom protocol implementation you must implement this interface and create a + * concrete sub-class of {@link com.rometools.rome.propono.atom.server.AtomHandlerFactory} which is + * capable of instantiating it. + *

+ */ +public interface AtomHandler { + /** + * Get username of authenticated user. Return the username of the authenticated user + */ + public String getAuthenticatedUsername(); + + /** + * Return {@link com.rometools.rome.propono.atom.common.AtomService} object that contains the + * {@link com.rometools.rome.propono.atom.common.Workspace} objects available to the currently + * authenticated user and within those the + * {@link com.rometools.rome.propono.atom.common.Collection} avalaible. + */ + public AtomService getAtomService(AtomRequest req) throws AtomException; + + /** + * Get categories, a list of Categories objects + */ + public Categories getCategories(AtomRequest req) throws AtomException; + + /** + * Return collection or portion of collection specified by request. + * + * @param req Details of HTTP request + */ + public Feed getCollection(AtomRequest req) throws AtomException; + + /** + * Store new entry in collection specified by request and return representation of entry as it + * is stored on server. + * + * @param req Details of HTTP request + * @return Location URL of new entry + */ + public Entry postEntry(AtomRequest req, Entry entry) throws AtomException; + + /** + * Get entry specified by request. + * + * @param req Details of HTTP request + */ + public Entry getEntry(AtomRequest req) throws AtomException; + + /** + * Get media resource specified by request. + * + * @param req Details of HTTP request + */ + public AtomMediaResource getMediaResource(AtomRequest req) throws AtomException; + + /** + * Update entry specified by request and return new entry as represented on the server. + * + * @param req Details of HTTP request + */ + public void putEntry(AtomRequest req, Entry entry) throws AtomException; + + /** + * Delete entry specified by request. + * + * @param req Details of HTTP request + */ + public void deleteEntry(AtomRequest req) throws AtomException; + + /** + * Store media data in collection specified by request, create an Atom media-link entry to store + * metadata for the new media file and return that entry to the caller. + * + * @param req Details of HTTP request + * @param entry New entry initialzied with only title and content type + * @return Location URL of new media entry + */ + public Entry postMedia(AtomRequest req, Entry entry) throws AtomException; + + /** + * Update the media file part of a media-link entry. + * + * @param req Details of HTTP request + */ + public void putMedia(AtomRequest req) throws AtomException; + + /** + * Return true if specified request represents URI of a Service Document. + * + * @param req Details of HTTP request + */ + public boolean isAtomServiceURI(AtomRequest req); + + /** + * Return true if specified request represents URI of a Categories Document. + * + * @param req Details of HTTP request + */ + public boolean isCategoriesURI(AtomRequest req); + + /** + * Return true if specified request represents URI of a collection. + * + * @param req Details of HTTP request + */ + public boolean isCollectionURI(AtomRequest req); + + /** + * Return true if specified request represents URI of an Atom entry. + * + * @param req Details of HTTP request + */ + public boolean isEntryURI(AtomRequest req); + + /** + * Return true if specified patrequesthinfo represents media-edit URI. + * + * @param req Details of HTTP request + */ + public boolean isMediaEditURI(AtomRequest req); +} diff --git a/src/main/java/com/rometools/propono/atom/server/AtomHandlerFactory.java b/src/main/java/com/rometools/propono/atom/server/AtomHandlerFactory.java new file mode 100644 index 0000000..3bd635f --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/AtomHandlerFactory.java @@ -0,0 +1,100 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.server; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Defines a factory that enables the {@link com.rometools.rome.propono.atom.server.AtomServlet} to + * obtain an {@link com.rometools.rome.propono.atom.server.AtomHandler} that handles an Atom + * request. + * + *

+ * To create your own Atom protocol implementation you must sub-class this class with your own + * factory that is capable of creating instances of your + * {@link com.rometools.rome.propono.atom.server.AtomHandler} impementation. + *

+ */ +public abstract class AtomHandlerFactory { + + private static final Logger LOG = LoggerFactory.getLogger(AtomHandlerFactory.class); + + private static final String DEFAULT_PROPERTY_NAME = "com.rometools.propono.atom.server.AtomHandlerFactory"; + private static final String FALLBACK_IMPL_NAME = "com.rometools.propono.atom.server.impl.FileBasedAtomHandlerFactory"; + + /* + *

Protected constructor to prevent instantiation. Use {@link #newInstance()}.

+ */ + protected AtomHandlerFactory() { + } + + /** + * Obtain a new instance of a AtomHandlerFactory. This static method creates a new + * factory instance. This method uses the following ordered lookup procedure to determine the + * AtomHandlerFactory implementation class to load: + *
    + *
  • + * Use the com.rometools.rome.propono.atom.server.AtomHandlerFactory system + * property.
  • + *
  • + * Use the properties file "/propono.properties" in the classpath. This configuration file is in + * standard java.util.Properties format and contains the fully qualified name of + * the implementation class with the key being the system property defined above. + * + * The propono.properties file is read only once by Propono and it's values are then cached for + * future use. If the file does not exist when the first attempt is made to read from it, no + * further attempts are made to check for its existence. It is not possible to change the value + * of any property in propono.properties after it has been read for the first time.
  • + *
  • + * If not available, to determine the classname. The Services API will look for a classname in + * the file: META-INF/services/com.rometools.rome.AtomHandlerFactory in jars + * available to the runtime.
  • + *
  • + * Platform default AtomHandlerFactory instance.
  • + *
+ * + * Once an application has obtained a reference to a AtomHandlerFactory it can use + * the factory to configure and obtain parser instances. + * + * @return New instance of a AtomHandlerFactory + * + * @throws FactoryConfigurationError if the implementation is not available or cannot be + * instantiated. + */ + public static AtomHandlerFactory newInstance() { + try { + return (AtomHandlerFactory) FactoryFinder.find(DEFAULT_PROPERTY_NAME, FALLBACK_IMPL_NAME); + } catch (final ConfigurationError e) { + LOG.error("An error occured while finding factory", e); + throw new FactoryConfigurationError(e.getException(), e.getMessage()); + } + } + + /** + * Creates a new instance of a {@link com.rometools.rome.propono.atom.server.AtomHandler} using + * the currently configured parameters. + * + * @return A new instance of a AtomHandler. + * + * @throws AtomConfigurationException if a AtomHandler cannot be created which satisfies the + * configuration requested. + */ + public abstract AtomHandler newAtomHandler(HttpServletRequest req, HttpServletResponse res); +} diff --git a/src/main/java/com/rometools/propono/atom/server/AtomMediaResource.java b/src/main/java/com/rometools/propono/atom/server/AtomMediaResource.java new file mode 100644 index 0000000..6b427d7 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/AtomMediaResource.java @@ -0,0 +1,95 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rometools.propono.atom.server; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.Date; + +import javax.activation.FileTypeMap; +import javax.activation.MimetypesFileTypeMap; + +/** + * Represents a media link entry. + */ +public class AtomMediaResource { + private String contentType = null; + private long contentLength = 0; + private InputStream inputStream = null; + private Date lastModified = null; + private static FileTypeMap map = null; + + static { + // TODO: figure out why PNG is missing from Java MIME types + map = FileTypeMap.getDefaultFileTypeMap(); + if (map instanceof MimetypesFileTypeMap) { + try { + ((MimetypesFileTypeMap) map).addMimeTypes("image/png png PNG"); + } catch (final Exception ignored) { + } + } + } + + public AtomMediaResource(final File resource) throws FileNotFoundException { + contentType = map.getContentType(resource.getName()); + contentLength = resource.length(); + lastModified = new Date(resource.lastModified()); + inputStream = new FileInputStream(resource); + } + + public AtomMediaResource(final String name, final long length, final Date lastModified, final InputStream is) throws FileNotFoundException { + contentType = map.getContentType(name); + contentLength = length; + this.lastModified = lastModified; + inputStream = is; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(final String contentType) { + this.contentType = contentType; + } + + public long getContentLength() { + return contentLength; + } + + public void setContentLength(final long contentLength) { + this.contentLength = contentLength; + } + + public InputStream getInputStream() { + return inputStream; + } + + public void setInputStream(final InputStream inputStream) { + this.inputStream = inputStream; + } + + public Date getLastModified() { + return lastModified; + } + + public void setLastModified(final Date lastModified) { + this.lastModified = lastModified; + } + +} diff --git a/src/main/java/com/rometools/propono/atom/server/AtomNotAuthorizedException.java b/src/main/java/com/rometools/propono/atom/server/AtomNotAuthorizedException.java new file mode 100644 index 0000000..8cd2e00 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/AtomNotAuthorizedException.java @@ -0,0 +1,58 @@ +/* + * Copyright 2007 Apache Software Foundation + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package com.rometools.propono.atom.server; + +import javax.servlet.http.HttpServletResponse; + +/** + * Exception to be thrown by AtomHandler implementations in the case that a user is not + * authorized to access a resource. + */ +public class AtomNotAuthorizedException extends AtomException { + + private static final long serialVersionUID = 1L; + + /** Construct new exception */ + public AtomNotAuthorizedException() { + super(); + } + + /** Construct new exception with message */ + public AtomNotAuthorizedException(final String msg) { + super(msg); + } + + /** Construct new exception with message and root cause */ + public AtomNotAuthorizedException(final String msg, final Throwable t) { + super(msg, t); + } + + /** Construct new exception to wrap root cause */ + public AtomNotAuthorizedException(final Throwable t) { + super(t); + } + + /** Get HTTP status code of exception (HTTP 403 unauthorized) */ + @Override + public int getStatus() { + return HttpServletResponse.SC_UNAUTHORIZED; + } + +} diff --git a/src/main/java/com/rometools/propono/atom/server/AtomNotFoundException.java b/src/main/java/com/rometools/propono/atom/server/AtomNotFoundException.java new file mode 100644 index 0000000..4655656 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/AtomNotFoundException.java @@ -0,0 +1,56 @@ +/* + * Copyright 2007 Apache Software Foundation + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package com.rometools.propono.atom.server; + +import javax.servlet.http.HttpServletResponse; + +/** + * Exception thrown by AtomHandler in that case a resource is not found. + */ +public class AtomNotFoundException extends AtomException { + + private static final long serialVersionUID = 1L; + + /** Construct new exception */ + public AtomNotFoundException() { + super(); + } + + /** Construct new exception with message */ + public AtomNotFoundException(final String msg) { + super(msg); + } + + /** Construct new exception with message and root cause */ + public AtomNotFoundException(final String msg, final Throwable t) { + super(msg, t); + } + + /** Construct new exception with root cause */ + public AtomNotFoundException(final Throwable t) { + super(t); + } + + /** Get HTTP status code associated with exception (404 not found) */ + @Override + public int getStatus() { + return HttpServletResponse.SC_NOT_FOUND; + } +} diff --git a/src/main/java/com/rometools/propono/atom/server/AtomRequest.java b/src/main/java/com/rometools/propono/atom/server/AtomRequest.java new file mode 100644 index 0000000..cb8d1a2 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/AtomRequest.java @@ -0,0 +1,137 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package com.rometools.propono.atom.server; + +import java.io.IOException; +import java.io.InputStream; +import java.security.Principal; +import java.util.Enumeration; +import java.util.Map; + +/** + * Represents HTTP request to be processed by AtomHandler. + */ +public interface AtomRequest { + + /** + * Returns any extra path information associated with the URL the client sent when it made this + * request. + */ + public String getPathInfo(); + + /** + * Returns the query string that is contained in the request URL after the path. + */ + public String getQueryString(); + + /** + * Returns the login of the user making this request, if the user has been authenticated, or + * null if the user has not been authenticated. + */ + public String getRemoteUser(); + + /** + * Returns a boolean indicating whether the authenticated user is included in the specified + * logical "role". + */ + public boolean isUserInRole(String arg0); + + /** + * Returns a java.security.Principal object containing the name of the current authenticated + * user. + */ + public Principal getUserPrincipal(); + + /** + * Returns the part of this request's URL from the protocol name up to the query string in the + * first line of the HTTP request. + */ + public String getRequestURI(); + + /** + * Reconstructs the URL the client used to make the request. + */ + public StringBuffer getRequestURL(); + + /** + * Returns the length, in bytes, of the request body and made available by the input stream, or + * -1 if the length is not known. + */ + public int getContentLength(); + + /** + * Returns the MIME type of the body of the request, or null if the type is not known. + */ + public String getContentType(); + + /** + * Returns the value of a request parameter as a String, or null if the parameter does not + * exist. + */ + public String getParameter(String arg0); + + /** + * Returns an Enumeration of String objects containing the names of the parameters contained in + * this request. + */ + public Enumeration getParameterNames(); + + /** + * Returns an array of String objects containing all of the values the given request parameter + * has, or null if the parameter does not exist. + */ + public String[] getParameterValues(String arg0); + + /** + * Returns a java.util.Map of the parameters of this request. + */ + public Map getParameterMap(); + + /** + * Retrieves the body of the request as binary data using a ServletInputStream. + */ + public InputStream getInputStream() throws IOException; + + /** + * Returns the value of the specified request header as a long value that represents a Date + * object. + */ + public long getDateHeader(String arg0); + + /** + * Returns the value of the specified request header as a String. + */ + public String getHeader(String arg0); + + /** + * Returns all the values of the specified request header as an Enumeration of String objects. + */ + public Enumeration getHeaders(String arg0); + + /** + * Returns an enumeration of all the header names this request contains. + */ + public Enumeration getHeaderNames(); + + /** + * Returns the value of the specified request header as an int. + */ + public int getIntHeader(String arg0); +} diff --git a/src/main/java/com/rometools/propono/atom/server/AtomRequestImpl.java b/src/main/java/com/rometools/propono/atom/server/AtomRequestImpl.java new file mode 100644 index 0000000..3de3fc9 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/AtomRequestImpl.java @@ -0,0 +1,139 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package com.rometools.propono.atom.server; + +import java.io.IOException; +import java.io.InputStream; +import java.security.Principal; +import java.util.Enumeration; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +/** + * Default request implementation. + */ +public class AtomRequestImpl implements AtomRequest { + + private HttpServletRequest wrapped = null; + + public AtomRequestImpl(final HttpServletRequest wrapped) { + this.wrapped = wrapped; + } + + @Override + public String getPathInfo() { + return wrapped.getPathInfo() != null ? wrapped.getPathInfo() : ""; + } + + @Override + public String getQueryString() { + return wrapped.getQueryString(); + } + + @Override + public String getRemoteUser() { + return wrapped.getRemoteUser(); + } + + @Override + public boolean isUserInRole(final String arg0) { + return wrapped.isUserInRole(arg0); + } + + @Override + public Principal getUserPrincipal() { + return wrapped.getUserPrincipal(); + } + + @Override + public String getRequestURI() { + return wrapped.getRequestURI(); + } + + @Override + public StringBuffer getRequestURL() { + return wrapped.getRequestURL(); + } + + @Override + public int getContentLength() { + return wrapped.getContentLength(); + } + + @Override + public String getContentType() { + return wrapped.getContentType(); + } + + @Override + public String getParameter(final String arg0) { + return wrapped.getParameter(arg0); + } + + @Override + @SuppressWarnings("unchecked") + public Enumeration getParameterNames() { + return wrapped.getParameterNames(); + } + + @Override + public String[] getParameterValues(final String arg0) { + return wrapped.getParameterValues(arg0); + } + + @Override + @SuppressWarnings("unchecked") + public Map getParameterMap() { + return wrapped.getParameterMap(); + } + + @Override + public InputStream getInputStream() throws IOException { + return wrapped.getInputStream(); + } + + @Override + public long getDateHeader(final String arg0) { + return wrapped.getDateHeader(arg0); + } + + @Override + public String getHeader(final String arg0) { + return wrapped.getHeader(arg0); + } + + @Override + @SuppressWarnings("unchecked") + public Enumeration getHeaders(final String arg0) { + return wrapped.getHeaders(arg0); + } + + @Override + @SuppressWarnings("unchecked") + public Enumeration getHeaderNames() { + return wrapped.getHeaderNames(); + } + + @Override + public int getIntHeader(final String arg0) { + return wrapped.getIntHeader(arg0); + } +} diff --git a/src/main/java/com/rometools/propono/atom/server/AtomServlet.java b/src/main/java/com/rometools/propono/atom/server/AtomServlet.java new file mode 100644 index 0000000..9933f33 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/AtomServlet.java @@ -0,0 +1,371 @@ +/* + * Copyright 2007 Apache Software Foundation + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package com.rometools.propono.atom.server; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Writer; +import java.util.Collections; +import java.util.Locale; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.jdom2.Document; +import org.jdom2.output.Format; +import org.jdom2.output.XMLOutputter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.rometools.propono.atom.common.AtomService; +import com.rometools.propono.atom.common.Categories; +import com.rometools.propono.utils.Utilities; +import com.rometools.rome.feed.atom.Content; +import com.rometools.rome.feed.atom.Entry; +import com.rometools.rome.feed.atom.Feed; +import com.rometools.rome.feed.atom.Link; +import com.rometools.rome.io.WireFeedOutput; +import com.rometools.rome.io.impl.Atom10Generator; +import com.rometools.rome.io.impl.Atom10Parser; + +/** + * Atom Servlet implements Atom protocol by calling an + * {@link com.rometools.rome.propono.atom.server.AtomHandler} implementation. This servlet takes + * care of parsing incoming XML into ROME Atom {@link com.rometools.rome.feed.atom.Entry} objects, + * passing those to the handler and serializing to the response the entries and feeds returned by + * the handler. + */ +public class AtomServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + /** + * Get feed type support by Servlet, "atom_1.0" + */ + public static final String FEED_TYPE = "atom_1.0"; + private static String contextDirPath = null; + + private static final Logger LOG = LoggerFactory.getLogger(AtomServlet.class); + + static { + Atom10Parser.setResolveURIs(true); + } + + // ----------------------------------------------------------------------------- + /** + * Create an Atom request handler. TODO: make AtomRequestHandler implementation configurable. + */ + private AtomHandler createAtomRequestHandler(final HttpServletRequest request, final HttpServletResponse response) throws ServletException { + final AtomHandlerFactory ahf = AtomHandlerFactory.newInstance(); + return ahf.newAtomHandler(request, response); + } + + // ----------------------------------------------------------------------------- + /** + * Handles an Atom GET by calling handler and writing results to response. + */ + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse res) throws ServletException, IOException { + LOG.debug("Entering"); + final AtomHandler handler = createAtomRequestHandler(req, res); + final String userName = handler.getAuthenticatedUsername(); + if (userName != null) { + final AtomRequest areq = new AtomRequestImpl(req); + try { + if (handler.isAtomServiceURI(areq)) { + // return an Atom Service document + final AtomService service = handler.getAtomService(areq); + final Document doc = service.serviceToDocument(); + res.setContentType("application/atomsvc+xml; charset=utf-8"); + final Writer writer = res.getWriter(); + final XMLOutputter outputter = new XMLOutputter(); + outputter.setFormat(Format.getPrettyFormat()); + outputter.output(doc, writer); + writer.close(); + res.setStatus(HttpServletResponse.SC_OK); + } else if (handler.isCategoriesURI(areq)) { + final Categories cats = handler.getCategories(areq); + res.setContentType("application/xml"); + final Writer writer = res.getWriter(); + final Document catsDoc = new Document(); + catsDoc.setRootElement(cats.categoriesToElement()); + final XMLOutputter outputter = new XMLOutputter(); + outputter.output(catsDoc, writer); + writer.close(); + res.setStatus(HttpServletResponse.SC_OK); + } else if (handler.isCollectionURI(areq)) { + // return a collection + final Feed col = handler.getCollection(areq); + col.setFeedType(FEED_TYPE); + final WireFeedOutput wireFeedOutput = new WireFeedOutput(); + final Document feedDoc = wireFeedOutput.outputJDom(col); + res.setContentType("application/atom+xml; charset=utf-8"); + final Writer writer = res.getWriter(); + final XMLOutputter outputter = new XMLOutputter(); + outputter.setFormat(Format.getPrettyFormat()); + outputter.output(feedDoc, writer); + writer.close(); + res.setStatus(HttpServletResponse.SC_OK); + } else if (handler.isEntryURI(areq)) { + // return an entry + final Entry entry = handler.getEntry(areq); + if (entry != null) { + res.setContentType("application/atom+xml; type=entry; charset=utf-8"); + final Writer writer = res.getWriter(); + Atom10Generator.serializeEntry(entry, writer); + writer.close(); + } else { + res.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + } else if (handler.isMediaEditURI(areq)) { + final AtomMediaResource entry = handler.getMediaResource(areq); + res.setContentType(entry.getContentType()); + res.setContentLength((int) entry.getContentLength()); + Utilities.copyInputToOutput(entry.getInputStream(), res.getOutputStream()); + res.getOutputStream().flush(); + res.getOutputStream().close(); + } else { + res.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + } catch (final AtomException ae) { + res.sendError(ae.getStatus(), ae.getMessage()); + LOG.debug("An error occured while processing GET", ae); + } catch (final Exception e) { + res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); + LOG.debug("An error occured while processing GET", e); + } + } else { + res.setHeader("WWW-Authenticate", "BASIC realm=\"AtomPub\""); + res.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } + LOG.debug("Exiting"); + } + + // ----------------------------------------------------------------------------- + /** + * Handles an Atom POST by calling handler to identify URI, reading/parsing data, calling + * handler and writing results to response. + */ + @Override + protected void doPost(final HttpServletRequest req, final HttpServletResponse res) throws ServletException, IOException { + LOG.debug("Entering"); + final AtomHandler handler = createAtomRequestHandler(req, res); + final String userName = handler.getAuthenticatedUsername(); + if (userName != null) { + final AtomRequest areq = new AtomRequestImpl(req); + try { + if (handler.isCollectionURI(areq)) { + + if (req.getContentType().startsWith("application/atom+xml")) { + + // parse incoming entry + final Entry entry = Atom10Parser.parseEntry(new BufferedReader(new InputStreamReader(req.getInputStream(), "UTF-8")), null, Locale.US); + + // call handler to post it + final Entry newEntry = handler.postEntry(areq, entry); + + // set Location and Content-Location headers + for (final Object element : newEntry.getOtherLinks()) { + final Link link = (Link) element; + if ("edit".equals(link.getRel())) { + res.addHeader("Location", link.getHrefResolved()); + break; + } + } + for (final Object element : newEntry.getAlternateLinks()) { + final Link link = (Link) element; + if ("alternate".equals(link.getRel())) { + res.addHeader("Content-Location", link.getHrefResolved()); + break; + } + } + + // write entry back out to response + res.setStatus(HttpServletResponse.SC_CREATED); + res.setContentType("application/atom+xml; type=entry; charset=utf-8"); + + final Writer writer = res.getWriter(); + Atom10Generator.serializeEntry(newEntry, writer); + writer.close(); + + } else if (req.getContentType() != null) { + + // get incoming title and slug from HTTP header + final String title = areq.getHeader("Title"); + + // create new entry for resource, set title and type + final Entry resource = new Entry(); + resource.setTitle(title); + final Content content = new Content(); + content.setType(areq.getContentType()); + resource.setContents(Collections.singletonList(content)); + + // hand input stream off to hander to post file + final Entry newEntry = handler.postMedia(areq, resource); + + // set Location and Content-Location headers + for (final Object element : newEntry.getOtherLinks()) { + final Link link = (Link) element; + if ("edit".equals(link.getRel())) { + res.addHeader("Location", link.getHrefResolved()); + break; + } + } + for (final Object element : newEntry.getAlternateLinks()) { + final Link link = (Link) element; + if ("alternate".equals(link.getRel())) { + res.addHeader("Content-Location", link.getHrefResolved()); + break; + } + } + + res.setStatus(HttpServletResponse.SC_CREATED); + res.setContentType("application/atom+xml; type=entry; charset=utf-8"); + + final Writer writer = res.getWriter(); + Atom10Generator.serializeEntry(newEntry, writer); + writer.close(); + + } else { + res.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, "No content-type specified in request"); + } + + } else { + res.sendError(HttpServletResponse.SC_NOT_FOUND, "Invalid collection specified in request"); + } + } catch (final AtomException ae) { + res.sendError(ae.getStatus(), ae.getMessage()); + LOG.debug("An error occured while processing POST", ae); + } catch (final Exception e) { + res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); + LOG.debug("An error occured while processing POST", e); + } + } else { + res.setHeader("WWW-Authenticate", "BASIC realm=\"AtomPub\""); + res.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } + LOG.debug("Exiting"); + } + + // ----------------------------------------------------------------------------- + /** + * Handles an Atom PUT by calling handler to identify URI, reading/parsing data, calling handler + * and writing results to response. + */ + @Override + protected void doPut(final HttpServletRequest req, final HttpServletResponse res) throws ServletException, IOException { + LOG.debug("Entering"); + final AtomHandler handler = createAtomRequestHandler(req, res); + final String userName = handler.getAuthenticatedUsername(); + if (userName != null) { + final AtomRequest areq = new AtomRequestImpl(req); + try { + if (handler.isEntryURI(areq)) { + + // parse incoming entry + final Entry unsavedEntry = Atom10Parser.parseEntry(new BufferedReader(new InputStreamReader(req.getInputStream(), "UTF-8")), null, + Locale.US); + + // call handler to put entry + handler.putEntry(areq, unsavedEntry); + + res.setStatus(HttpServletResponse.SC_OK); + + } else if (handler.isMediaEditURI(areq)) { + + // hand input stream to handler + handler.putMedia(areq); + + res.setStatus(HttpServletResponse.SC_OK); + + } else { + res.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + } catch (final AtomException ae) { + res.sendError(ae.getStatus(), ae.getMessage()); + LOG.debug("An error occured while processing PUT", ae); + } catch (final Exception e) { + res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); + LOG.debug("An error occured while processing PUT", e); + } + } else { + res.setHeader("WWW-Authenticate", "BASIC realm=\"AtomPub\""); + // Wanted to use sendError() here but Tomcat sends 403 forbidden + // when I do that, so sticking with setStatus() for time being. + res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + LOG.debug("Exiting"); + } + + // ----------------------------------------------------------------------------- + /** + * Handle Atom DELETE by calling appropriate handler. + */ + @Override + protected void doDelete(final HttpServletRequest req, final HttpServletResponse res) throws ServletException, IOException { + LOG.debug("Entering"); + final AtomHandler handler = createAtomRequestHandler(req, res); + final String userName = handler.getAuthenticatedUsername(); + if (userName != null) { + final AtomRequest areq = new AtomRequestImpl(req); + try { + if (handler.isEntryURI(areq)) { + handler.deleteEntry(areq); + res.setStatus(HttpServletResponse.SC_OK); + } else { + res.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + } catch (final AtomException ae) { + res.sendError(ae.getStatus(), ae.getMessage()); + LOG.debug("An error occured while processing DELETE", ae); + } catch (final Exception e) { + res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); + LOG.debug("An error occured while processing DELETE", e); + } + } else { + res.setHeader("WWW-Authenticate", "BASIC realm=\"AtomPub\""); + // Wanted to use sendError() here but Tomcat sends 403 forbidden + // when I do that, so sticking with setStatus() for time being. + res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + LOG.debug("Exiting"); + } + + /** + * Initialize servlet. + */ + @Override + public void init(final ServletConfig config) throws ServletException { + super.init(config); + contextDirPath = getServletContext().getRealPath("/"); + + } + + /** + * Get absolute path to Servlet context directory. + */ + public static String getContextDirPath() { + return contextDirPath; + } +} diff --git a/src/main/java/com/rometools/propono/atom/server/ConfigurationError.java b/src/main/java/com/rometools/propono/atom/server/ConfigurationError.java new file mode 100644 index 0000000..1aca365 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/ConfigurationError.java @@ -0,0 +1,21 @@ +package com.rometools.propono.atom.server; + +class ConfigurationError extends Error { + + private static final long serialVersionUID = 1L; + + private final Exception exception; + + /** + * Construct a new instance with the specified detail string and exception. + */ + ConfigurationError(final String msg, final Exception x) { + super(msg); + exception = x; + } + + Exception getException() { + return exception; + } + +} \ No newline at end of file diff --git a/src/main/java/com/rometools/propono/atom/server/FactoryConfigurationError.java b/src/main/java/com/rometools/propono/atom/server/FactoryConfigurationError.java new file mode 100644 index 0000000..62a1b98 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/FactoryConfigurationError.java @@ -0,0 +1,101 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.server; + +/** + * Thrown when a problem with configuration with the + * {@link com.rometools.rome.propono.atom.server.AtomHandlerFactory} exists. This error will + * typically be thrown when the class of a parser factory specified in the system properties cannot + * be found or instantiated. + */ +public class FactoryConfigurationError extends Error { + + private static final long serialVersionUID = 1L; + + /** + * Exception that represents the error. + */ + private final Exception exception; + + /** + * Create a new FactoryConfigurationError with no detail mesage. + */ + public FactoryConfigurationError() { + super(); + exception = null; + } + + /** + * Create a new FactoryConfigurationError with the String specified + * as an error message. + * + * @param msg The error message for the exception. + */ + public FactoryConfigurationError(final String msg) { + super(msg); + exception = null; + } + + /** + * Create a new FactoryConfigurationError with a given Exception base + * cause of the error. + * + * @param e The exception to be encapsulated in a FactoryConfigurationError. + */ + public FactoryConfigurationError(final Exception e) { + super(e.toString()); + exception = e; + } + + /** + * Create a new FactoryConfigurationError with the given Exception + * base cause and detail message. + * + * @param e The exception to be encapsulated in a FactoryConfigurationError + * @param msg The detail message. + */ + public FactoryConfigurationError(final Exception e, final String msg) { + super(msg); + exception = e; + } + + /** + * Return the message (if any) for this error . If there is no message for the exception and + * there is an encapsulated exception then the message of that exception, if it exists will be + * returned. Else the name of the encapsulated exception will be returned. + * + * @return The error message. + */ + @Override + public String getMessage() { + final String message = super.getMessage(); + + if (message == null && exception != null) { + return exception.getMessage(); + } + + return message; + } + + /** + * Return the actual exception (if any) that caused this exception to be raised. + * + * @return The encapsulated exception, or null if there is none. + */ + public Exception getException() { + return exception; + } +} diff --git a/src/main/java/com/rometools/propono/atom/server/FactoryFinder.java b/src/main/java/com/rometools/propono/atom/server/FactoryFinder.java new file mode 100644 index 0000000..12f0b05 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/FactoryFinder.java @@ -0,0 +1,248 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.server; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Properties; + +/** + * Find {@link com.rometools.rome.propono.atom.server.AtomHandlerFactory} based on properties + * files. + */ +class FactoryFinder { + + private static boolean debug = false; + private static Properties cacheProps = new Properties(); + private static SecuritySupport ss = new SecuritySupport(); + private static boolean firstTime = true; + + private static void dPrint(final String msg) { + if (debug) { + System.err.println("Propono: " + msg); + } + } + + /** + * Create an instance of a class using the specified ClassLoader and optionally fall back to the + * current ClassLoader if not found. + * + * @param className Name of the concrete class corresponding to the service provider + * + * @param cl ClassLoader to use to load the class, null means to use the bootstrap ClassLoader + * + * @param doFallback true if the current ClassLoader should be tried as a fallback if the class + * is not found using cl + */ + private static Object newInstance(final String className, ClassLoader cl, final boolean doFallback) throws ConfigurationError { + + try { + Class providerClass; + if (cl == null) { + // If classloader is null Use the bootstrap ClassLoader. + // Thus Class.forName(String) will use the current + // ClassLoader which will be the bootstrap ClassLoader. + providerClass = Class.forName(className); + } else { + try { + providerClass = cl.loadClass(className); + } catch (final ClassNotFoundException x) { + if (doFallback) { + // Fall back to current classloader + cl = FactoryFinder.class.getClassLoader(); + providerClass = cl.loadClass(className); + } else { + throw x; + } + } + } + + final Object instance = providerClass.newInstance(); + dPrint("created new instance of " + providerClass + " using ClassLoader: " + cl); + return instance; + } catch (final ClassNotFoundException x) { + throw new ConfigurationError("Provider " + className + " not found", x); + } catch (final Exception x) { + throw new ConfigurationError("Provider " + className + " could not be instantiated: " + x, x); + } + } + + /** + * Finds the implementation Class object in the specified order. Main entry point. + * + * @return Class object of factory, never null + * + * @param factoryId Name of the factory to find, same as a property name + * @param fallbackClassName Implementation class name, if nothing else is found. Use null to + * mean no fallback. + * + * Package private so this code can be shared. + */ + static Object find(final String factoryId, final String fallbackClassName) throws ConfigurationError { + + // Figure out which ClassLoader to use for loading the provider + // class. If there is a Context ClassLoader then use it. + + ClassLoader classLoader = ss.getContextClassLoader(); + + if (classLoader == null) { + // if we have no Context ClassLoader + // so use the current ClassLoader + classLoader = FactoryFinder.class.getClassLoader(); + } + + dPrint("find factoryId =" + factoryId); + + // Use the system property first + try { + final String systemProp = ss.getSystemProperty(factoryId); + if (systemProp != null) { + dPrint("found system property, value=" + systemProp); + return newInstance(systemProp, classLoader, true); + } + } catch (final SecurityException se) { + // if first option fails due to any reason we should try next option in the + // look up algorithm. + } + + // try to read from /propono.properties + try { + final String configFile = "/propono.properties"; + String factoryClassName = null; + if (firstTime) { + synchronized (cacheProps) { + if (firstTime) { + try { + final InputStream is = FactoryFinder.class.getResourceAsStream(configFile); + firstTime = false; + if (is != null) { + dPrint("Read properties file: " + configFile); + cacheProps.load(is); + } + } catch (final Exception intentionallyIgnored) { + } + } + } + } + factoryClassName = cacheProps.getProperty(factoryId); + + if (factoryClassName != null) { + dPrint("found in $java.home/propono.properties, value=" + factoryClassName); + return newInstance(factoryClassName, classLoader, true); + } + } catch (final Exception ex) { + if (debug) { + ex.printStackTrace(); + } + } + + // Try Jar Service Provider Mechanism + final Object provider = findJarServiceProvider(factoryId); + if (provider != null) { + return provider; + } + if (fallbackClassName == null) { + throw new ConfigurationError("Provider for " + factoryId + " cannot be found", null); + } + + dPrint("loaded from fallback value: " + fallbackClassName); + return newInstance(fallbackClassName, classLoader, true); + } + + /* + * Try to find provider using Jar Service Provider Mechanism + * @return instance of provider class if found or null + */ + private static Object findJarServiceProvider(final String factoryId) throws ConfigurationError { + + final String serviceId = "META-INF/services/" + factoryId; + InputStream is = null; + + // First try the Context ClassLoader + ClassLoader cl = ss.getContextClassLoader(); + if (cl != null) { + is = ss.getResourceAsStream(cl, serviceId); + + // If no provider found then try the current ClassLoader + if (is == null) { + cl = FactoryFinder.class.getClassLoader(); + is = ss.getResourceAsStream(cl, serviceId); + } + } else { + // No Context ClassLoader, try the current + // ClassLoader + cl = FactoryFinder.class.getClassLoader(); + is = ss.getResourceAsStream(cl, serviceId); + } + + if (is == null) { + // No provider found + return null; + } + + dPrint("found jar resource=" + serviceId + " using ClassLoader: " + cl); + + // Read the service provider name in UTF-8 as specified in + // the jar spec. Unfortunately this fails in Microsoft + // VJ++, which does not implement the UTF-8 + // encoding. Theoretically, we should simply let it fail in + // that case, since the JVM is obviously broken if it + // doesn't support such a basic standard. But since there + // are still some users attempting to use VJ++ for + // development, we have dropped in a fallback which makes a + // second attempt using the platform's default encoding. In + // VJ++ this is apparently ASCII, which is a subset of + // UTF-8... and since the strings we'll be reading here are + // also primarily limited to the 7-bit ASCII range (at + // least, in English versions), this should work well + // enough to keep us on the air until we're ready to + // officially decommit from VJ++. [Edited comment from + // jkesselm] + BufferedReader rd; + try { + rd = new BufferedReader(new InputStreamReader(is, "UTF-8")); + } catch (final java.io.UnsupportedEncodingException e) { + rd = new BufferedReader(new InputStreamReader(is)); + } + + String factoryClassName = null; + try { + // XXX Does not handle all possible input as specified by the + // Jar Service Provider specification + factoryClassName = rd.readLine(); + rd.close(); + } catch (final IOException x) { + // No provider found + return null; + } + + if (factoryClassName != null && !"".equals(factoryClassName)) { + dPrint("found in resource, value=" + factoryClassName); + + // Note: here we do not want to fall back to the current + // ClassLoader because we want to avoid the case where the + // resource file was found using one ClassLoader and the + // provider class was instantiated using a different one. + return newInstance(factoryClassName, cl, false); + } + + // No provider found + return null; + } + +} diff --git a/src/main/java/com/rometools/propono/atom/server/SecuritySupport.java b/src/main/java/com/rometools/propono/atom/server/SecuritySupport.java new file mode 100644 index 0000000..5f7b2b4 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/SecuritySupport.java @@ -0,0 +1,98 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.server; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; + +/** + * This class is duplicated for each subpackage, it is package private and therefore is not exposed + * as part of the public API. + */ +class SecuritySupport { + + ClassLoader getContextClassLoader() { + final PrivilegedAction action = new PrivilegedAction() { + @Override + public ClassLoader run() { + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + } catch (final SecurityException ex) { + } + return cl; + } + }; + return AccessController.doPrivileged(action); + } + + String getSystemProperty(final String propName) { + final PrivilegedAction action = new PrivilegedAction() { + @Override + public String run() { + return System.getProperty(propName); + } + }; + return AccessController.doPrivileged(action); + } + + FileInputStream getFileInputStream(final File file) throws FileNotFoundException { + try { + final PrivilegedExceptionAction action = new PrivilegedExceptionAction() { + @Override + public FileInputStream run() throws FileNotFoundException { + return new FileInputStream(file); + } + }; + return AccessController.doPrivileged(action); + } catch (final PrivilegedActionException e) { + throw (FileNotFoundException) e.getException(); + } + } + + InputStream getResourceAsStream(final ClassLoader cl, final String name) { + final PrivilegedAction action = new PrivilegedAction() { + @Override + public InputStream run() { + InputStream ris; + if (cl == null) { + ris = ClassLoader.getSystemResourceAsStream(name); + } else { + ris = cl.getResourceAsStream(name); + } + return ris; + } + }; + return AccessController.doPrivileged(action); + } + + boolean doesFileExist(final File f) { + final PrivilegedAction action = new PrivilegedAction() { + @Override + public Boolean run() { + return f.exists(); + } + }; + return AccessController.doPrivileged(action).booleanValue(); + } + +} diff --git a/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomHandler.java b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomHandler.java new file mode 100644 index 0000000..dc754da --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomHandler.java @@ -0,0 +1,469 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.server.impl; + +import java.io.File; +import java.util.StringTokenizer; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.rometools.propono.atom.common.AtomService; +import com.rometools.propono.atom.common.Categories; +import com.rometools.propono.atom.server.AtomException; +import com.rometools.propono.atom.server.AtomHandler; +import com.rometools.propono.atom.server.AtomMediaResource; +import com.rometools.propono.atom.server.AtomRequest; +import com.rometools.propono.atom.server.AtomServlet; +import com.rometools.rome.feed.atom.Entry; +import com.rometools.rome.feed.atom.Feed; + +/** + * File-based {@link com.rometools.rome.propono.atom.server.AtomHandler} implementation that stores + * entries and media-entries to disk. Implemented using + * {@link com.rometools.rome.propono.atom.server.impl.FileBasedAtomService}. + */ +public class FileBasedAtomHandler implements AtomHandler { + + private static final Logger LOG = LoggerFactory.getLogger(FileBasedAtomHandler.class); + + private String userName = null; + private String atomProtocolURL = null; + private String contextURI = null; + private FileBasedAtomService service = null; + + /** + * Construct handler to handle one request. + * + * @param req Request to be handled. + */ + public FileBasedAtomHandler(final HttpServletRequest req) { + this(req, AtomServlet.getContextDirPath()); + } + + /** + * Contruct handler for one request, using specified file storage directory. + * + * @param req Request to be handled. + * @param uploaddir File storage upload dir. + */ + public FileBasedAtomHandler(final HttpServletRequest req, final String uploaddir) { + LOG.debug("ctor"); + + userName = authenticateBASIC(req); + + atomProtocolURL = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + req.getContextPath() + req.getServletPath(); + + contextURI = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + req.getContextPath(); + + try { + service = new FileBasedAtomService(userName, uploaddir, contextURI, req.getContextPath(), req.getServletPath()); + } catch (final Throwable t) { + throw new RuntimeException("ERROR creating FileBasedAtomService", t); + } + } + + /** + * Method used for validating user. Developers can overwrite this method and use credentials + * stored in Database or LDAP to confirm if the user is allowed to access this service. + * + * @param login user submitted login id + * @param password user submitted password + */ + public boolean validateUser(final String login, final String password) { + return true; + } + + /** + * Get username of authenticated user + * + * @return User name. + */ + @Override + public String getAuthenticatedUsername() { + // For now return userName as the login id entered for authorization + return userName; + } + + /** + * Get base URI of Atom protocol implementation. + * + * @return Base URI of Atom protocol implemenation. + */ + public String getAtomProtocolURL() { + if (atomProtocolURL == null) { + return "app"; + } else { + return atomProtocolURL; + } + } + + /** + * Return introspection document + * + * @throws com.rometools.rome.propono.atom.server.AtomException Unexpected exception. + * @return AtomService object with workspaces and collections. + */ + @Override + public AtomService getAtomService(final AtomRequest areq) throws AtomException { + return service; + } + + /** + * Returns null because we use in-line categories. + * + * @throws com.rometools.rome.propono.atom.server.AtomException Unexpected exception. + * @return Categories object + */ + @Override + public Categories getCategories(final AtomRequest areq) throws AtomException { + LOG.debug("getCollection"); + final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/"); + final String handle = pathInfo[0]; + final String collection = pathInfo[1]; + final FileBasedCollection col = service.findCollectionByHandle(handle, collection); + return col.getCategories(true).get(0); + } + + /** + * Get collection specified by pathinfo. + * + * @param areq Details of HTTP request + * @return ROME feed representing collection. + * @throws com.rometools.rome.propono.atom.server.AtomException Invalid collection or other + * exception. + */ + @Override + public Feed getCollection(final AtomRequest areq) throws AtomException { + LOG.debug("getCollection"); + final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/"); + final String handle = pathInfo[0]; + final String collection = pathInfo[1]; + final FileBasedCollection col = service.findCollectionByHandle(handle, collection); + return col.getFeedDocument(); + } + + /** + * Create a new entry specified by pathInfo and posted entry. We save the submitted Atom entry + * verbatim, but we do set the id and reset the update time. + * + * @param entry Entry to be added to collection. + * @param areq Details of HTTP request + * @throws com.rometools.rome.propono.atom.server.AtomException On invalid collection or other + * error. + * @return Entry as represented on server. + */ + @Override + public Entry postEntry(final AtomRequest areq, final Entry entry) throws AtomException { + LOG.debug("postEntry"); + + final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/"); + final String handle = pathInfo[0]; + final String collection = pathInfo[1]; + final FileBasedCollection col = service.findCollectionByHandle(handle, collection); + try { + return col.addEntry(entry); + + } catch (final Exception fe) { + fe.printStackTrace(); + throw new AtomException(fe); + } + } + + /** + * Get entry specified by pathInfo. + * + * @param areq Details of HTTP request + * @throws com.rometools.rome.propono.atom.server.AtomException On invalid pathinfo or other + * error. + * @return ROME Entry object. + */ + @Override + public Entry getEntry(final AtomRequest areq) throws AtomException { + LOG.debug("getEntry"); + final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/"); + final String handle = pathInfo[0]; + final String collection = pathInfo[1]; + final String fileName = pathInfo[2]; + final FileBasedCollection col = service.findCollectionByHandle(handle, collection); + try { + return col.getEntry(fileName); + + } catch (final Exception re) { + if (re instanceof AtomException) { + throw (AtomException) re; + } + throw new AtomException("ERROR: getting entry", re); + } + } + + /** + * Update entry specified by pathInfo and posted entry. + * + * @param entry + * @param areq Details of HTTP request + * @throws com.rometools.rome.propono.atom.server.AtomException + */ + @Override + public void putEntry(final AtomRequest areq, final Entry entry) throws AtomException { + LOG.debug("putEntry"); + final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/"); + final String handle = pathInfo[0]; + final String collection = pathInfo[1]; + final String fileName = pathInfo[2]; + final FileBasedCollection col = service.findCollectionByHandle(handle, collection); + try { + col.updateEntry(entry, fileName); + + } catch (final Exception fe) { + throw new AtomException(fe); + } + } + + /** + * Delete entry specified by pathInfo. + * + * @param areq Details of HTTP request + */ + @Override + public void deleteEntry(final AtomRequest areq) throws AtomException { + LOG.debug("deleteEntry"); + final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/"); + final String handle = pathInfo[0]; + final String collection = pathInfo[1]; + final String fileName = pathInfo[2]; + final FileBasedCollection col = service.findCollectionByHandle(handle, collection); + try { + col.deleteEntry(fileName); + + } catch (final Exception e) { + final String msg = "ERROR in atom.deleteResource"; + LOG.error(msg, e); + throw new AtomException(msg); + } + } + + /** + * Store media data in collection specified by pathInfo, create an Atom media-link entry to + * store metadata for the new media file and return that entry to the caller. + * + * @param areq Details of HTTP request + * @param entry New entry initialzied with only title and content type + * @return Location URL of new media entry + */ + @Override + public Entry postMedia(final AtomRequest areq, final Entry entry) throws AtomException { + + // get incoming slug from HTTP header + final String slug = areq.getHeader("Slug"); + + if (LOG.isDebugEnabled()) { + LOG.debug("postMedia - title: " + entry.getTitle() + " slug:" + slug); + } + + try { + final File tempFile = null; + final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/"); + final String handle = pathInfo[0]; + final String collection = pathInfo[1]; + final FileBasedCollection col = service.findCollectionByHandle(handle, collection); + try { + col.addMediaEntry(entry, slug, areq.getInputStream()); + + } catch (final Exception e) { + e.printStackTrace(); + final String msg = "ERROR reading posted file"; + LOG.error(msg, e); + throw new AtomException(msg, e); + } finally { + if (tempFile != null) { + tempFile.delete(); + } + } + + } catch (final Exception re) { + throw new AtomException("ERROR: posting media"); + } + return entry; + } + + /** + * Update the media file part of a media-link entry. + * + * @param areq Details of HTTP request Assuming pathInfo of form /user-name/resource/name + */ + @Override + public void putMedia(final AtomRequest areq) throws AtomException { + + LOG.debug("putMedia"); + final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/"); + final String handle = pathInfo[0]; + final String collection = pathInfo[1]; + final String fileName = pathInfo[3]; + final FileBasedCollection col = service.findCollectionByHandle(handle, collection); + try { + col.updateMediaEntry(fileName, areq.getContentType(), areq.getInputStream()); + + } catch (final Exception re) { + throw new AtomException("ERROR: posting media"); + } + } + + @Override + public AtomMediaResource getMediaResource(final AtomRequest areq) throws AtomException { + LOG.debug("putMedia"); + final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/"); + final String handle = pathInfo[0]; + final String collection = pathInfo[1]; + final String fileName = pathInfo[3]; + final FileBasedCollection col = service.findCollectionByHandle(handle, collection); + try { + return col.getMediaResource(fileName); + + } catch (final Exception re) { + throw new AtomException("ERROR: posting media"); + } + } + + /** + * Return true if specified pathinfo represents URI of service doc. + */ + @Override + public boolean isAtomServiceURI(final AtomRequest areq) { + final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/"); + if (pathInfo.length == 0) { + return true; + } + return false; + } + + /** + * Return true if specified pathinfo represents URI of category doc. + */ + @Override + public boolean isCategoriesURI(final AtomRequest areq) { + LOG.debug("isCategoriesDocumentURI"); + final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/"); + if (pathInfo.length == 3 && "categories".equals(pathInfo[2])) { + return true; + } + return false; + } + + /** + * Return true if specified pathinfo represents URI of a collection. + */ + @Override + public boolean isCollectionURI(final AtomRequest areq) { + LOG.debug("isCollectionURI"); + // workspace/collection-plural + // if length is 2 and points to a valid collection then YES + final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/"); + if (pathInfo.length == 2) { + final String handle = pathInfo[0]; + final String collection = pathInfo[1]; + if (service.findCollectionByHandle(handle, collection) != null) { + return true; + } + } + return false; + + } + + /** + * Return true if specified pathinfo represents URI of an Atom entry. + */ + @Override + public boolean isEntryURI(final AtomRequest areq) { + LOG.debug("isEntryURI"); + // workspace/collection-singular/fsid + // if length is 3 and points to a valid collection then YES + final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/"); + if (pathInfo.length == 3) { + final String handle = pathInfo[0]; + final String collection = pathInfo[1]; + if (service.findCollectionByHandle(handle, collection) != null) { + return true; + } + } + return false; + } + + /** + * Return true if specified pathinfo represents media-edit URI. + */ + @Override + public boolean isMediaEditURI(final AtomRequest areq) { + LOG.debug("isMediaEditURI"); + // workspace/collection-singular/fsid/media/fsid + // if length is 4, points to a valid collection and fsid is mentioned + // twice then YES + final String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/"); + if (pathInfo.length == 4) { + final String handle = pathInfo[0]; + final String collection = pathInfo[1]; + final String media = pathInfo[2]; + // final String fsid = pathInfo[3]; + if (service.findCollectionByHandle(handle, collection) != null && media.equals("media")) { + return true; + } + } + return false; + + } + + /** + * BASIC authentication. + */ + public String authenticateBASIC(final HttpServletRequest request) { + LOG.debug("authenticateBASIC"); + boolean valid = false; + String userID = null; + String password = null; + try { + final String authHeader = request.getHeader("Authorization"); + if (authHeader != null) { + final StringTokenizer st = new StringTokenizer(authHeader); + if (st.hasMoreTokens()) { + final String basic = st.nextToken(); + if (basic.equalsIgnoreCase("Basic")) { + final String credentials = st.nextToken(); + final String userPass = new String(Base64.decodeBase64(credentials.getBytes())); + final int p = userPass.indexOf(":"); + if (p != -1) { + userID = userPass.substring(0, p); + password = userPass.substring(p + 1); + + // Validate the User. + valid = validateUser(userID, password); + } + } + } + } + } catch (final Exception e) { + LOG.debug("An error occured while processing Basic authentication", e); + } + if (valid) { + // For now assume userID as userName + return userID; + } + return null; + } +} diff --git a/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomHandlerFactory.java b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomHandlerFactory.java new file mode 100644 index 0000000..f4cb6bc --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomHandlerFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.server.impl; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.rometools.propono.atom.server.AtomHandler; +import com.rometools.propono.atom.server.AtomHandlerFactory; + +/** + * Extends {@link com.rometools.rome.propono.atom.server.AtomHandlerFactory} to create and return + * {@link com.rometools.rome.propono.atom.server.impl.FileBasedAtomHandler}. + */ +public class FileBasedAtomHandlerFactory extends AtomHandlerFactory { + + /** + * Create new AtomHandler. + */ + @Override + public AtomHandler newAtomHandler(final HttpServletRequest req, final HttpServletResponse res) { + return new FileBasedAtomHandler(req); + } + +} diff --git a/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomService.java b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomService.java new file mode 100644 index 0000000..3df365c --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedAtomService.java @@ -0,0 +1,193 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.server.impl; + +import java.io.InputStream; +import java.util.Map; +import java.util.Properties; +import java.util.TreeMap; + +import com.rometools.propono.atom.common.AtomService; +import com.rometools.propono.utils.Utilities; + +/** + * File based Atom service. Supports one workspace per user. Collections in workspace are defined in + * /propono.properties, for example: + * + *
+ *    # 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 final Map workspaceMap = new TreeMap(); + private final Map collectionMap = new TreeMap(); + private static Properties cacheProps = new Properties(); + private boolean firstTime = true; + + /** + * Creates a new instance of FileBasedAtomService. + */ + public FileBasedAtomService(final String userName, final String baseDir, final String contextURI, final String contextPath, final String servletPath) + throws Exception { + final String workspaceHandle = userName; + + // One workspace per user + final FileBasedWorkspace workspace = new FileBasedWorkspace(workspaceHandle, baseDir); + workspaceMap.put(userName, workspace); + + if (firstTime) { + synchronized (cacheProps) { + final InputStream is = getClass().getResourceAsStream("/propono.properties"); + if (is != null) { + cacheProps.load(is); + } + firstTime = false; + } + } + // can't find propono.properties, so use system props instead + if (cacheProps == null) { + cacheProps = System.getProperties(); + } + + final String relativeURIsString = cacheProps.getProperty("propono.atomserver.filebased.relativeURIs"); + final boolean relativeURIs = "true".equals(relativeURIsString); + + final String inlineCategoriesString = cacheProps.getProperty("propono.atomserver.filebased.inlineCategories"); + final boolean inlineCategories = "true".equals(inlineCategoriesString); + + final String colnames = cacheProps.getProperty("propono.atomserver.filebased.collections"); + if (colnames != null) { + + // collections specified in propono.properties, use those + + final String[] colarray = Utilities.stringToStringArray(colnames, ","); + for (final String element : colarray) { + final String prefix = "propono.atomserver.filebased.collection." + element + "."; + final String collectionTitle = cacheProps.getProperty(prefix + "title"); + final String collectionSingular = cacheProps.getProperty(prefix + "singular"); + final String collectionPlural = cacheProps.getProperty(prefix + "plural"); + final String collectionAccept = cacheProps.getProperty(prefix + "accept"); + + final String catNamesString = cacheProps.getProperty(prefix + "categories"); + final String[] catNames = Utilities.stringToStringArray(catNamesString, ","); + + final FileBasedCollection entries = new FileBasedCollection(collectionTitle, workspaceHandle, collectionPlural, collectionSingular, + collectionAccept, inlineCategories, catNames, relativeURIs, contextURI, contextPath, servletPath, baseDir); + workspace.addCollection(entries); + // want to be able to look up collection by singular and plural names + collectionMap.put(workspaceHandle + "|" + collectionSingular, entries); + collectionMap.put(workspaceHandle + "|" + collectionPlural, entries); + } + } else { + + // Fallback to two collections. One collection for accepting entries + // and other collection for other ( resources/uploaded images etc.) + + final String[] catNames = new String[] { "general", "category1", "category2" }; + + final FileBasedCollection entries = new FileBasedCollection("Entries", workspaceHandle, "entries", "entry", "application/atom+xml;type=entry", + inlineCategories, catNames, relativeURIs, contextURI, contextPath, servletPath, baseDir); + workspace.addCollection(entries); + // want to be able to look up collection by singular and plural names + collectionMap.put(workspaceHandle + "|entry", entries); + collectionMap.put(workspaceHandle + "|entries", entries); + + final FileBasedCollection resources = new FileBasedCollection("Resources", workspaceHandle, "resources", "resource", "*/*", inlineCategories, + catNames, relativeURIs, contextURI, contextPath, servletPath, baseDir); + // want to be able to look up collection by singular and plural names + workspace.addCollection(resources); + collectionMap.put(workspaceHandle + "|resource", resources); + collectionMap.put(workspaceHandle + "|resources", resources); + } + + getWorkspaces().add(workspace); + } + + /** + * Find workspace by handle, returns null of not found. + */ + FileBasedWorkspace findWorkspaceByHandle(final String handle) { + return workspaceMap.get(handle); + } + + FileBasedCollection findCollectionByHandle(final String handle, final String collection) { + return collectionMap.get(handle + "|" + collection); + } +} diff --git a/src/main/java/com/rometools/propono/atom/server/impl/FileBasedCollection.java b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedCollection.java new file mode 100644 index 0000000..ab296c7 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedCollection.java @@ -0,0 +1,815 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.server.impl; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.StringTokenizer; + +import javax.activation.FileTypeMap; +import javax.activation.MimetypesFileTypeMap; + +import org.jdom2.Document; +import org.jdom2.output.XMLOutputter; + +import com.rometools.propono.atom.common.Categories; +import com.rometools.propono.atom.common.Collection; +import com.rometools.propono.atom.common.rome.AppModule; +import com.rometools.propono.atom.common.rome.AppModuleImpl; +import com.rometools.propono.atom.server.AtomException; +import com.rometools.propono.atom.server.AtomMediaResource; +import com.rometools.propono.atom.server.AtomNotFoundException; +import com.rometools.propono.utils.Utilities; +import com.rometools.rome.feed.WireFeed; +import com.rometools.rome.feed.atom.Category; +import com.rometools.rome.feed.atom.Content; +import com.rometools.rome.feed.atom.Entry; +import com.rometools.rome.feed.atom.Feed; +import com.rometools.rome.feed.atom.Link; +import com.rometools.rome.feed.module.Module; +import com.rometools.rome.io.FeedException; +import com.rometools.rome.io.WireFeedInput; +import com.rometools.rome.io.WireFeedOutput; +import com.rometools.rome.io.impl.Atom10Generator; +import com.rometools.rome.io.impl.Atom10Parser; + +/** + * File based Atom collection implementation. This is the heart of the file-based Atom service + * implementation. It provides methods for adding, getting updating and deleting Atom entries and + * media entries. + */ +public class FileBasedCollection extends Collection { + + private String handle = null; + private String singular = null; + private String collection = null; + + private boolean inlineCats = false; + private String[] catNames = null; + + private boolean relativeURIs = false; + private String contextURI = null; + private String servletPath = null; + private String baseDir = null; + + private static final String FEED_TYPE = "atom_1.0"; + + /** + * Construct by providing title (plain text, no HTML), a workspace handle, a plural collection + * name (e.g. entries), a singular collection name (e.g. entry), the base directory for file + * storage, the content-type range accepted by the collection and the root Atom protocol URI for + * the service. + * + * @param title Title of collection (plain text, no HTML) + * @param handle Workspace handle + * @param collection Collection handle, plural + * @param singular Collection handle, singular + * @param accept Content type range accepted by collection + * @param inlineCats True for inline categories + * @param catNames Category names for this workspace + * @param baseDir Base directory for file storage + * @param relativeURIs True for relative URIs + * @param contextURI Absolute URI of context that hosts APP service + * @param contextPath Context path of APP service (e.g. "/sample-atomserver") + * @param servletPath Servlet path of APP service (e.g. "/app") + */ + public FileBasedCollection(final String title, final String handle, final String collection, final String singular, final String accept, + final boolean inlineCats, final String[] catNames, final boolean relativeURIs, final String contextURI, final String contextPath, + final String servletPath, final String baseDir) { + super(title, "text", relativeURIs ? servletPath.substring(1) + "/" + handle + "/" + collection : contextURI + servletPath + "/" + handle + "/" + + collection); + + this.handle = handle; + this.collection = collection; + this.singular = singular; + this.inlineCats = inlineCats; + this.catNames = catNames; + this.baseDir = baseDir; + this.relativeURIs = relativeURIs; + this.contextURI = contextURI; + this.servletPath = servletPath; + + addAccept(accept); + } + + /** + * Get feed document representing collection. + * + * @throws com.rometools.rome.propono.atom.server.AtomException On error retrieving feed file. + * @return Atom Feed representing collection. + */ + public Feed getFeedDocument() throws AtomException { + InputStream in = null; + synchronized (FileStore.getFileStore()) { + in = FileStore.getFileStore().getFileInputStream(getFeedPath()); + if (in == null) { + in = createDefaultFeedDocument(contextURI + servletPath + "/" + handle + "/" + collection); + } + } + try { + final WireFeedInput input = new WireFeedInput(); + final WireFeed wireFeed = input.build(new InputStreamReader(in, "UTF-8")); + return (Feed) wireFeed; + } catch (final Exception ex) { + throw new AtomException(ex); + } + } + + /** + * Get list of one Categories object containing categories allowed by collection. + * + * @param inline True if Categories object should contain collection of in-line Categories + * objects or false if it should set the Href for out-of-line categories. + */ + public List getCategories(final boolean inline) { + final Categories cats = new Categories(); + cats.setFixed(true); + cats.setScheme(contextURI + "/" + handle + "/" + singular); + if (inline) { + for (final String catName : catNames) { + final Category cat = new Category(); + cat.setTerm(catName); + cats.addCategory(cat); + } + } else { + cats.setHref(getCategoriesURI()); + } + return Collections.singletonList(cats); + } + + /** + * Get list of one Categories object containing categories allowed by collection, returns + * in-line categories if collection set to use in-line categories. + */ + @Override + public List getCategories() { + return getCategories(inlineCats); + } + + /** + * Add entry to collection. + * + * @param entry Entry to be added to collection. Entry will be saved to disk in a directory + * under the collection's directory and the path will follow the pattern + * [collection-plural]/[entryid]/entry.xml. The entry will be added to the + * collection's feed in [collection-plural]/feed.xml. + * @throws java.lang.Exception On error. + * @return Entry as it exists on the server. + */ + public Entry addEntry(final Entry entry) throws Exception { + synchronized (FileStore.getFileStore()) { + final Feed f = getFeedDocument(); + + final String fsid = FileStore.getFileStore().getNextId(); + updateTimestamps(entry); + + // Save entry to file + final String entryPath = getEntryPath(fsid); + + final OutputStream os = FileStore.getFileStore().getFileOutputStream(entryPath); + updateEntryAppLinks(entry, fsid, true); + Atom10Generator.serializeEntry(entry, new OutputStreamWriter(os, "UTF-8")); + os.flush(); + os.close(); + + // Update feed file + updateEntryAppLinks(entry, fsid, false); + updateFeedDocumentWithNewEntry(f, entry); + + return entry; + } + } + + /** + * Add media entry to collection. Accepts a media file to be added to collection. The file will + * be saved to disk in a directory under the collection's directory and the path will follow the + * pattern [collection-plural]/[entryid]/media/[entryid]. An Atom entry will be + * created to store metadata for the entry and it will exist at the path + * [collection-plural]/[entryid]/entry.xml. The entry will be added to the + * collection's feed in [collection-plural]/feed.xml. + * + * @param entry Entry object + * @param slug String to be used in file-name + * @param is Source of media data + * @throws java.lang.Exception On Error + * @return Location URI of entry + */ + public String addMediaEntry(final Entry entry, final String slug, final InputStream is) throws Exception { + synchronized (FileStore.getFileStore()) { + + // Save media file temp file + final Content content = entry.getContents().get(0); + if (entry.getTitle() == null) { + entry.setTitle(slug); + } + final String fileName = createFileName(slug != null ? slug : entry.getTitle(), content.getType()); + final File tempFile = File.createTempFile(fileName, "tmp"); + final FileOutputStream fos = new FileOutputStream(tempFile); + Utilities.copyInputToOutput(is, fos); + fos.close(); + + // Save media file + final FileInputStream fis = new FileInputStream(tempFile); + saveMediaFile(fileName, content.getType(), tempFile.length(), fis); + fis.close(); + final File resourceFile = new File(getEntryMediaPath(fileName)); + + // Create media-link entry + updateTimestamps(entry); + + // Save media-link entry + final String entryPath = getEntryPath(fileName); + final OutputStream os = FileStore.getFileStore().getFileOutputStream(entryPath); + updateMediaEntryAppLinks(entry, resourceFile.getName(), true); + Atom10Generator.serializeEntry(entry, new OutputStreamWriter(os, "UTF-8")); + os.flush(); + os.close(); + + // Update feed with new entry + final Feed f = getFeedDocument(); + updateMediaEntryAppLinks(entry, resourceFile.getName(), false); + updateFeedDocumentWithNewEntry(f, entry); + + return getEntryEditURI(fileName, false, true); + } + } + + /** + * Get an entry from the collection. + * + * @param fsid Internal ID of entry to be returned + * @throws java.lang.Exception On error + * @return Entry specified by fileName/ID + */ + public Entry getEntry(String fsid) throws Exception { + if (fsid.endsWith(".media-link")) { + fsid = fsid.substring(0, fsid.length() - ".media-link".length()); + } + + final String entryPath = getEntryPath(fsid); + + checkExistence(entryPath); + final InputStream in = FileStore.getFileStore().getFileInputStream(entryPath); + + final Entry entry; + final File resource = new File(fsid); + if (resource.exists()) { + entry = loadAtomResourceEntry(in, resource); + updateMediaEntryAppLinks(entry, fsid, true); + } else { + entry = loadAtomEntry(in); + updateEntryAppLinks(entry, fsid, true); + } + return entry; + } + + /** + * Get media resource wrapping a file. + */ + public AtomMediaResource getMediaResource(final String fileName) throws Exception { + final String filePath = getEntryMediaPath(fileName); + final File resource = new File(filePath); + return new AtomMediaResource(resource); + } + + /** + * Update an entry in the collection. + * + * @param entry Updated entry to be stored + * @param fsid Internal ID of entry + * @throws java.lang.Exception On error + */ + public void updateEntry(final Entry entry, String fsid) throws Exception { + synchronized (FileStore.getFileStore()) { + + final Feed f = getFeedDocument(); + + if (fsid.endsWith(".media-link")) { + fsid = fsid.substring(0, fsid.length() - ".media-link".length()); + } + + updateTimestamps(entry); + + updateEntryAppLinks(entry, fsid, false); + updateFeedDocumentWithExistingEntry(f, entry); + + final String entryPath = getEntryPath(fsid); + final OutputStream os = FileStore.getFileStore().getFileOutputStream(entryPath); + updateEntryAppLinks(entry, fsid, true); + Atom10Generator.serializeEntry(entry, new OutputStreamWriter(os, "UTF-8")); + os.flush(); + os.close(); + } + } + + /** + * Update media associated with a media-link entry. + * + * @param fileName Internal ID of entry being updated + * @param contentType Content type of data + * @param is Source of updated data + * @throws java.lang.Exception On error + * @return Updated Entry as it exists on server + */ + public Entry updateMediaEntry(final String fileName, final String contentType, final InputStream is) throws Exception { + synchronized (FileStore.getFileStore()) { + + final File tempFile = File.createTempFile(fileName, "tmp"); + final FileOutputStream fos = new FileOutputStream(tempFile); + Utilities.copyInputToOutput(is, fos); + fos.close(); + + // Update media file + final FileInputStream fis = new FileInputStream(tempFile); + saveMediaFile(fileName, contentType, tempFile.length(), fis); + fis.close(); + final File resourceFile = new File(getEntryMediaPath(fileName)); + + // Load media-link entry to return + final String entryPath = getEntryPath(fileName); + final InputStream in = FileStore.getFileStore().getFileInputStream(entryPath); + final Entry atomEntry = loadAtomResourceEntry(in, resourceFile); + + updateTimestamps(atomEntry); + updateMediaEntryAppLinks(atomEntry, fileName, false); + + // Update feed with new entry + final Feed f = getFeedDocument(); + updateFeedDocumentWithExistingEntry(f, atomEntry); + + // Save updated media-link entry + final OutputStream os = FileStore.getFileStore().getFileOutputStream(entryPath); + updateMediaEntryAppLinks(atomEntry, fileName, true); + Atom10Generator.serializeEntry(atomEntry, new OutputStreamWriter(os, "UTF-8")); + os.flush(); + os.close(); + + return atomEntry; + } + } + + /** + * Delete an entry and any associated media file. + * + * @param fsid Internal ID of entry + * @throws java.lang.Exception On error + */ + public void deleteEntry(final String fsid) throws Exception { + synchronized (FileStore.getFileStore()) { + + // Remove entry from Feed + final Feed feed = getFeedDocument(); + updateFeedDocumentRemovingEntry(feed, fsid); + + final String entryFilePath = getEntryPath(fsid); + FileStore.getFileStore().deleteFile(entryFilePath); + + final String entryMediaPath = getEntryMediaPath(fsid); + if (entryMediaPath != null) { + FileStore.getFileStore().deleteFile(entryMediaPath); + } + + final String entryDirPath = getEntryDirPath(fsid); + FileStore.getFileStore().deleteDirectory(entryDirPath); + + try { + Thread.sleep(500L); + } catch (final Exception ignored) { + } + } + } + + private void updateFeedDocumentWithNewEntry(final Feed f, final Entry e) throws AtomException { + boolean inserted = false; + for (int i = 0; i < f.getEntries().size(); i++) { + final Entry entry = f.getEntries().get(i); + final AppModule mod = (AppModule) entry.getModule(AppModule.URI); + final AppModule newMod = (AppModule) e.getModule(AppModule.URI); + if (newMod.getEdited().before(mod.getEdited())) { + f.getEntries().add(i, e); + inserted = true; + break; + } + } + if (!inserted) { + f.getEntries().add(0, e); + } + updateFeedDocument(f); + } + + private void updateFeedDocumentRemovingEntry(final Feed f, final String id) throws AtomException { + final Entry e = findEntry("urn:uuid:" + id, f); + f.getEntries().remove(e); + updateFeedDocument(f); + } + + private void updateFeedDocumentWithExistingEntry(final Feed f, final Entry e) throws AtomException { + final Entry old = findEntry(e.getId(), f); + f.getEntries().remove(old); + + boolean inserted = false; + for (int i = 0; i < f.getEntries().size(); i++) { + final Entry entry = f.getEntries().get(i); + final AppModule entryAppModule = (AppModule) entry.getModule(AppModule.URI); + final AppModule eAppModule = (AppModule) entry.getModule(AppModule.URI); + if (eAppModule.getEdited().before(entryAppModule.getEdited())) { + f.getEntries().add(i, e); + inserted = true; + break; + } + } + if (!inserted) { + f.getEntries().add(0, e); + } + updateFeedDocument(f); + } + + private Entry findEntry(final String id, final Feed feed) { + for (final Entry entry : feed.getEntries()) { + if (id.equals(entry.getId())) { + return entry; + } + } + return null; + } + + private void updateFeedDocument(final Feed f) throws AtomException { + try { + synchronized (FileStore.getFileStore()) { + final WireFeedOutput wireFeedOutput = new WireFeedOutput(); + final Document feedDoc = wireFeedOutput.outputJDom(f); + final XMLOutputter outputter = new XMLOutputter(); + // outputter.setFormat(Format.getPrettyFormat()); + final OutputStream fos = FileStore.getFileStore().getFileOutputStream(getFeedPath()); + outputter.output(feedDoc, new OutputStreamWriter(fos, "UTF-8")); + } + } catch (final FeedException fex) { + throw new AtomException(fex); + } catch (final IOException ex) { + throw new AtomException(ex); + } + } + + private InputStream createDefaultFeedDocument(final String uri) throws AtomException { + + final Feed f = new Feed(); + f.setTitle("Feed"); + f.setId(uri); + f.setFeedType(FEED_TYPE); + + final Link selfLink = new Link(); + selfLink.setRel("self"); + selfLink.setHref(uri); + f.getOtherLinks().add(selfLink); + + try { + final WireFeedOutput wireFeedOutput = new WireFeedOutput(); + final Document feedDoc = wireFeedOutput.outputJDom(f); + final XMLOutputter outputter = new XMLOutputter(); + // outputter.setFormat(Format.getCompactFormat()); + final OutputStream fos = FileStore.getFileStore().getFileOutputStream(getFeedPath()); + outputter.output(feedDoc, new OutputStreamWriter(fos, "UTF-8")); + + } catch (final FeedException ex) { + throw new AtomException(ex); + } catch (final IOException ex) { + throw new AtomException(ex); + } catch (final Exception e) { + + e.printStackTrace(); + } + return FileStore.getFileStore().getFileInputStream(getFeedPath()); + } + + private Entry loadAtomResourceEntry(final InputStream in, final File file) { + try { + final Entry entry = Atom10Parser.parseEntry(new BufferedReader(new InputStreamReader(in)), null, Locale.US); + updateMediaEntryAppLinks(entry, file.getName(), true); + return entry; + + } catch (final Exception e) { + e.printStackTrace(); + return null; + } + } + + private void updateEntryAppLinks(final Entry entry, final String fsid, final boolean singleEntry) { + + entry.setId("urn:uuid:" + fsid); + + // Look for existing alt links and the alt link + Link altLink = null; + List altLinks = entry.getAlternateLinks(); + if (altLinks != null) { + for (final Link link : altLinks) { + if (link.getRel() == null || "alternate".equals(link.getRel())) { + altLink = link; + break; + } + } + } else { + // No alt links found, so add them now + altLinks = new ArrayList(); + entry.setAlternateLinks(altLinks); + } + // The alt link not found, so add it now + if (altLink == null) { + altLink = new Link(); + altLinks.add(altLink); + } + // Set correct value for the alt link + altLink.setRel("alternate"); + altLink.setHref(getEntryViewURI(fsid)); + + // Look for existing other links and the edit link + Link editLink = null; + List otherLinks = entry.getOtherLinks(); + if (otherLinks != null) { + for (final Link link : otherLinks) { + if ("edit".equals(link.getRel())) { + editLink = link; + break; + } + } + } else { + // No other links found, so add them now + otherLinks = new ArrayList(); + entry.setOtherLinks(otherLinks); + } + // The edit link not found, so add it now + if (editLink == null) { + editLink = new Link(); + otherLinks.add(editLink); + } + // Set correct value for the edit link + editLink.setRel("edit"); + editLink.setHref(getEntryEditURI(fsid, relativeURIs, singleEntry)); + } + + private void updateMediaEntryAppLinks(final Entry entry, final String fileName, final boolean singleEntry) { + + // TODO: figure out why PNG is missing from Java MIME types + final FileTypeMap map = FileTypeMap.getDefaultFileTypeMap(); + if (map instanceof MimetypesFileTypeMap) { + try { + ((MimetypesFileTypeMap) map).addMimeTypes("image/png png PNG"); + } catch (final Exception ignored) { + } + } + entry.setId(getEntryMediaViewURI(fileName)); + entry.setTitle(fileName); + entry.setUpdated(new Date()); + + final List otherlinks = new ArrayList(); + entry.setOtherLinks(otherlinks); + + final Link editlink = new Link(); + editlink.setRel("edit"); + editlink.setHref(getEntryEditURI(fileName, relativeURIs, singleEntry)); + otherlinks.add(editlink); + + final Link editMedialink = new Link(); + editMedialink.setRel("edit-media"); + editMedialink.setHref(getEntryMediaEditURI(fileName, relativeURIs, singleEntry)); + otherlinks.add(editMedialink); + + final Content content = entry.getContents().get(0); + content.setSrc(getEntryMediaViewURI(fileName)); + final List contents = new ArrayList(); + contents.add(content); + entry.setContents(contents); + } + + /** + * Create a Rome Atom entry based on a Roller entry. Content is escaped. Link is stored as + * rel=alternate link. + */ + private Entry loadAtomEntry(final InputStream in) { + try { + return Atom10Parser.parseEntry(new BufferedReader(new InputStreamReader(in, "UTF-8")), null, Locale.US); + } catch (final Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * Update existing or add new app:edited. + */ + private void updateTimestamps(final Entry entry) { + // We're not differenting between an update and an edit (yet) + entry.setUpdated(new Date()); + + AppModule appModule = (AppModule) entry.getModule(AppModule.URI); + if (appModule == null) { + appModule = new AppModuleImpl(); + final List modules = entry.getModules() == null ? new ArrayList() : entry.getModules(); + modules.add(appModule); + entry.setModules(modules); + } + appModule.setEdited(entry.getUpdated()); + } + + /** + * Save file to website's resource directory. + * + * @param handle Weblog handle to save to + * @param name Name of file to save + * @param size Size of file to be saved + * @param is Read file from input stream + */ + private void saveMediaFile(final String name, final String contentType, final long size, final InputStream is) throws AtomException { + + final byte[] buffer = new byte[8192]; + int bytesRead = 0; + + final File dirPath = new File(getEntryMediaPath(name)); + if (!dirPath.getParentFile().exists()) { + dirPath.getParentFile().mkdirs(); + } + OutputStream bos = null; + try { + bos = new FileOutputStream(dirPath.getAbsolutePath()); + while ((bytesRead = is.read(buffer, 0, 8192)) != -1) { + bos.write(buffer, 0, bytesRead); + } + } catch (final Exception e) { + throw new AtomException("ERROR uploading file", e); + } finally { + try { + bos.flush(); + bos.close(); + } catch (final Exception ignored) { + } + } + + } + + /** + * Creates a file name for a file based on a weblog handle, title string and a content-type. + * + * @param handle Weblog handle + * @param title Title to be used as basis for file name (or null) + * @param contentType Content type of file (must not be null) + * + * If a title is specified, the method will apply the same create-anchor logic we use + * for weblog entries to create a file name based on the title. + * + * If title is null, the base file name will be the weblog handle plus a YYYYMMDDHHSS + * timestamp. + * + * The extension will be formed by using the part of content type that comes after he + * slash. + * + * For example: weblog.handle = "daveblog" title = "Port Antonio" content-type = + * "image/jpg" Would result in port_antonio.jpg + * + * Another example: weblog.handle = "daveblog" title = null content-type = + * "image/jpg" Might result in daveblog-200608201034.jpg + */ + private String createFileName(final String title, final String contentType) { + + if (handle == null) { + throw new IllegalArgumentException("weblog handle cannot be null"); + } + if (contentType == null) { + throw new IllegalArgumentException("contentType cannot be null"); + } + + String fileName = null; + + final SimpleDateFormat sdf = new SimpleDateFormat(); + sdf.applyPattern("yyyyMMddHHssSSS"); + + // Determine the extension based on the contentType. This is a hack. + // The info we need to map from contentType to file extension is in + // JRE/lib/content-type.properties, but Java Activation doesn't provide + // a way to do a reverse mapping or to get at the data. + final String[] typeTokens = contentType.split("/"); + final String ext = typeTokens[1]; + + if (title != null && !title.trim().equals("")) { + // We've got a title, so use it to build file name + final String base = Utilities.replaceNonAlphanumeric(title, ' '); + final StringTokenizer toker = new StringTokenizer(base); + String tmp = null; + int count = 0; + while (toker.hasMoreTokens() && count < 5) { + String s = toker.nextToken(); + s = s.toLowerCase(); + tmp = tmp == null ? s : tmp + "_" + s; + count++; + } + fileName = tmp + "-" + sdf.format(new Date()) + "." + ext; + + } else { + // No title or text, so instead we'll use the item's date + // in YYYYMMDD format to form the file name + fileName = handle + "-" + sdf.format(new Date()) + "." + ext; + } + + return fileName; + } + + // ------------------------------------------------------------ URI methods + + private String getEntryEditURI(final String fsid, final boolean relative, final boolean singleEntry) { + String entryURI = null; + if (relative) { + if (singleEntry) { + entryURI = fsid; + } else { + entryURI = singular + "/" + fsid; + } + } else { + entryURI = contextURI + servletPath + "/" + handle + "/" + singular + "/" + fsid; + } + return entryURI; + } + + private String getEntryViewURI(final String fsid) { + return contextURI + "/" + handle + "/" + collection + "/" + fsid + "/entry.xml"; + } + + private String getEntryMediaEditURI(final String fsid, final boolean relative, final boolean singleEntry) { + String entryURI = null; + if (relative) { + if (singleEntry) { + entryURI = "media/" + fsid; + } else { + entryURI = singular + "/media/" + fsid; + } + } else { + entryURI = contextURI + servletPath + "/" + handle + "/" + singular + "/media/" + fsid; + } + return entryURI; + + } + + private String getEntryMediaViewURI(final String fsid) { + return contextURI + "/" + handle + "/" + collection + "/" + fsid + "/media/" + fsid; + } + + private String getCategoriesURI() { + if (!relativeURIs) { + return contextURI + servletPath + "/" + handle + "/" + singular + "/categories"; + } else { + return servletPath + "/" + handle + "/" + singular + "/categories"; + } + } + + // ------------------------------------------------------- File path methods + + private String getBaseDir() { + return baseDir; + } + + private String getFeedPath() { + return getBaseDir() + handle + File.separator + collection + File.separator + "feed.xml"; + } + + private String getEntryDirPath(final String id) { + return getBaseDir() + handle + File.separator + collection + File.separator + id; + } + + private String getEntryPath(final String id) { + return getEntryDirPath(id) + File.separator + "entry.xml"; + } + + private String getEntryMediaPath(final String id) { + return getEntryDirPath(id) + File.separator + "media" + File.separator + id; + } + + private static void checkExistence(final String path) throws AtomNotFoundException { + if (!FileStore.getFileStore().exists(path)) { + throw new AtomNotFoundException("Entry does not exist"); + } + } + +} diff --git a/src/main/java/com/rometools/propono/atom/server/impl/FileBasedWorkspace.java b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedWorkspace.java new file mode 100644 index 0000000..163c6cc --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/impl/FileBasedWorkspace.java @@ -0,0 +1,30 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.server.impl; + +import com.rometools.propono.atom.common.Workspace; + +/** + * File based Atom service Workspace. + */ +public class FileBasedWorkspace extends Workspace { + + /** Creates a new instance of FileBasedWorkspace */ + public FileBasedWorkspace(final String handle, final String baseDir) { + super(handle, "text"); + } + +} diff --git a/src/main/java/com/rometools/propono/atom/server/impl/FileStore.java b/src/main/java/com/rometools/propono/atom/server/impl/FileStore.java new file mode 100644 index 0000000..035c341 --- /dev/null +++ b/src/main/java/com/rometools/propono/atom/server/impl/FileStore.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.server.impl; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.lang3.RandomStringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class which helps in handling File persistence related operations. + */ +class FileStore { + + private static final Logger LOG = LoggerFactory.getLogger(FileStore.class); + + private static final FileStore fileStore = new FileStore(); + + public String getNextId() { + // return UUID.randomUUID().toString(); // requires JDK 1.5 + return RandomStringUtils.randomAlphanumeric(20); + } + + public boolean exists(final String path) { + final File f = new File(path); + return f.exists(); + } + + public InputStream getFileInputStream(final String path) { + LOG.debug("getFileContents path: " + path); + try { + return new BufferedInputStream(new FileInputStream(path)); + } catch (final FileNotFoundException e) { + LOG.debug(" File not found: " + path); + return null; + } + } + + public OutputStream getFileOutputStream(final String path) { + LOG.debug("getFileOutputStream path: " + path); + try { + final File f = new File(path); + f.getParentFile().mkdirs(); + return new BufferedOutputStream(new FileOutputStream(f)); + } catch (final FileNotFoundException e) { + LOG.debug(" File not found: " + path); + return null; + } + } + + public void createOrUpdateFile(final String path, final InputStream content) throws FileNotFoundException, IOException { + LOG.debug("createOrUpdateFile path: " + path); + final File f = new File(path); + f.mkdirs(); + final FileOutputStream out = new FileOutputStream(f); + + final byte[] buffer = new byte[2048]; + int read; + while ((read = content.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + out.close(); + } + + public void deleteFile(final String path) { + LOG.debug("deleteFile path: " + path); + final File f = new File(path); + if (!f.delete()) { + LOG.debug(" Failed to delete: " + f.getAbsolutePath()); + } + } + + public static FileStore getFileStore() { + return fileStore; + } + + public boolean deleteDirectory(final String path) { + return deleteDirectory(new File(path)); + } + + public boolean deleteDirectory(final File path) { + LOG.debug("deleteDirectory path: " + path); + if (path.exists()) { + final File[] files = path.listFiles(); + for (final File file : files) { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + file.delete(); + } + } + } + return path.delete(); + } +} diff --git a/src/main/java/com/rometools/propono/blogclient/BaseBlogEntry.java b/src/main/java/com/rometools/propono/blogclient/BaseBlogEntry.java new file mode 100644 index 0000000..7d5aca9 --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/BaseBlogEntry.java @@ -0,0 +1,212 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Base implementation of a blog entry. + */ +public abstract class BaseBlogEntry implements BlogEntry { + protected String id = null; + protected Person author = null; + protected Content content = null; + protected String title = null; + protected String permalink = null; + protected String summary = null; + protected Date modificationDate = null; + protected Date publicationDate = null; + protected List categories = new ArrayList(); + protected boolean draft = false; + private Blog blog = null; + + /** + * Contruct abstract blog entry. + */ + public BaseBlogEntry(final Blog blog) { + this.blog = blog; + } + + /** + * {@inheritDoc} + */ + @Override + public String getId() { + return id; + } + + /** + * {@inheritDoc} + */ + @Override + public String getPermalink() { + return permalink; + } + + void setPermalink(final String permalink) { + this.permalink = permalink; + } + + /** + * {@inheritDoc} + */ + @Override + public Person getAuthor() { + return author; + } + + /** + * {@inheritDoc} + */ + @Override + public void setAuthor(final Person author) { + this.author = author; + } + + /** + * {@inheritDoc} + */ + @Override + public Content getContent() { + return content; + } + + /** + * {@inheritDoc} + */ + @Override + public void setContent(final Content content) { + this.content = content; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean getDraft() { + return draft; + } + + /** + * {@inheritDoc} + */ + @Override + public void setDraft(final boolean draft) { + this.draft = draft; + } + + /** + * {@inheritDoc} + */ + @Override + public Date getPublicationDate() { + return publicationDate; + } + + /** + * {@inheritDoc} + */ + @Override + public void setPublicationDate(final Date pubDate) { + publicationDate = pubDate; + } + + /** + * {@inheritDoc} + */ + @Override + public Date getModificationDate() { + return modificationDate; + } + + /** + * {@inheritDoc} + */ + @Override + public void setModificationDate(final Date date) { + modificationDate = date; + } + + /** + * {@inheritDoc} + */ + @Override + public String getTitle() { + return title; + } + + /** + * {@inheritDoc} + */ + @Override + public void setTitle(final String title) { + this.title = title; + } + + /** + * {@inheritDoc} + */ + @Override + public String getSummary() { + return summary; + } + + /** + * {@inheritDoc} + */ + @Override + public void setSummary(final String summary) { + this.summary = summary; + } + + /** + * {@inheritDoc} + */ + @Override + public List getCategories() { + return categories; + } + + /** + * {@inheritDoc} + */ + @Override + public void setCategories(final List categories) { + this.categories = categories; + } + + /** + * {@inheritDoc} + */ + @Override + public Blog getBlog() { + return blog; + } + + void setBlog(final Blog blog) { + this.blog = blog; + } + + /** + * String representation, returns id. + */ + @Override + public String toString() { + return id; + } +} diff --git a/src/main/java/com/rometools/propono/blogclient/Blog.java b/src/main/java/com/rometools/propono/blogclient/Blog.java new file mode 100644 index 0000000..eb5d505 --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/Blog.java @@ -0,0 +1,235 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient; + +import java.util.Iterator; +import java.util.List; + +/** + *

+ * Represents a blog, which has collections of entries and resources. You can access the collections + * using the getCollections() and getCollection(String name) methods, which return Blog.Collection + * objects, which you can use to create, retrieve update or delete entries within a collection. + *

+ */ +public interface Blog { + + /** + * Token can be used to fetch this blog again from getBlog() method. + * + * @return Blog object specified by token. + */ + public String getToken(); + + /** + * Name of this blog. + * + * @return Display name of this blog. + */ + public String getName(); + + /** + * Get a single BlogEntry (or BlogResource) by token. + * + * @param token Token from blog entry's getToken() method. + * @throws com.rometools.rome.propono.blogclient.BlogClientException On error fetching the blog + * entry. + * @return Blog entry specified by token. + */ + public BlogEntry getEntry(String token) throws BlogClientException; + + /** + * Gets listing of entry and resource collections available in the blog, including the primary + * collections. + * + * @throws BlogClientException On error fetching collections. + * @return List of Blog.Collection objects. + */ + public List getCollections() throws BlogClientException; + + /** + * Get collection by token. + * + * @param token Token from a collection's getToken() method. + * @throws BlogClientException On error fetching collection. + * @return Blog.Collection object. + */ + public Collection getCollection(String token) throws BlogClientException; + + /** + * Represents an entry or resource collection on a blog server. + */ + public interface Collection { + + /** + * Get blog that contains this collection. + * + * @return Blog that contains this collection. + */ + public Blog getBlog(); + + /** + * Title of collection. + * + * @return Title of collecton. + */ + public String getTitle(); + + /** + * Token that can be used to fetch collection. + * + * @return Token that can be used to fetch collection. + */ + public String getToken(); + + /** + * Content-types accepted by collection. + * + * @return Comma-separated list of content-types accepted. + */ + public List getAccepts(); + + /** + * Determines if collection will accept a content-type. + * + * @param contentType Content-type to be considered. + * @return True of content type will be accepted, false otherwise. + */ + public boolean accepts(String contentType); + + /** + * Return categories allowed by colletion. + * + * @throws BlogClientException On error fetching categories. + * @return List of BlogEntry.Category objects for this collection. + */ + public List getCategories() throws BlogClientException; + + /** + * Create but do not save new entry in collection. To save entry, call its save() method. + * + * @throws BlogClientException On error creating entry. + * @return New BlogEntry object. + */ + public BlogEntry newEntry() throws BlogClientException; + + /** + * Create but do not save new resource in collection. To save resource, call its save() + * method. + * + * @param name Name of new resource. + * @param contentType MIME content-type of new resource. + * @param bytes Data for new resource. + * @throws BlogClientException On error creating entry. + * @return New BlogResource object, + */ + public BlogResource newResource(String name, String contentType, byte[] bytes) throws BlogClientException; + + /** + * Get iterator over entries/resources in this collection. + * + * @return List of BlogEntry objects, some may be BlogResources. + * @throws BlogClientException On error fetching entries/resources. + */ + public Iterator getEntries() throws BlogClientException; + + /** + * Save or update a BlogEntry in this collection by adding it to this collection and then + * calling it's entry.save() method. + * + * @param entry BlogEntry to be saved. + * @throws BlogClientException On error saving entry. + * @return URI of entry. + */ + public String saveEntry(BlogEntry entry) throws BlogClientException; + + /** + * Save or update resource in this collection + * + * @param resource BlogResource to be saved. + * @throws BlogClientException On error saving resource. + * @return URI of resource. + */ + public String saveResource(BlogResource resource) throws BlogClientException; + } + + // Deprecated primary collection methods, instead use collections directly. + + /** + * Get iterator over entries in primary entries collection (the first collection that accepts + * entries). Note that entries may be partial, so don't try to update and save them: to update + * and entry, first fetch it with getEntry(), change fields, then call entry.save(); + * + * @return To iterate over all entries in collection. + * @throws BlogClientException On failure or if there is no primary entries collection. + * + * @deprecated Instead use collections directly. + */ + @Deprecated + public Iterator getEntries() throws BlogClientException; + + /** + * Get entries in primary resources collection (the first collection that accepts anything other + * than entries). + * + * @throws BlogClientException On failure or if there is no primary resources collection. + * @return To iterate over all resojrces in collection. + * + * @deprecated Instead use collections directly. + */ + @Deprecated + public Iterator getResources() throws BlogClientException; + + /** + * Create but do not save it to server new BlogEntry in primary entries collection (the first + * collection found that accepts entries). To save the entry to the server to a collection, use + * the entry's save() method. + * + * @throws BlogClientException On error or if there is no primary entries collection. + * @return Unsaved BlogEntry in primary entries collection. + * + * @deprecated Instead use collections directly. + */ + @Deprecated + public BlogEntry newEntry() throws BlogClientException; + + /** + * Create but do not save it to server new BlogResource in primary resources collection (the + * first collection found that accepts resources). To save the resource to the server to a + * collection, use the resource's save() method. + * + * @param name Name of resource to be saved. + * @param type MIME content type of resource data. + * @param bytes Bytes of resource data. + * @throws BlogClientException On error or if there is no primary respurces collection. + * @return Unsaved BlogEntry in primary resources collection. + * + * @deprecated Instead use collections directly. + */ + @Deprecated + public BlogResource newResource(String name, String type, byte[] bytes) throws BlogClientException; + + /** + * Returns list of available BlogEntry.Category in primary entries collection. + * + * @throws BlogClientException On error or if there is no primary entries collection. + * @return List of BlogEntry.Category objects. + * + * @deprecated Instead use collections directly. + */ + @Deprecated + public List getCategories() throws BlogClientException; +} diff --git a/src/main/java/com/rometools/propono/blogclient/BlogClientException.java b/src/main/java/com/rometools/propono/blogclient/BlogClientException.java new file mode 100644 index 0000000..ac44e48 --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/BlogClientException.java @@ -0,0 +1,44 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient; + +/** + * Represents a Blog Client exception, the library throws these instead of implementation specific + * exceptions. + */ +public class BlogClientException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Construct a new exception + * + * @param msg Text message that explains exception + */ + public BlogClientException(final String msg) { + super(msg); + } + + /** + * Construct a new exception which wraps a throwable. + * + * @param msg Text message that explains exception + * @param t Throwable to be wrapped by exception + */ + public BlogClientException(final String msg, final Throwable t) { + super(msg, t); + } +} diff --git a/src/main/java/com/rometools/propono/blogclient/BlogConnection.java b/src/main/java/com/rometools/propono/blogclient/BlogConnection.java new file mode 100644 index 0000000..4eb2fcc --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/BlogConnection.java @@ -0,0 +1,34 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient; + +import java.util.List; + +/** + * A BlogConnection is a single-user connection to a blog server where the user has access to + * multiple blogs, which are each represented by a Blog interface. + */ +public interface BlogConnection { + + /** Returns collection of blogs available from this connection */ + public abstract List getBlogs(); + + /** Get blog by token */ + public abstract Blog getBlog(String token); + + /** Set appkey (optional, needed by some blog servers) */ + public void setAppkey(String appkey); +} \ No newline at end of file diff --git a/src/main/java/com/rometools/propono/blogclient/BlogConnectionFactory.java b/src/main/java/com/rometools/propono/blogclient/BlogConnectionFactory.java new file mode 100644 index 0000000..0494a37 --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/BlogConnectionFactory.java @@ -0,0 +1,74 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient; + +import java.lang.reflect.Constructor; + +/** + * Entry point to the Blogapps blog client library. + */ +public class BlogConnectionFactory { + + // BlogConnection implementations must: + // 1) implement BlogConnection + // 2) privide contructor that accepts three strings args: url, username and password. + + // TODO: make implementations configurable + private static String ATOMPROTOCOL_IMPL_CLASS = "com.rometools.propono.blogclient.atomprotocol.AtomConnection"; + + private static String METAWEBLOG_IMPL_CLASS = "com.rometools.propono.blogclient.metaweblog.MetaWeblogConnection"; + + /** + * Create a connection to a blog server. + * + * @param type Connection type, must be "atom" or "metaweblog" + * @param url End-point URL to connect to + * @param username Username for login to blog server + * @param password Password for login to blog server + */ + public static BlogConnection getBlogConnection(final String type, final String url, final String username, final String password) + throws BlogClientException { + BlogConnection blogConnection = null; + if (type == null || type.equals("metaweblog")) { + blogConnection = createBlogConnection(METAWEBLOG_IMPL_CLASS, url, username, password); + } else if (type.equals("atom")) { + blogConnection = createBlogConnection(ATOMPROTOCOL_IMPL_CLASS, url, username, password); + } else { + throw new BlogClientException("Type must be 'atom' or 'metaweblog'"); + } + return blogConnection; + } + + private static BlogConnection createBlogConnection(final String className, final String url, final String username, final String password) + throws BlogClientException { + + Class conClass; + try { + conClass = Class.forName(className); + } catch (final ClassNotFoundException ex) { + throw new BlogClientException("BlogConnection impl. class not found: " + className, ex); + } + final Class[] args = new Class[] { String.class, String.class, String.class }; + Constructor ctor; + try { + ctor = conClass.getConstructor(args); + return (BlogConnection) ctor.newInstance(new Object[] { url, username, password }); + } catch (final Throwable t) { + throw new BlogClientException("ERROR instantiating BlogConnection impl.", t); + } + } + +} diff --git a/src/main/java/com/rometools/propono/blogclient/BlogEntry.java b/src/main/java/com/rometools/propono/blogclient/BlogEntry.java new file mode 100644 index 0000000..854e5a6 --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/BlogEntry.java @@ -0,0 +1,261 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient; + +import java.util.Date; +import java.util.List; + +/** + * Represents a single blog entry. + */ +public interface BlogEntry { + + /** Get token, which can be used to fetch the blog entry */ + public String getToken(); + + /** + * Save this entry to it's collection. If this is a new entry and does not have a collection + * yet, then save() will save it to the primary collection. + */ + public void save() throws BlogClientException; + + /** Delete this entry from blog server */ + public void delete() throws BlogClientException; + + /** Permanent link to this entry (assigned by server) */ + public String getPermalink(); + + /** Blog is associated with a blog */ + public Blog getBlog(); + + /** Get categories, a list of BlogEntry.Category objects */ + public List getCategories(); + + /** Set categories, a list of BlogEntry.Category objects */ + public void setCategories(List categories); + + /** Get globally unique ID of this blog entry */ + public String getId(); + + /** Get title of this blog entry */ + public String getTitle(); + + /** Set title of this blog entry */ + public void setTitle(String title); + + /** Get summary of this blog entry */ + public String getSummary(); + + /** Set summary of this blog entry */ + public void setSummary(String summary); + + /** Get content of this blog entry */ + public Content getContent(); + + /** Set content of this blog entry */ + public void setContent(Content content); + + /** Get draft status of this entry */ + public boolean getDraft(); + + /** Set draft status of this entry */ + public void setDraft(boolean draft); + + /** Get author of this entry */ + public Person getAuthor(); + + /** Set author of this entry */ + public void setAuthor(Person author); + + /** Set publish date of this entry */ + public Date getPublicationDate(); + + /** Get publish date of this entry */ + public void setPublicationDate(Date date); + + /** Get update date of this entry */ + public Date getModificationDate(); + + /** Set update date of this entry */ + public void setModificationDate(Date date); + + /** Represents blog entry content */ + public class Content { + String type = "html"; + String value = null; + String src = null; + + /** Construct content */ + public Content() { + } + + /** Construct content with value (and type="html") */ + public Content(final String value) { + this.value = value; + } + + /** Get value of content if in-line */ + public String getValue() { + return value; + } + + /** Set value of content if in-line */ + public void setValue(final String value) { + this.value = value; + } + + /** + * Get type of content, either "text", "html", "xhtml" or a MIME content-type. Defaults to + * HTML. + */ + public String getType() { + return type; + } + + /** + * Set type of content, either "text", "html", "xhtml" or a MIME content-type. Defaults to + * HTML. + */ + public void setType(final String type) { + this.type = type; + } + + /** Get URI of content if out-of-line */ + public String getSrc() { + return src; + } + + /** Set URI of content if out-of-line */ + public void setSrc(final String src) { + this.src = src; + } + } + + /** Represents a blog author or contributor */ + public class Person { + String name; + String email; + String url; + + /** Get person's email */ + public String getEmail() { + return email; + } + + /** Set person's email */ + public void setEmail(final String email) { + this.email = email; + } + + /** Get person's name */ + public String getName() { + return name; + } + + /** Set person's name */ + public void setName(final String name) { + this.name = name; + } + + /** Get person's URL */ + public String getUrl() { + return url; + } + + /** Set person's URL */ + public void setUrl(final String url) { + this.url = url; + } + + /** Returns person's name */ + @Override + public String toString() { + return name; + } + } + + /** Represents a weblog category */ + public class Category { + String id; + String name; + String url; + + /** + * Create new Catetory + */ + public Category() { + } + + /** + * Create new category with name. + */ + public Category(final String id) { + this.id = id; + name = id; + } + + /** + * Determines if categories are equal based on id. + */ + @Override + public boolean equals(final Object obj) { + final Category other = (Category) obj; + if (obj == null) { + return false; + } + if (getId() != null && other.getId() != null && getId().equals(other.getId())) { + return true; + } + return false; + } + + /** Get category id */ + public String getId() { + return id; + } + + /** Set category id */ + public void setId(final String id) { + this.id = id; + } + + /** Get category display name */ + public String getName() { + return name; + } + + /** Set category display name */ + public void setName(final String name) { + this.name = name; + } + + /** Get URL of category domain */ + public String getUrl() { + return url; + } + + /** Set URL of category domain */ + public void setUrl(final String url) { + this.url = url; + } + + /** Return category's name or id for display */ + @Override + public String toString() { + return name != null ? name : id; + } + } +} diff --git a/src/main/java/com/rometools/propono/blogclient/BlogResource.java b/src/main/java/com/rometools/propono/blogclient/BlogResource.java new file mode 100644 index 0000000..e9ffa55 --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/BlogResource.java @@ -0,0 +1,37 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient; + +import java.io.InputStream; + +/** + * Represents a file that has been uploaded to a blog. + *

+ * Resources are modeled as a type of BlogEntry, but be aware: not all servers can save resource + * metadata (i.e. title, category, author, etc.). MetaWeblog based servers can't save metadata at + * all and Atom protocol servers are not required to preserve uploaded file metadata. + */ +public interface BlogResource extends BlogEntry { + + /** Get resource name (name is required) */ + public String getName(); + + /** Get resource as stream, using content.src as URL */ + public InputStream getAsStream() throws BlogClientException; + + /** Update resource by immediately uploading new bytes to server */ + public void update(byte[] newBytes) throws BlogClientException; +} diff --git a/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomBlog.java b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomBlog.java new file mode 100644 index 0000000..03c3949 --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomBlog.java @@ -0,0 +1,231 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient.atomprotocol; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import com.rometools.propono.atom.client.ClientAtomService; +import com.rometools.propono.atom.client.ClientCollection; +import com.rometools.propono.atom.client.ClientEntry; +import com.rometools.propono.atom.client.ClientMediaEntry; +import com.rometools.propono.atom.client.ClientWorkspace; +import com.rometools.propono.blogclient.Blog; +import com.rometools.propono.blogclient.BlogClientException; +import com.rometools.propono.blogclient.BlogEntry; +import com.rometools.propono.blogclient.BlogResource; +import com.rometools.propono.utils.ProponoException; + +/** + * Atom protocol implementation of the BlogClient Blog interface. + */ +public class AtomBlog implements Blog { + + private String name = null; + private ClientAtomService service; + private ClientWorkspace workspace = null; + private AtomCollection entriesCollection = null; + private AtomCollection resourcesCollection = null; + private final Map collections = new TreeMap(); + + /** + * Create AtomBlog using specified HTTPClient, user account and workspace, called by + * AtomConnection. Fetches Atom Service document and creates an AtomCollection object for each + * collection found. The first entry collection is considered the primary entry collection. And + * the first resource collection is considered the primary resource collection. + */ + AtomBlog(final ClientAtomService service, final ClientWorkspace workspace) { + + setService(service); + setWorkspace(workspace); + name = workspace.getTitle(); + final List collect = workspace.getCollections(); + final Iterator members = collect.iterator(); + + while (members.hasNext()) { + final ClientCollection col = (ClientCollection) members.next(); + if (col.accepts("entry") && entriesCollection == null) { + // first entry collection is primary entry collection + entriesCollection = new AtomCollection(this, col); + } else if (!col.accepts("entry") && resourcesCollection == null) { + // first non-entry collection is primary resource collection + resourcesCollection = new AtomCollection(this, col); + } + collections.put(col.getHrefResolved(), new AtomCollection(this, col)); + } + + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return name; + } + + /** + * String display of blog, returns name. + */ + @Override + public String toString() { + return getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getToken() { + return entriesCollection.getToken(); + } + + /** + * {@inheritDoc} + */ + @Override + public BlogEntry newEntry() throws BlogClientException { + if (entriesCollection == null) { + throw new BlogClientException("No entry collection"); + } + return entriesCollection.newEntry(); + } + + /** + * {@inheritDoc} + */ + @Override + public BlogEntry getEntry(final String token) throws BlogClientException { + ClientEntry clientEntry = null; + try { + clientEntry = getService().getEntry(token); + } catch (final ProponoException ex) { + throw new BlogClientException("ERROR: fetching entry", ex); + } + if (clientEntry != null && clientEntry instanceof ClientMediaEntry) { + return new AtomResource(this, (ClientMediaEntry) clientEntry); + } else if (clientEntry != null && clientEntry instanceof ClientEntry) { + return new AtomEntry(this, clientEntry); + } else { + throw new BlogClientException("ERROR: unknown object type returned"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator getEntries() throws BlogClientException { + if (entriesCollection == null) { + throw new BlogClientException("No primary entry collection"); + } + return new AtomEntryIterator(entriesCollection); + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator getResources() throws BlogClientException { + if (resourcesCollection == null) { + throw new BlogClientException("No primary entry collection"); + } + return new AtomEntryIterator(resourcesCollection); + } + + String saveEntry(final BlogEntry entry) throws BlogClientException { + if (entriesCollection == null) { + throw new BlogClientException("No primary entry collection"); + } + return entriesCollection.saveEntry(entry); + } + + void deleteEntry(final BlogEntry entry) throws BlogClientException { + if (entriesCollection == null) { + throw new BlogClientException("No primary entry collection"); + } + entriesCollection.deleteEntry(entry); + } + + /** + * {@inheritDoc} + */ + @Override + public List getCategories() throws BlogClientException { + if (entriesCollection == null) { + throw new BlogClientException("No primary entry collection"); + } + return entriesCollection.getCategories(); + } + + /** + * {@inheritDoc} + */ + @Override + public BlogResource newResource(final String name, final String contentType, final byte[] bytes) throws BlogClientException { + if (resourcesCollection == null) { + throw new BlogClientException("No resource collection"); + } + return resourcesCollection.newResource(name, contentType, bytes); + } + + String saveResource(final BlogResource res) throws BlogClientException { + if (resourcesCollection == null) { + throw new BlogClientException("No primary resource collection"); + } + return resourcesCollection.saveResource(res); + } + + void deleteResource(final BlogResource resource) throws BlogClientException { + deleteEntry(resource); + } + + /** + * {@inheritDoc} + */ + @Override + public List getCollections() throws BlogClientException { + return new ArrayList(collections.values()); + } + + /** + * {@inheritDoc} + */ + @Override + public Blog.Collection getCollection(final String token) throws BlogClientException { + return collections.get(token); + } + + ClientAtomService getService() { + return service; + } + + void setService(final ClientAtomService service) { + this.service = service; + } + + ClientWorkspace getWorkspace() { + return workspace; + } + + void setWorkspace(final ClientWorkspace workspace) { + this.workspace = workspace; + } + +} diff --git a/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomCollection.java b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomCollection.java new file mode 100644 index 0000000..d944b89 --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomCollection.java @@ -0,0 +1,173 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient.atomprotocol; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.rometools.propono.atom.client.ClientAtomService; +import com.rometools.propono.atom.client.ClientCollection; +import com.rometools.propono.atom.client.ClientEntry; +import com.rometools.propono.atom.common.Categories; +import com.rometools.propono.blogclient.Blog; +import com.rometools.propono.blogclient.BlogClientException; +import com.rometools.propono.blogclient.BlogEntry; +import com.rometools.propono.blogclient.BlogResource; +import com.rometools.rome.feed.atom.Category; + +/** + * Atom protocol implementation of BlogClient Blog.Collection. + */ +public class AtomCollection implements Blog.Collection { + + private Blog blog = null; + private List categories = new ArrayList(); + + private ClientCollection clientCollection = null; + + AtomCollection(final AtomBlog blog, final ClientCollection col) { + this.blog = blog; + clientCollection = col; + for (final Object element : col.getCategories()) { + final Categories cats = (Categories) element; + for (final Object element2 : cats.getCategories()) { + final Category cat = (Category) element2; + final BlogEntry.Category blogCat = new BlogEntry.Category(cat.getTerm()); + blogCat.setName(cat.getLabel()); + blogCat.setUrl(cat.getScheme()); + getCategories().add(blogCat); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public String getTitle() { + return getClientCollection().getTitle(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getToken() { + return getClientCollection().getHrefResolved(); + } + + /** + * {@inheritDoc} + */ + @Override + public List getAccepts() { + return getClientCollection().getAccepts(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean accepts(final String ct) { + return getClientCollection().accepts(ct); + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator getEntries() throws BlogClientException { + return new AtomEntryIterator(this); + } + + /** + * {@inheritDoc} + */ + @Override + public BlogEntry newEntry() throws BlogClientException { + final AtomBlog atomBlog = (AtomBlog) getBlog(); + final BlogEntry entry = new AtomEntry(atomBlog, this); + return entry; + } + + /** + * {@inheritDoc} + */ + @Override + public BlogResource newResource(final String name, final String contentType, final byte[] bytes) throws BlogClientException { + return new AtomResource(this, name, contentType, bytes); + } + + /** + * {@inheritDoc} + */ + @Override + public String saveResource(final BlogResource res) throws BlogClientException { + ((AtomResource) res).setCollection(this); + res.save(); + return res.getContent().getSrc(); + } + + /** + * {@inheritDoc} + */ + @Override + public String saveEntry(final BlogEntry entry) throws BlogClientException { + ((AtomEntry) entry).setCollection(this); + entry.save(); + return entry.getPermalink(); + } + + void deleteEntry(final BlogEntry entry) throws BlogClientException { + try { + final ClientAtomService service = ((AtomBlog) getBlog()).getService(); + final ClientEntry clientEntry = service.getEntry(entry.getToken()); + clientEntry.remove(); + + } catch (final Exception e) { + throw new BlogClientException("ERROR deleting entry", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Blog getBlog() { + return blog; + } + + void setBlog(final AtomBlog blog) { + this.blog = blog; + } + + /** + * {@inheritDoc} + */ + @Override + public List getCategories() { + return categories; + } + + void setCategories(final List categories) { + this.categories = categories; + } + + ClientCollection getClientCollection() { + return clientCollection; + } +} diff --git a/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomConnection.java b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomConnection.java new file mode 100644 index 0000000..db2cc78 --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomConnection.java @@ -0,0 +1,85 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient.atomprotocol; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import com.rometools.propono.atom.client.AtomClientFactory; +import com.rometools.propono.atom.client.BasicAuthStrategy; +import com.rometools.propono.atom.client.ClientAtomService; +import com.rometools.propono.atom.client.ClientWorkspace; +import com.rometools.propono.atom.common.Workspace; +import com.rometools.propono.blogclient.Blog; +import com.rometools.propono.blogclient.BlogClientException; +import com.rometools.propono.blogclient.BlogConnection; + +/** + * Atom protocol of BlogConnection. Connects to Atom server, creates AtomBlog object for each Atom + * workspace found and within each blog a collection for each Atom collection found. + */ +public class AtomConnection implements BlogConnection { + + private final Map blogs = new HashMap(); + + /** + * Create Atom blog client instance for specified URL and user account. + * + * @param uri End-point URL of Atom service + * @param username Username of account + * @param password Password of account + */ + public AtomConnection(final String uri, final String username, final String password) throws BlogClientException { + + try { + final ClientAtomService service = AtomClientFactory.getAtomService(uri, new BasicAuthStrategy(username, password)); + final Iterator iter = service.getWorkspaces().iterator(); + while (iter.hasNext()) { + final ClientWorkspace workspace = (ClientWorkspace) iter.next(); + final Blog blog = new AtomBlog(service, workspace); + blogs.put(blog.getToken(), blog); + } + } catch (final Throwable t) { + throw new BlogClientException("Error connecting to blog server", t); + } + } + + /** + * {@inheritDoc} + */ + @Override + public List getBlogs() { + return new ArrayList(blogs.values()); + } + + /** + * {@inheritDoc} + */ + @Override + public Blog getBlog(final String token) { + return blogs.get(token); + } + + /** + * {@inheritDoc} + */ + @Override + public void setAppkey(final String appkey) { + } +} diff --git a/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomEntry.java b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomEntry.java new file mode 100644 index 0000000..945473b --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomEntry.java @@ -0,0 +1,226 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient.atomprotocol; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.rometools.propono.atom.client.ClientEntry; +import com.rometools.propono.atom.common.rome.AppModule; +import com.rometools.propono.atom.common.rome.AppModuleImpl; +import com.rometools.propono.blogclient.BaseBlogEntry; +import com.rometools.propono.blogclient.BlogClientException; +import com.rometools.propono.blogclient.BlogEntry; +import com.rometools.propono.utils.ProponoException; +import com.rometools.rome.feed.atom.Entry; +import com.rometools.rome.feed.atom.Link; +import com.rometools.rome.feed.module.Module; +import com.rometools.rome.feed.synd.SyndPerson; + +/** + * Atom protocol implementation of BlogEntry. + */ +public class AtomEntry extends BaseBlogEntry implements BlogEntry { + + String editURI = null; + AtomCollection collection = null; + + AtomEntry(final AtomBlog blog, final AtomCollection collection) throws BlogClientException { + super(blog); + this.collection = collection; + } + + AtomEntry(final AtomCollection collection, final ClientEntry entry) throws BlogClientException { + this((AtomBlog) collection.getBlog(), collection); + // clientEntry = entry; + copyFromRomeEntry(entry); + } + + AtomEntry(final AtomBlog blog, final ClientEntry entry) throws BlogClientException { + super(blog); + // clientEntry = entry; + copyFromRomeEntry(entry); + } + + @Override + public String getToken() { + return editURI; + } + + AtomCollection getCollection() { + return collection; + } + + void setCollection(final AtomCollection collection) { + this.collection = collection; + } + + /** + * True if entry's token's are equal. + */ + @Override + public boolean equals(final Object o) { + if (o instanceof AtomEntry) { + final AtomEntry other = (AtomEntry) o; + if (other.getToken() != null && getToken() != null) { + return other.getToken().equals(getToken()); + } + } + return false; + } + + @Override + public void save() throws BlogClientException { + final boolean create = getToken() == null; + if (create && getCollection() == null) { + throw new BlogClientException("Cannot save entry, no collection"); + } else if (create) { + try { + final ClientEntry clientEntry = collection.getClientCollection().createEntry(); + copyToRomeEntry(clientEntry); + collection.getClientCollection().addEntry(clientEntry); + copyFromRomeEntry(clientEntry); + } catch (final ProponoException ex) { + throw new BlogClientException("Error saving entry", ex); + } + } else { + try { + final ClientEntry clientEntry = ((AtomBlog) getBlog()).getService().getEntry(getToken()); + copyToRomeEntry(clientEntry); + clientEntry.update(); + copyFromRomeEntry(clientEntry); + } catch (final ProponoException ex) { + throw new BlogClientException("Error updating entry", ex); + } + } + } + + @Override + public void delete() throws BlogClientException { + if (getToken() == null) { + throw new BlogClientException("Cannot delete unsaved entry"); + } + try { + final ClientEntry clientEntry = ((AtomBlog) getBlog()).getService().getEntry(editURI); + clientEntry.remove(); + } catch (final ProponoException ex) { + throw new BlogClientException("Error removing entry", ex); + } + } + + void copyFromRomeEntry(final ClientEntry entry) { + id = entry.getId(); + title = entry.getTitle(); + editURI = entry.getEditURI(); + final List altlinks = entry.getAlternateLinks(); + if (altlinks != null) { + for (final Link link : altlinks) { + if ("alternate".equals(link.getRel()) || link.getRel() == null) { + permalink = link.getHrefResolved(); + break; + } + } + } + final List contents = entry.getContents(); + com.rometools.rome.feed.atom.Content romeContent = null; + if (contents != null && !contents.isEmpty()) { + romeContent = contents.get(0); + } + if (romeContent != null) { + content = new BlogEntry.Content(romeContent.getValue()); + content.setType(romeContent.getType()); + content.setSrc(romeContent.getSrc()); + } + if (entry.getCategories() != null) { + final List cats = new ArrayList(); + final List romeCats = entry.getCategories(); + for (final com.rometools.rome.feed.atom.Category romeCat : romeCats) { + final BlogEntry.Category cat = new BlogEntry.Category(); + cat.setId(romeCat.getTerm()); + cat.setUrl(romeCat.getScheme()); + cat.setName(romeCat.getLabel()); + cats.add(cat); + } + categories = cats; + } + final List authors = entry.getAuthors(); + if (authors != null && !authors.isEmpty()) { + final com.rometools.rome.feed.atom.Person romeAuthor = (com.rometools.rome.feed.atom.Person) authors.get(0); + if (romeAuthor != null) { + author = new Person(); + author.setName(romeAuthor.getName()); + author.setEmail(romeAuthor.getEmail()); + author.setUrl(romeAuthor.getUrl()); + } + } + publicationDate = entry.getPublished(); + modificationDate = entry.getModified(); + + final AppModule control = (AppModule) entry.getModule(AppModule.URI); + if (control != null && control.getDraft() != null) { + draft = control.getDraft().booleanValue(); + } else { + draft = false; + } + } + + Entry copyToRomeEntry(final ClientEntry entry) { + if (id != null) { + entry.setId(id); + } + entry.setTitle(title); + if (author != null) { + final com.rometools.rome.feed.atom.Person person = new com.rometools.rome.feed.atom.Person(); + person.setName(author.getName()); + person.setEmail(author.getEmail()); + person.setUrl(author.getUrl()); + final List authors = new ArrayList(); + authors.add(person); + entry.setAuthors(authors); + } + if (content != null) { + final com.rometools.rome.feed.atom.Content romeContent = new com.rometools.rome.feed.atom.Content(); + romeContent.setValue(content.getValue()); + romeContent.setType(content.getType()); + final List contents = new ArrayList(); + contents.add(romeContent); + entry.setContents(contents); + } + if (categories != null) { + final List romeCats = new ArrayList(); + for (final Category cat : categories) { + final com.rometools.rome.feed.atom.Category romeCategory = new com.rometools.rome.feed.atom.Category(); + romeCategory.setTerm(cat.getId()); + romeCategory.setScheme(cat.getUrl()); + romeCategory.setLabel(cat.getName()); + romeCats.add(romeCategory); + } + entry.setCategories(romeCats); + } + entry.setPublished(publicationDate == null ? new Date() : publicationDate); + entry.setModified(modificationDate == null ? new Date() : modificationDate); + + final List modules = new ArrayList(); + final AppModule control = new AppModuleImpl(); + control.setDraft(new Boolean(draft)); + modules.add(control); + entry.setModules(modules); + + return entry; + } + +} diff --git a/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomEntryIterator.java b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomEntryIterator.java new file mode 100644 index 0000000..74e0956 --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomEntryIterator.java @@ -0,0 +1,80 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient.atomprotocol; + +import java.util.Iterator; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.rometools.propono.atom.client.ClientEntry; +import com.rometools.propono.atom.client.ClientMediaEntry; +import com.rometools.propono.blogclient.BlogClientException; +import com.rometools.propono.blogclient.BlogEntry; + +/** + * Atom protocol implementation of BlogClient entry iterator. + */ +public class AtomEntryIterator implements Iterator { + + private static final Logger LOG = LoggerFactory.getLogger(AtomEntryIterator.class); + + private Iterator iterator = null; + private AtomCollection collection = null; + + AtomEntryIterator(final AtomCollection collection) throws BlogClientException { + try { + this.collection = collection; + iterator = collection.getClientCollection().getEntries(); + } catch (final Exception e) { + throw new BlogClientException("ERROR fetching collection", e); + } + } + + /** + * True if more entries are available. + */ + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + /** + * Get next entry. + */ + @Override + public BlogEntry next() { + try { + final ClientEntry entry = iterator.next(); + if (entry instanceof ClientMediaEntry) { + return new AtomResource(collection, (ClientMediaEntry) entry); + } else { + return new AtomEntry(collection, entry); + } + } catch (final Exception e) { + LOG.error("An error occured while fetching entry", e); + } + return null; + } + + /** + * Remove is not supported. + */ + @Override + public void remove() { + // optional method, not implemented + } +} diff --git a/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomResource.java b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomResource.java new file mode 100644 index 0000000..59ccf60 --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/atomprotocol/AtomResource.java @@ -0,0 +1,132 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient.atomprotocol; + +import java.io.InputStream; +import java.util.List; + +import com.rometools.propono.atom.client.ClientAtomService; +import com.rometools.propono.atom.client.ClientCollection; +import com.rometools.propono.atom.client.ClientEntry; +import com.rometools.propono.atom.client.ClientMediaEntry; +import com.rometools.propono.blogclient.BlogClientException; +import com.rometools.propono.blogclient.BlogEntry; +import com.rometools.propono.blogclient.BlogResource; +import com.rometools.rome.feed.atom.Link; + +/** + * Atom protocol implementation of BlogResource. + */ +public class AtomResource extends AtomEntry implements BlogResource { + + private AtomCollection collection; + private byte[] bytes; + + AtomResource(final AtomCollection collection, final String name, final String contentType, final byte[] bytes) throws BlogClientException { + super((AtomBlog) collection.getBlog(), collection); + this.collection = collection; + this.bytes = bytes; + final BlogEntry.Content rcontent = new BlogEntry.Content(); + rcontent.setType(contentType); + setContent(rcontent); + } + + AtomResource(final AtomCollection collection, final ClientMediaEntry entry) throws BlogClientException { + super(collection, entry); + } + + AtomResource(final AtomBlog blog, final ClientMediaEntry entry) throws BlogClientException { + super(blog, entry); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return getTitle(); + } + + byte[] getBytes() { + return bytes; + } + + /** + * {@inheritDoc} + */ + @Override + public InputStream getAsStream() throws BlogClientException { + try { + return null; // ((ClientMediaEntry)clientEntry).getAsStream(); + } catch (final Exception e) { + throw new BlogClientException("Error creating entry", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void save() throws BlogClientException { + try { + if (getToken() == null) { + final ClientAtomService clientService = ((AtomBlog) getBlog()).getService(); + final ClientCollection clientCollection = collection.getClientCollection(); + + final ClientMediaEntry clientEntry = new ClientMediaEntry(clientService, clientCollection, getTitle(), "", getContent().getType(), getBytes()); + + copyToRomeEntry(clientEntry); + collection.getClientCollection().addEntry(clientEntry); + editURI = clientEntry.getEditURI(); + + } else { + final ClientAtomService clientService = ((AtomBlog) getBlog()).getService(); + final ClientMediaEntry clientEntry = (ClientMediaEntry) clientService.getEntry(editURI); + clientEntry.update(); + } + } catch (final Exception e) { + throw new BlogClientException("Error creating entry", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void update(final byte[] newBytes) throws BlogClientException { + try { + // ((ClientMediaEntry)clientEntry).setBytes(newBytes); + // clientEntry.update(); + } catch (final Exception e) { + throw new BlogClientException("Error creating entry", e); + } + } + + @Override + void copyFromRomeEntry(final ClientEntry entry) { + super.copyFromRomeEntry(entry); + final List links = entry.getOtherLinks(); + if (links != null) { + for (final Link link : links) { + if ("edit-media".equals(link.getRel())) { + id = link.getHrefResolved(); + break; + } + } + } + } + +} diff --git a/src/main/java/com/rometools/propono/blogclient/blogclient-diagram.gif b/src/main/java/com/rometools/propono/blogclient/blogclient-diagram.gif new file mode 100644 index 0000000..35dac52 Binary files /dev/null and b/src/main/java/com/rometools/propono/blogclient/blogclient-diagram.gif differ diff --git a/src/main/java/com/rometools/propono/blogclient/metaweblog/MetaWeblogBlog.java b/src/main/java/com/rometools/propono/blogclient/metaweblog/MetaWeblogBlog.java new file mode 100644 index 0000000..5367d57 --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/metaweblog/MetaWeblogBlog.java @@ -0,0 +1,467 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient.metaweblog; + +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import org.apache.xmlrpc.client.XmlRpcClient; +import org.apache.xmlrpc.client.XmlRpcClientConfigImpl; + +import com.rometools.propono.blogclient.Blog; +import com.rometools.propono.blogclient.BlogClientException; +import com.rometools.propono.blogclient.BlogEntry; +import com.rometools.propono.blogclient.BlogEntry.Category; +import com.rometools.propono.blogclient.BlogResource; + +/** + * Blog implementation that uses a mix of Blogger and MetaWeblog API methods. + */ +public class MetaWeblogBlog implements Blog { + + private final String blogid; + private final String name; + private final URL url; + private final String userName; + private final String password; + private final Map collections; + + private String appkey = "dummy"; + private XmlRpcClient xmlRpcClient = null; + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return name; + } + + /** + * {@inheritDoc} + */ + @Override + public String getToken() { + return blogid; + } + + /** + * String representation of blog, returns the name. + */ + @Override + public String toString() { + return getName(); + } + + private XmlRpcClient getXmlRpcClient() { + + if (xmlRpcClient == null) { + final XmlRpcClientConfigImpl xmlrpcConfig = new XmlRpcClientConfigImpl(); + xmlrpcConfig.setServerURL(url); + xmlRpcClient = new XmlRpcClient(); + xmlRpcClient.setConfig(xmlrpcConfig); + } + return xmlRpcClient; + } + + MetaWeblogBlog(final String blogid, final String name, final URL url, final String userName, final String password) { + this.blogid = blogid; + this.name = name; + this.url = url; + this.userName = userName; + this.password = password; + collections = new TreeMap(); + collections.put("entries", new MetaWeblogBlogCollection(this, "entries", "Entries", "entry")); + collections.put("resources", new MetaWeblogBlogCollection(this, "resources", "Resources", "*")); + } + + MetaWeblogBlog(final String blogId, final String name, final URL url, final String userName, final String password, final String appkey) { + this(blogId, name, url, userName, password); + this.appkey = appkey; + } + + /** + * {@inheritDoc} + */ + @Override + public BlogEntry newEntry() { + return new MetaWeblogEntry(this, new HashMap()); + } + + String saveEntry(final BlogEntry entry) throws BlogClientException { + final Blog.Collection col = collections.get("entries"); + return col.saveEntry(entry); + } + + /** + * {@inheritDoc} + */ + @Override + public BlogEntry getEntry(final String id) throws BlogClientException { + try { + final Object[] params = new Object[] { id, userName, password }; + final Object response = getXmlRpcClient().execute("metaWeblog.getPost", params); + @SuppressWarnings("unchecked") + final Map result = (Map) response; + return new MetaWeblogEntry(this, result); + } catch (final Exception e) { + throw new BlogClientException("ERROR: XML-RPC error getting entry", e); + } + } + + void deleteEntry(final String id) throws BlogClientException { + try { + getXmlRpcClient().execute("blogger.deletePost", new Object[] { appkey, id, userName, password, Boolean.FALSE }); + } catch (final Exception e) { + throw new BlogClientException("ERROR: XML-RPC error getting entry", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator getEntries() throws BlogClientException { + return new EntryIterator(); + } + + /** + * {@inheritDoc} + */ + @Override + public BlogResource newResource(final String name, final String contentType, final byte[] bytes) throws BlogClientException { + return new MetaWeblogResource(this, name, contentType, bytes); + } + + String saveResource(final MetaWeblogResource resource) throws BlogClientException { + final Blog.Collection col = collections.get("resources"); + return col.saveResource(resource); + } + + BlogResource getResource(final String token) throws BlogClientException { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public NoOpIterator getResources() throws BlogClientException { + return new NoOpIterator(); + } + + void deleteResource(final BlogResource resource) throws BlogClientException { + // no-op + } + + /** + * {@inheritDoc} + */ + @Override + public List getCategories() throws BlogClientException { + + final ArrayList ret = new ArrayList(); + + try { + + final Object result = getXmlRpcClient().execute("metaWeblog.getCategories", new Object[] { blogid, userName, password }); + + if (result != null && result instanceof HashMap) { + + // Standard MetaWeblog API style: struct of struts + @SuppressWarnings("unchecked") + final Map catsmap = (Map) result; + + final Set keys = catsmap.keySet(); + for (final String key : keys) { + @SuppressWarnings("unchecked") + final Map catmap = (Map) catsmap.get(key); + final BlogEntry.Category category = new BlogEntry.Category(key); + final String description = (String) catmap.get("description"); + category.setName(description); + // catmap.get("htmlUrl"); + // catmap.get("rssUrl"); + ret.add(category); + } + + } else if (result != null && result instanceof Object[]) { + // Wordpress style: array of structs + final Object[] array = (Object[]) result; + for (final Object map : array) { + @SuppressWarnings("unchecked") + final Map catmap = (Map) map; + final String categoryId = (String) catmap.get("categoryId"); + final String categoryName = (String) catmap.get("categoryName"); + final BlogEntry.Category category = new BlogEntry.Category(categoryId); + category.setName(categoryName); + ret.add(category); + } + } + } catch (final Exception e) { + e.printStackTrace(); + } + + return ret; + + } + + private Map createPostStructure(final BlogEntry entry) { + return ((MetaWeblogEntry) entry).toPostStructure(); + } + + /** + * {@inheritDoc} + */ + @Override + public List getCollections() throws BlogClientException { + return new ArrayList(collections.values()); + } + + /** + * {@inheritDoc} + */ + @Override + public Blog.Collection getCollection(final String token) throws BlogClientException { + return collections.get(token); + } + + // ------------------------------------------------------------------------- + + /** MetaWeblog API impplementation of Blog.Collection */ + public class MetaWeblogBlogCollection implements Blog.Collection { + private String accept = null; + private String title = null; + private String token = null; + private Blog blog = null; + + /** + * @param token Identifier for collection, unique within blog + * @param title Title of collection + * @param accept Content types accepted, either "entry" or "*" + */ + public MetaWeblogBlogCollection(final Blog blog, final String token, final String title, final String accept) { + this.blog = blog; + this.accept = accept; + this.title = title; + this.token = token; + } + + /** + * {@inheritDoc} + */ + @Override + public String getTitle() { + return title; + } + + /** + * {@inheritDoc} + */ + @Override + public String getToken() { + return token; + } + + /** + * {@inheritDoc} + */ + @Override + public List getAccepts() { + return Collections.singletonList(accept); + } + + /** + * {@inheritDoc} + */ + @Override + public BlogResource newResource(final String name, final String contentType, final byte[] bytes) throws BlogClientException { + return blog.newResource(name, contentType, bytes); + } + + /** + * {@inheritDoc} + */ + @Override + public BlogEntry newEntry() throws BlogClientException { + return blog.newEntry(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean accepts(final String ct) { + if (accept.equals("*")) { + // everything accepted + return true; + } else if (accept.equals("entry") && ct.equals("application/metaweblog+xml")) { + // entries only accepted and "application/metaweblog+xml" means entry + return true; + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator getEntries() throws BlogClientException { + Iterator ret = null; + if (accept.equals("entry")) { + ret = MetaWeblogBlog.this.getEntries(); + } else { + ret = getResources(); + } + return ret; + } + + /** + * {@inheritDoc} + */ + @Override + public String saveEntry(final BlogEntry entry) throws BlogClientException { + String ret = entry.getId(); + if (entry.getId() == null) { + try { + ret = (String) getXmlRpcClient().execute("metaWeblog.newPost", + new Object[] { blogid, userName, password, createPostStructure(entry), new Boolean(!entry.getDraft()) }); + } catch (final Exception e) { + throw new BlogClientException("ERROR: XML-RPC error saving new entry", e); + } + } else { + try { + getXmlRpcClient().execute("metaWeblog.editPost", + new Object[] { entry.getId(), userName, password, createPostStructure(entry), new Boolean(!entry.getDraft()) }); + } catch (final Exception e) { + throw new BlogClientException("ERROR: XML-RPC error updating entry", e); + } + } + return ret; + } + + /** + * {@inheritDoc} + */ + @Override + public String saveResource(final BlogResource res) throws BlogClientException { + final MetaWeblogResource resource = (MetaWeblogResource) res; + try { + final HashMap resmap = new HashMap(); + resmap.put("name", resource.getName()); + resmap.put("type", resource.getContent().getType()); + resmap.put("bits", resource.getBytes()); + final Object[] params = new Object[] { blogid, userName, password, resmap }; + final Object response = getXmlRpcClient().execute("metaWeblog.newMediaObject", params); + @SuppressWarnings("unchecked") + final Map result = (Map) response; + final String url = (String) result.get("url"); + res.getContent().setSrc(url); + return url; + } catch (final Exception e) { + throw new BlogClientException("ERROR: loading or uploading file", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public List getCategories() throws BlogClientException { + return MetaWeblogBlog.this.getCategories(); + } + + /** + * {@inheritDoc} + */ + @Override + public Blog getBlog() { + return blog; + } + } + + // ------------------------------------------------------------------------- + /** + * Iterates over MetaWeblog API entries. + */ + public class EntryIterator implements Iterator { + + private int pos = 0; + private boolean eod = false; + private static final int BUFSIZE = 30; + private List> results = null; + + /** + * Iterator for looping over MetaWeblog API entries. + */ + public EntryIterator() throws BlogClientException { + getNextEntries(); + } + + /** + * Returns true if more entries are avialable. + */ + @Override + public boolean hasNext() { + if (pos == results.size() && !eod) { + try { + getNextEntries(); + } catch (final Exception ignored) { + } + } + return pos < results.size(); + } + + /** + * Get next entry. + */ + @Override + public BlogEntry next() { + return new MetaWeblogEntry(MetaWeblogBlog.this, results.get(pos++)); + } + + /** + * Remove is not implemented. + */ + @Override + public void remove() { + } + + private void getNextEntries() throws BlogClientException { + final int requestSize = pos + BUFSIZE; + try { + final Object[] params = new Object[] { blogid, userName, password, new Integer(requestSize) }; + final Object response = getXmlRpcClient().execute("metaWeblog.getRecentPosts", params); + @SuppressWarnings("unchecked") + final Map[] resultsArray = (Map[]) response; + results = Arrays.asList(resultsArray); + } catch (final Exception e) { + throw new BlogClientException("ERROR: XML-RPC error getting entry", e); + } + if (results.size() < requestSize) { + eod = true; + } + } + } + +} diff --git a/src/main/java/com/rometools/propono/blogclient/metaweblog/MetaWeblogConnection.java b/src/main/java/com/rometools/propono/blogclient/metaweblog/MetaWeblogConnection.java new file mode 100644 index 0000000..8800f19 --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/metaweblog/MetaWeblogConnection.java @@ -0,0 +1,106 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient.metaweblog; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.xmlrpc.XmlRpcException; +import org.apache.xmlrpc.client.XmlRpcClient; +import org.apache.xmlrpc.client.XmlRpcClientConfigImpl; + +import com.rometools.propono.blogclient.Blog; +import com.rometools.propono.blogclient.BlogClientException; +import com.rometools.propono.blogclient.BlogConnection; + +/** + * BlogClient implementation that uses a mix of Blogger and MetaWeblog API methods. + */ +public class MetaWeblogConnection implements BlogConnection { + + private URL url = null; + private String userName = null; + private String password = null; + private String appkey = "null"; + private Map blogs = null; + + private XmlRpcClient xmlRpcClient = null; + + public MetaWeblogConnection(final String url, final String userName, final String password) throws BlogClientException { + this.userName = userName; + this.password = password; + try { + this.url = new URL(url); + blogs = createBlogMap(); + } catch (final Throwable t) { + throw new BlogClientException("ERROR connecting to server", t); + } + } + + private XmlRpcClient getXmlRpcClient() { + if (xmlRpcClient == null) { + final XmlRpcClientConfigImpl xmlrpcConfig = new XmlRpcClientConfigImpl(); + xmlrpcConfig.setServerURL(url); + xmlRpcClient = new XmlRpcClient(); + xmlRpcClient.setConfig(xmlrpcConfig); + } + return xmlRpcClient; + } + + /** + * {@inheritDoc} + */ + @Override + public List getBlogs() { + return new ArrayList(blogs.values()); + } + + /** + * {@inheritDoc} + */ + private Map createBlogMap() throws XmlRpcException, IOException { + final Map blogMap = new HashMap(); + final Object[] results = (Object[]) getXmlRpcClient().execute("blogger.getUsersBlogs", new Object[] { appkey, userName, password }); + for (final Object result : results) { + @SuppressWarnings("unchecked") + final Map blog = (Map) result; + final String blogid = (String) blog.get("blogid"); + final String name = (String) blog.get("blogName"); + blogMap.put(blogid, new MetaWeblogBlog(blogid, name, url, userName, password)); + } + return blogMap; + } + + /** + * {@inheritDoc} + */ + @Override + public Blog getBlog(final String token) { + return blogs.get(token); + } + + /** + * {@inheritDoc} + */ + @Override + public void setAppkey(final String appkey) { + this.appkey = appkey; + } +} diff --git a/src/main/java/com/rometools/propono/blogclient/metaweblog/MetaWeblogEntry.java b/src/main/java/com/rometools/propono/blogclient/metaweblog/MetaWeblogEntry.java new file mode 100644 index 0000000..f558773 --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/metaweblog/MetaWeblogEntry.java @@ -0,0 +1,126 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient.metaweblog; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.rometools.propono.blogclient.BaseBlogEntry; +import com.rometools.propono.blogclient.BlogClientException; +import com.rometools.propono.blogclient.BlogEntry; + +/** + * MetaWeblog API implementation of an entry. + */ +public class MetaWeblogEntry extends BaseBlogEntry { + + MetaWeblogEntry(final MetaWeblogBlog blog, final Map entryMap) { + super(blog); + id = (String) entryMap.get("postid"); + + content = new Content((String) entryMap.get("description")); + + // let's pretend MetaWeblog API has a content-type + content.setType("application/metaweblog+xml"); + + // no way to tell if entry is draft or not + draft = false; + + title = (String) entryMap.get("title"); + publicationDate = (Date) entryMap.get("dateCreated"); + permalink = (String) entryMap.get("permaLink"); + + // AlexisMP: fix to get the author value populated. + author.setName((String) entryMap.get("userid")); + author.setEmail((String) entryMap.get("author")); + + categories = new ArrayList(); + final Object[] catArray = (Object[]) entryMap.get("categories"); + if (catArray != null) { + for (final Object element : catArray) { + final Category cat = new Category((String) element); + categories.add(cat); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public String getToken() { + return id; + } + + /** + * True if tokens are equal + */ + @Override + public boolean equals(final Object o) { + if (o instanceof MetaWeblogEntry) { + final MetaWeblogEntry other = (MetaWeblogEntry) o; + if (other.id != null && id != null) { + return other.id.equals(id); + } + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public void save() throws BlogClientException { + id = ((MetaWeblogBlog) getBlog()).saveEntry(this); + } + + /** + * {@inheritDoc} + */ + @Override + public void delete() throws BlogClientException { + ((MetaWeblogBlog) getBlog()).deleteEntry(id); + } + + Map toPostStructure() { + final Map struct = new HashMap(); + if (getTitle() != null) { + struct.put("title", getTitle()); + } + if (getContent() != null && getContent().getValue() != null) { + struct.put("description", getContent().getValue()); + } + if (getCategories() != null && !getCategories().isEmpty()) { + final List catArray = new ArrayList(); + final List cats = getCategories(); + for (int i = 0; i < cats.size(); i++) { + final BlogEntry.Category cat = cats.get(i); + catArray.add(cat.getName()); + } + struct.put("categories", catArray); + } + if (getPublicationDate() != null) { + struct.put("dateCreated", getPublicationDate()); + } + if (getId() != null) { + struct.put("postid", getId()); + } + return struct; + } +} diff --git a/src/main/java/com/rometools/propono/blogclient/metaweblog/MetaWeblogResource.java b/src/main/java/com/rometools/propono/blogclient/metaweblog/MetaWeblogResource.java new file mode 100644 index 0000000..440cd74 --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/metaweblog/MetaWeblogResource.java @@ -0,0 +1,121 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.blogclient.metaweblog; + +import java.io.InputStream; +import java.util.HashMap; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.methods.GetMethod; + +import com.rometools.propono.blogclient.BlogClientException; +import com.rometools.propono.blogclient.BlogResource; + +/** + * MetaWeblog API implementation of an resource entry. + */ +public class MetaWeblogResource extends MetaWeblogEntry implements BlogResource { + private final MetaWeblogBlog blog; + private final String name; + private final String contentType; + private byte[] bytes; + + MetaWeblogResource(final MetaWeblogBlog blog, final String name, final String contentType, final byte[] bytes) { + super(blog, new HashMap()); + this.blog = blog; + this.name = name; + this.contentType = contentType; + this.bytes = bytes; + content = new Content(); + content.setType(contentType); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return name; + } + + /** + * {@inheritDoc} + */ + @Override + public String getToken() { + return null; + } + + /** + * Get content-type of associated media resource. + */ + public String getContentType() { + return contentType; + } + + /** + * Get media resource as input stream. + */ + @Override + public InputStream getAsStream() throws BlogClientException { + final HttpClient httpClient = new HttpClient(); + final GetMethod method = new GetMethod(permalink); + try { + httpClient.executeMethod(method); + } catch (final Exception e) { + throw new BlogClientException("ERROR: error reading file", e); + } + if (method.getStatusCode() != 200) { + throw new BlogClientException("ERROR HTTP status=" + method.getStatusCode()); + } + try { + return method.getResponseBodyAsStream(); + } catch (final Exception e) { + throw new BlogClientException("ERROR: error reading file", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void save() throws BlogClientException { + blog.saveResource(this); + } + + /** + * {@inheritDoc} + */ + @Override + public void update(final byte[] bytes) throws BlogClientException { + this.bytes = bytes; + save(); + } + + /** + * Get resource data as byte array. + */ + public byte[] getBytes() { + return bytes; + } + + /** + * Not supported by MetaWeblog API + */ + @Override + public void delete() throws BlogClientException { + } +} diff --git a/src/main/java/com/rometools/propono/blogclient/metaweblog/NoOpIterator.java b/src/main/java/com/rometools/propono/blogclient/metaweblog/NoOpIterator.java new file mode 100644 index 0000000..0f147eb --- /dev/null +++ b/src/main/java/com/rometools/propono/blogclient/metaweblog/NoOpIterator.java @@ -0,0 +1,24 @@ +package com.rometools.propono.blogclient.metaweblog; + +import java.util.Iterator; + +class NoOpIterator implements Iterator { + + /** No-op */ + @Override + public boolean hasNext() { + return false; + } + + /** No-op */ + @Override + public T next() { + return null; + } + + /** No-op */ + @Override + public void remove() { + } + +} \ No newline at end of file diff --git a/src/main/java/com/rometools/propono/utils/ProponoException.java b/src/main/java/com/rometools/propono/utils/ProponoException.java new file mode 100644 index 0000000..0ba8a6f --- /dev/null +++ b/src/main/java/com/rometools/propono/utils/ProponoException.java @@ -0,0 +1,154 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.utils; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/** + * Base Propono exception class. + */ +public class ProponoException extends Exception { + + private static final long serialVersionUID = 1L; + private Throwable mRootCause = null; + + /** + * Construct emtpy exception object. + */ + public ProponoException() { + super(); + } + + /** + * Construct ProponoException with message string. + * + * @param s Error message string. + */ + public ProponoException(final String s) { + super(s); + } + + /** + * Construct ProponoException with message string. + * + * @param s Error message string. + */ + public ProponoException(final String s, final String longMessage) { + super(s); + } + + /** + * Construct ProponoException, wrapping existing throwable. + * + * @param s Error message + * @param t Existing connection to wrap. + */ + public ProponoException(final String s, final Throwable t) { + super(s); + mRootCause = t; + } + + /** + * Construct ProponoException, wrapping existing throwable. + * + * @param s Error message + * @param t Existing connection to wrap. + */ + public ProponoException(final String s, final String longMessage, final Throwable t) { + super(s); + mRootCause = t; + } + + /** + * Construct ProponoException, wrapping existing throwable. + * + * @param t Existing exception to be wrapped. + */ + public ProponoException(final Throwable t) { + mRootCause = t; + } + + /** + * Get root cause object, or null if none. + * + * @return Root cause or null if none. + */ + public Throwable getRootCause() { + return mRootCause; + } + + /** + * Get root cause message. + * + * @return Root cause message. + */ + public String getRootCauseMessage() { + String rcmessage = null; + if (getRootCause() != null) { + if (getRootCause().getCause() != null) { + rcmessage = getRootCause().getCause().getMessage(); + } + rcmessage = rcmessage == null ? getRootCause().getMessage() : rcmessage; + rcmessage = rcmessage == null ? super.getMessage() : rcmessage; + rcmessage = rcmessage == null ? "NONE" : rcmessage; + } + return rcmessage; + } + + /** + * Print stack trace for exception and for root cause exception if htere is one. + * + * @see java.lang.Throwable#printStackTrace() + */ + @Override + public void printStackTrace() { + super.printStackTrace(); + if (mRootCause != null) { + System.out.println("--- ROOT CAUSE ---"); + mRootCause.printStackTrace(); + } + } + + /** + * Print stack trace for exception and for root cause exception if htere is one. + * + * @param s Stream to print to. + */ + @Override + public void printStackTrace(final PrintStream s) { + super.printStackTrace(s); + if (mRootCause != null) { + s.println("--- ROOT CAUSE ---"); + mRootCause.printStackTrace(s); + } + } + + /** + * Print stack trace for exception and for root cause exception if htere is one. + * + * @param s Writer to write to. + */ + @Override + public void printStackTrace(final PrintWriter s) { + super.printStackTrace(s); + if (null != mRootCause) { + s.println("--- ROOT CAUSE ---"); + mRootCause.printStackTrace(s); + } + } + +} diff --git a/src/main/java/com/rometools/propono/utils/Utilities.java b/src/main/java/com/rometools/propono/utils/Utilities.java new file mode 100644 index 0000000..e0da06e --- /dev/null +++ b/src/main/java/com/rometools/propono/utils/Utilities.java @@ -0,0 +1,270 @@ +/* + * Copyright 2007 Dave Johnson (Blogapps project) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.utils; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.NoSuchElementException; +import java.util.StringTokenizer; +import java.util.regex.Pattern; + +/** + * Utilities for file I/O and string manipulation. + */ +public final class Utilities { + + private static final String LS = System.getProperty("line.separator"); + + private Utilities() { + } + + /** + * Returns the contents of the file in a byte array (from JavaAlmanac). + */ + public static byte[] getBytesFromFile(final File file) throws IOException { + + final InputStream is = new FileInputStream(file); + try { + + // Get the size of the file + final long length = file.length(); + + // You cannot create an array using a long type. + // It needs to be an int type. + // Before converting to an int type, check + // to ensure that file is not larger than Integer.MAX_VALUE. + if (length > Integer.MAX_VALUE) { + // File is too large + } + + // Create the byte array to hold the data + final 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()); + } + + return bytes; + + } finally { + is.close(); + } + } + + /** + * Read input from stream and into string. + */ + public static String streamToString(final InputStream is) throws IOException { + final StringBuffer sb = new StringBuffer(); + final 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(final InputStream input, final OutputStream output) throws IOException { + final BufferedInputStream in = new BufferedInputStream(input); + final BufferedOutputStream out = new BufferedOutputStream(output); + final 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 (final IOException ex) { + throw new IOException("Closing file streams, " + ex.getMessage()); + } + } + + /** + * Replaces occurences of non-alphanumeric characters with a supplied char. + */ + public static String replaceNonAlphanumeric(final String str, final char subst) { + final StringBuffer ret = new StringBuffer(str.length()); + final char[] testChars = str.toCharArray(); + for (final char testChar : testChars) { + if (Character.isLetterOrDigit(testChar)) { + ret.append(testChar); + } else { + ret.append(subst); + } + } + return ret.toString(); + } + + /** + * Convert string to string array. + */ + public static String[] stringToStringArray(final String instr, final String delim) throws NoSuchElementException, NumberFormatException { + final StringTokenizer toker = new StringTokenizer(instr, delim); + final 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(final String[] stringArray, final String delim) { + String ret = ""; + for (final String element : stringArray) { + if (ret.length() > 0) { + ret = ret + delim + element; + } else { + ret = element; + } + } + return ret; + } + + static Pattern absoluteURIPattern = Pattern.compile("^[a-z0-9]*:.*$"); + + // private static boolean isAbsoluteURI(final String uri) { + // return absoluteURIPattern.matcher(uri).find(); + // } + + // private static boolean isRelativeURI(final 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(final String baseURI, final 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 + // final int slashslash = xmlbase.indexOf("//"); + // final 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("..")) { + // final String ret = null; + // final String[] parts = append.split("/"); + // for (final String part : parts) { + // if ("..".equals(part)) { + // final int last = base.lastIndexOf("/"); + // if (last != -1) { + // base = base.substring(0, last); + // append = append.substring(3, append.length()); + // } else { + // break; + // } + // } + // } + // } + // return base + "/" + append; + // } + + // /** + // * Strip starting slash from beginning of string. + // */ + // private static String stripStartingSlash(String s) { + // if (s != null && s.startsWith("/")) { + // s = s.substring(1, s.length()); + // } + // return s; + // } + + // /** + // * Strip trailing slash from end of string. + // */ + // private static String stripTrailingSlash(String s) { + // if (s != null && s.endsWith("/")) { + // s = s.substring(0, s.length() - 1); + // } + // return s; + // } + +} diff --git a/src/main/resources/propono-version.properties b/src/main/resources/propono-version.properties new file mode 100644 index 0000000..118b2be --- /dev/null +++ b/src/main/resources/propono-version.properties @@ -0,0 +1 @@ +rome.propono.version=2.0.0 \ No newline at end of file diff --git a/src/main/resources/rome.properties b/src/main/resources/rome.properties new file mode 100644 index 0000000..a3e4af9 --- /dev/null +++ b/src/main/resources/rome.properties @@ -0,0 +1,7 @@ +# Configures Propono APP extension module + +atom_1.0.item.ModuleParser.classes=\ +com.rometools.propono.atom.common.rome.AppModuleParser + +atom_1.0.item.ModuleGenerator.classes=\ +com.rometools.propono.atom.common.rome.AppModuleGenerator diff --git a/src/site/apt/ROMEProponoVersion0.4.apt b/src/site/apt/ROMEProponoVersion0.4.apt new file mode 100644 index 0000000..5022c68 --- /dev/null +++ b/src/site/apt/ROMEProponoVersion0.4.apt @@ -0,0 +1,29 @@ + ----- + ROME Propono Version 0.4 + ----- + mkurz + ----- + 2011-08-16 04:52:04.427 + ----- + +ROME Propono Version 0.4 + + This is the first release of the {{{./index.html}Rome Propono}} publishing library. It's a beta release and will be followed closely by Propono 0.5. + +*Downloads + + * {{{./rome\-propono\-0.4\-src.zip}rome\-propono\-0.4\-src.zip}} + + * {{{./rome\-propono\-0.4.tar.gz}rome\-propono\-0.4.tar.gz}} + + * {{{./rome\-propono\-0.4.zip}rome\-propono\-0.4.zip}} + + * {{{./rome\-propono\-0.4\-src.tar.gz}rome\-propono\-0.4\-src.tar.gz}} + + [] + +*API Docs + + * {{{https://rome.dev.java.net/apidocs/subprojects/propono/0.4/overview\-summary.html}Propono 0.4 API Docs}} + + [] diff --git a/src/site/apt/ROMEProponoVersion0.5.apt b/src/site/apt/ROMEProponoVersion0.5.apt new file mode 100644 index 0000000..a125d44 --- /dev/null +++ b/src/site/apt/ROMEProponoVersion0.5.apt @@ -0,0 +1,39 @@ + ----- + ROME Propono Version 0.5 + ----- + mkurz + ----- + 2011-08-16 04:50:16.111 + ----- + +ROME Propono Version 0.5 + + This is the second release of the {{{./index.html}Rome Propono}} publishing library. It's a bug fix release follow\-on to 0.4. Here are the changes: + + * 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 + + [] + +*Downloads + + * {{{./rome\-propono\-0.5\-src.zip}rome\-propono\-0.5\-src.zip}} + + * {{{./rome\-propono\-0.5.tar.gz}rome\-propono\-0.5.tar.gz}} + + * {{{./rome\-propono\-0.5.zip}rome\-propono\-0.5.zip}} + + * {{{./rome\-propono\-0.5\-src.tar.gz}rome\-propono\-0.5\-src.tar.gz}} + + [] + +*API Docs + + * {{{https://rome.dev.java.net/apidocs/subprojects/propono/0.5/overview\-summary.html}Propono 0.5 API Docs}} + + [] diff --git a/src/site/apt/ROMEProponoVersion0.6.apt b/src/site/apt/ROMEProponoVersion0.6.apt new file mode 100644 index 0000000..5ecb57f --- /dev/null +++ b/src/site/apt/ROMEProponoVersion0.6.apt @@ -0,0 +1,85 @@ + ----- + ROME Propono Version 0.6 + ----- + mkurz + ----- + 2011-08-16 04:48:12.766 + ----- + +ROME Propono Version 0.6 + + September 30, 2007 + + This is the third release of the {{{./index.html}Rome Propono}} publishing library. It includes major changes to add support for the final + Atom Publishing Protocol specification relative URIs and out\-of\-line categories. It's an an interim release it includes a pre\-release + version of ROME 0.9.1\-dev. A new version will follow as soon as ROME 0.9.1 (or 1.0) has been finalized. + +*Downloads + + * {{{./rome\-propono\-0.6\-src.zip}rome\-propono\-0.6\-src.zip}} + + * {{{./rome\-propono\-0.6.tar.gz}rome\-propono\-0.6.tar.gz}} + + * {{{./rome\-propono\-0.6.zip}rome\-propono\-0.6.zip}} + + * {{{./rome\-propono\-0.6\-src.tar.gz}rome\-propono\-0.6\-src.tar.gz}} + + [] + +*API Docs + + * {{{https://rome.dev.java.net/apidocs/subprojects/propono/0.6/overview\-summary.html}Propono 0.6 API Docs}} + + [] + +*Changes + + * 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). + + * 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 {{{http://java.net/jira/browse/ROME\-67}http://java.net/jira/browse/ROME\-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 {{{http://java.net/jira/browse/ROME\-68}http://java.net/jira/browse/ROME\-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 + + [] diff --git a/src/site/apt/ROMEProponoVersion1.0.apt b/src/site/apt/ROMEProponoVersion1.0.apt new file mode 100644 index 0000000..621a6f5 --- /dev/null +++ b/src/site/apt/ROMEProponoVersion1.0.apt @@ -0,0 +1,41 @@ + ----- + ROME Propono Version 1.0 + ----- + mkurz + ----- + 2011-08-16 04:44:34.302 + ----- + +ROME Propono Version 1.0 + + April 2009 + + ROME Propono 1.0 is coming soon. If you'd like to help out, you can try 1.0 RC1 and provide feedback to us on the ROME dev mail list. + +*Downloads + + * {{{./rome\-propono\-1.0RC1\-src.zip}rome\-propono\-1.0RC1\-src.zip}} + + * {{{./rome\-propono\-1.0RC1.tar.gz}rome\-propono\-1.0RC1.tar.gz}} + + * {{{./rome\-propono\-1.0RC1.zip}rome\-propono\-1.0RC1.zip}} + + * {{{./rome\-propono\-1.0RC1\-src.tar.gz}rome\-propono\-1.0RC1\-src.tar.gz}} + + [] + +*API Docs + + * {{{https://rome.dev.java.net/apidocs/subprojects/propono/1.0/overview\-summary.html}Propono 1.0 API Docs}} + + [] + +*Changes + + * Updated to ROME 1.0 + + * Added support for pluggable authentication in the AtomPub client + + * Added support for OAuth in the AtomPub client, see Javadocs for details + + [] diff --git a/src/site/apt/index.apt b/src/site/apt/index.apt new file mode 100644 index 0000000..724bd95 --- /dev/null +++ b/src/site/apt/index.apt @@ -0,0 +1,31 @@ + ----- + Home + ----- + mkurz + ----- + 2011-08-16 04:43:25.062 + ----- + +ROME Propono + + The ROME Propono subproject is a Java class library that supports publishing protocols, specifically the Atom Publishing Protocol and the legacy MetaWeblog API. + Propono includes an Atom client library, an Atom server framework and a Blog client that supports both Atom protocol and the MetaWeblog API. + +Documentation + + * See the {{{./apidocs/overview\-summary.html}Propono API docs}} for an explanation of Propono usage, diagrams and code examples. + + [] + +Releases + + * {{{./ROMEProponoVersion1.0.html}ROME Propono Version 1.0 (propono)}} \- Release 1.0 coming soon... + + * {{{./ROMEProponoVersion0.6.html}ROME Propono Version 0.6 (propono)}} + + * {{{./ROMEProponoVersion0.5.html}ROME Propono Version 0.5 (propono)}} + + * {{{./ROMEProponoVersion0.4.html}ROME Propono Version 0.4 (propono)}} + + [] + \ No newline at end of file diff --git a/src/site/resources/.nojekyll b/src/site/resources/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/src/site/resources/css/site.css b/src/site/resources/css/site.css new file mode 100644 index 0000000..43c3cd8 --- /dev/null +++ b/src/site/resources/css/site.css @@ -0,0 +1,8 @@ +h1 { + padding: 4px 4px 4px 6px; + border: 1px solid #999; + color: #900; + background-color: #ddd; + font-weight:900; + font-size: x-large; +} \ No newline at end of file diff --git a/src/site/resources/images/romelogo.png b/src/site/resources/images/romelogo.png new file mode 100644 index 0000000..2c90608 Binary files /dev/null and b/src/site/resources/images/romelogo.png differ diff --git a/src/site/resources/rome-propono-0.4-src.tar.gz b/src/site/resources/rome-propono-0.4-src.tar.gz new file mode 100644 index 0000000..39dd993 Binary files /dev/null and b/src/site/resources/rome-propono-0.4-src.tar.gz differ diff --git a/src/site/resources/rome-propono-0.4-src.zip b/src/site/resources/rome-propono-0.4-src.zip new file mode 100644 index 0000000..e65fb8a Binary files /dev/null and b/src/site/resources/rome-propono-0.4-src.zip differ diff --git a/src/site/resources/rome-propono-0.4.tar.gz b/src/site/resources/rome-propono-0.4.tar.gz new file mode 100644 index 0000000..a7f1ddf Binary files /dev/null and b/src/site/resources/rome-propono-0.4.tar.gz differ diff --git a/src/site/resources/rome-propono-0.4.zip b/src/site/resources/rome-propono-0.4.zip new file mode 100644 index 0000000..fedeae3 Binary files /dev/null and b/src/site/resources/rome-propono-0.4.zip differ diff --git a/src/site/resources/rome-propono-0.5-src.tar.gz b/src/site/resources/rome-propono-0.5-src.tar.gz new file mode 100644 index 0000000..03f79b4 Binary files /dev/null and b/src/site/resources/rome-propono-0.5-src.tar.gz differ diff --git a/src/site/resources/rome-propono-0.5-src.zip b/src/site/resources/rome-propono-0.5-src.zip new file mode 100644 index 0000000..421dbac Binary files /dev/null and b/src/site/resources/rome-propono-0.5-src.zip differ diff --git a/src/site/resources/rome-propono-0.5.tar.gz b/src/site/resources/rome-propono-0.5.tar.gz new file mode 100644 index 0000000..013163b Binary files /dev/null and b/src/site/resources/rome-propono-0.5.tar.gz differ diff --git a/src/site/resources/rome-propono-0.5.zip b/src/site/resources/rome-propono-0.5.zip new file mode 100644 index 0000000..5489239 Binary files /dev/null and b/src/site/resources/rome-propono-0.5.zip differ diff --git a/src/site/resources/rome-propono-0.6-src.tar.gz b/src/site/resources/rome-propono-0.6-src.tar.gz new file mode 100644 index 0000000..f98f9f4 Binary files /dev/null and b/src/site/resources/rome-propono-0.6-src.tar.gz differ diff --git a/src/site/resources/rome-propono-0.6-src.zip b/src/site/resources/rome-propono-0.6-src.zip new file mode 100644 index 0000000..641f438 Binary files /dev/null and b/src/site/resources/rome-propono-0.6-src.zip differ diff --git a/src/site/resources/rome-propono-0.6.tar.gz b/src/site/resources/rome-propono-0.6.tar.gz new file mode 100644 index 0000000..e287903 Binary files /dev/null and b/src/site/resources/rome-propono-0.6.tar.gz differ diff --git a/src/site/resources/rome-propono-0.6.zip b/src/site/resources/rome-propono-0.6.zip new file mode 100644 index 0000000..8986f22 Binary files /dev/null and b/src/site/resources/rome-propono-0.6.zip differ diff --git a/src/site/resources/rome-propono-1.0RC1-src.tar.gz b/src/site/resources/rome-propono-1.0RC1-src.tar.gz new file mode 100644 index 0000000..a187e6f Binary files /dev/null and b/src/site/resources/rome-propono-1.0RC1-src.tar.gz differ diff --git a/src/site/resources/rome-propono-1.0RC1-src.zip b/src/site/resources/rome-propono-1.0RC1-src.zip new file mode 100644 index 0000000..b3b9fa9 Binary files /dev/null and b/src/site/resources/rome-propono-1.0RC1-src.zip differ diff --git a/src/site/resources/rome-propono-1.0RC1.tar.gz b/src/site/resources/rome-propono-1.0RC1.tar.gz new file mode 100644 index 0000000..5ef8766 Binary files /dev/null and b/src/site/resources/rome-propono-1.0RC1.tar.gz differ diff --git a/src/site/resources/rome-propono-1.0RC1.zip b/src/site/resources/rome-propono-1.0RC1.zip new file mode 100644 index 0000000..de9ca52 Binary files /dev/null and b/src/site/resources/rome-propono-1.0RC1.zip differ diff --git a/src/site/site.xml b/src/site/site.xml new file mode 100644 index 0000000..08f97b4 --- /dev/null +++ b/src/site/site.xml @@ -0,0 +1,30 @@ + + + + + org.apache.maven.skins + maven-fluido-skin + 1.3.0 + + + + ROME + images/romelogo.png + http://github.com/rometools/ + + + + + + + +

+ + + + + + \ No newline at end of file diff --git a/src/test/java/com/rometools/propono/atom/client/AtomClientTest.java b/src/test/java/com/rometools/propono/atom/client/AtomClientTest.java new file mode 100644 index 0000000..58739e3 --- /dev/null +++ b/src/test/java/com/rometools/propono/atom/client/AtomClientTest.java @@ -0,0 +1,420 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.client; + +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.List; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import org.junit.Ignore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.rometools.propono.atom.common.Categories; +import com.rometools.propono.atom.common.Collection; +import com.rometools.propono.utils.ProponoException; +import com.rometools.rome.feed.atom.Category; +import com.rometools.rome.feed.atom.Content; + +/** + * Simple APP test designed to run against a live Atom server. + */ +@Ignore +public class AtomClientTest extends TestCase { + + private static Logger log = LoggerFactory.getLogger(AtomClientTest.class); + + private static ClientAtomService service = null; + + // Basic Auth example + private static String endpoint = "http://localhost:8080/sample-atomserver/app"; + private static String username = "admin"; + private static String password = "admin"; + static { + try { + service = AtomClientFactory.getAtomService(endpoint, new BasicAuthStrategy(username, password)); + } catch (final Exception e) { + log.error("ERROR creating service", e); + } + } + + /* + * // Roller OAuth example private static String endpoint = + * "http://macsnoopdave:8080/roller-services/app"; private static String consumerKey = + * "55132608a2fb68816bcd3d1caeafc933"; private static String consumerSecret = + * "bb420783-fdea-4270-ab83-36445c18c307"; private static String requestUri = + * "http://macsnoopdave:8080/roller-services/oauth/requestToken"; private static String + * authorizeUri = + * "http://macsnoopdave:8080/roller-services/oauth/authorize?userId=roller&oauth_callback=none"; + * private static String accessUri = + * "http://macsnoopdave:8080/roller-services/oauth/accessToken"; private static String username + * = "roller"; private static String password = "n/a"; static { try { service = + * AtomClientFactory.getAtomService(endpoint, new OAuthStrategy( username, consumerKey, + * consumerSecret, "HMAC-SHA1", requestUri, authorizeUri, accessUri)); } catch (Exception e) { + * log.error("ERROR creating service", e); } } + */ + + // GData Blogger API + /* + * private static String endpoint = + * "http://www.blogger.com/feeds/default/blogs?alt=atom-service"; private static String email = + * "EMAIL"; private static String password = "PASSWORD"; private static String serviceName = + * "blogger"; static { try { service = AtomClientFactory.getAtomService(endpoint, new + * GDataAuthStrategy(email, password, serviceName)); } catch (Exception e) { + * log.error("ERROR creating service", e); } } + */ + + public AtomClientTest(final String testName) { + super(testName); + } + + public String getEndpoint() { + return endpoint; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + @Override + protected void setUp() throws Exception { + } + + @Override + protected void tearDown() throws Exception { + } + + public static Test suite() { + final TestSuite suite = new TestSuite(AtomClientTest.class); + return suite; + } + + /** + * Tests that server has introspection doc with at least one workspace. + */ + public void testGetAtomService() throws Exception { + assertNotNull(service); + assertTrue(!service.getWorkspaces().isEmpty()); + for (final Object element : service.getWorkspaces()) { + final ClientWorkspace space = (ClientWorkspace) element; + assertNotNull(space.getTitle()); + log.debug("Workspace: " + space.getTitle()); + for (final Object element2 : space.getCollections()) { + final ClientCollection col = (ClientCollection) element2; + 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().isEmpty()); + int count = 0; + for (final Object element : service.getWorkspaces()) { + final ClientWorkspace space = (ClientWorkspace) element; + assertNotNull(space.getTitle()); + + for (final Object element2 : space.getCollections()) { + final ClientCollection col = (ClientCollection) element2; + if (col.accepts(Collection.ENTRY_TYPE)) { + + // we found a collection that accepts entries, so post one + final ClientEntry m1 = col.createEntry(); + m1.setTitle("Test post"); + final 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 + final 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 (final 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().isEmpty()); + int count = 0; + for (final Object element : service.getWorkspaces()) { + final ClientWorkspace space = (ClientWorkspace) element; + assertNotNull(space.getTitle()); + + for (final Object element2 : space.getCollections()) { + final ClientCollection col = (ClientCollection) element2; + if (col.accepts(Collection.ENTRY_TYPE)) { + + // we found a collection that accepts entries, so post one + final ClientEntry m1 = col.createEntry(); + m1.setTitle(col.getTitle() + ": Test post"); + final 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 + final ClientEntry m2 = col.getEntry(m1.getEditURI()); + assertNotNull(m2); + + m2.setTitle(col.getTitle() + ": Updated title"); + m2.update(); + + // entry should now be updated on server + final ClientEntry m3 = 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 (final ProponoException e) { + failed = true; + } + assertTrue(failed); + count++; + } + } + } + assertTrue(count > 0); + } + + public void testFindWorkspace() throws Exception { + assertNotNull(service); + final ClientWorkspace ws = (ClientWorkspace) service.findWorkspace("adminblog"); + if (ws != null) { + final ClientCollection col = (ClientCollection) ws.findCollection(null, "entry"); + final 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 + final ClientEntry saved = 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 (final 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().isEmpty()); + int count = 0; + for (final Object element2 : service.getWorkspaces()) { + final ClientWorkspace space = (ClientWorkspace) element2; + assertNotNull(space.getTitle()); + + for (final Object element3 : space.getCollections()) { + final ClientCollection col = (ClientCollection) element3; + if (col.accepts(Collection.ENTRY_TYPE)) { + + // we found a collection that accepts GIF, so post one + final ClientEntry m1 = col.createEntry(); + m1.setTitle("Test post"); + final 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; + final List entryCats = new ArrayList(); + for (int i = 0; i < col.getCategories().size(); i++) { + final Categories cats = col.getCategories().get(i); + if (cats.isFixed() && fixedCat == null) { + final String scheme = cats.getScheme(); + fixedCat = cats.getCategories().get(0); + if (fixedCat.getScheme() == null) { + fixedCat.setScheme(scheme); + } + entryCats.add(fixedCat); + } else if (!cats.isFixed() && unfixedCat == null) { + final String scheme = cats.getScheme(); + unfixedCat = new Category(); + unfixedCat.setScheme(scheme); + unfixedCat.setTerm("tagster"); + entryCats.add(unfixedCat); + } + } + m1.setCategories(entryCats); + col.addEntry(m1); + + // entry should now exist on server + final ClientEntry m2 = col.getEntry(m1.getEditURI()); + assertNotNull(m2); + + if (fixedCat != null) { + // we added a fixed category, let's make sure it's there + boolean foundCat = false; + for (final Object element : m2.getCategories()) { + final Category cat = (Category) element; + if (cat.getTerm().equals(fixedCat.getTerm())) { + foundCat = true; + } + } + assertTrue(foundCat); + } + + if (unfixedCat != null) { + // we added an unfixed category, let's make sure it's there + boolean foundCat = false; + for (final Object element : m2.getCategories()) { + final Category cat = (Category) element; + if (cat.getTerm().equals(unfixedCat.getTerm())) { + foundCat = true; + } + } + assertTrue(foundCat); + } + + // remove entry + m2.remove(); + + // fetching entry now should result in exception + boolean failed = false; + try { + col.getEntry(m1.getEditURI()); + } catch (final ProponoException e) { + failed = true; + } + assertTrue(failed); + count++; + } + } + } + assertTrue(count > 0); + } + + /** + * Post media entry to every media colletion avialable on server, then cleanup. + */ + public void testMediaPost() throws Exception { + assertNotNull(service); + assertTrue(!service.getWorkspaces().isEmpty()); + int count = 0; + for (final Object element : service.getWorkspaces()) { + final ClientWorkspace space = (ClientWorkspace) element; + assertNotNull(space.getTitle()); + + for (final Object element2 : space.getCollections()) { + final ClientCollection col = (ClientCollection) element2; + if (col.accepts("image/gif")) { + + // we found a collection that accepts GIF, so post one + final 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 + final 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 (final 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/rometools/propono/atom/client/BloggerDotComTest.java b/src/test/java/com/rometools/propono/atom/client/BloggerDotComTest.java new file mode 100644 index 0000000..b1aa31b --- /dev/null +++ b/src/test/java/com/rometools/propono/atom/client/BloggerDotComTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.client; + +import java.util.Iterator; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import org.junit.Ignore; + +import com.rometools.rome.feed.atom.Content; + +/** + * Simple APP test designed to run against Blogger.com. + */ +@Ignore +public class BloggerDotComTest extends TestCase { + + private final String collectionURI = "http://www.blogger.com/feeds/BLOGID/posts/default"; + // private final String atomServiceURI = + // "http://www.blogger.com/feeds/default/blogs?alt=atom-service"; + private final String email = "EMAIL"; + private final String password = "PASSWORD"; + + public BloggerDotComTest(final String testName) { + super(testName); + } + + @Override + protected void setUp() throws Exception { + } + + @Override + protected void tearDown() throws Exception { + } + + public static Test suite() { + final 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 (final Iterator it = col.getEntries(); it.hasNext();) { + final ClientEntry entry = it.next(); + assertNotNull(entry); + count++; + } + assertTrue(count > 0); + + col = AtomClientFactory.getCollection(collectionURI, new GDataAuthStrategy(email, password, "blogger")); + final ClientEntry p1 = col.createEntry(); + p1.setTitle("Propono post"); + final Content c = new Content(); + c.setValue("This is content from ROME Propono"); + p1.setContent(c); + col.addEntry(p1); + + final ClientEntry p2 = col.getEntry(p1.getEditURI()); + assertNotNull(p2); + + final ClientAtomService atomService = AtomClientFactory.getAtomService(collectionURI, new GDataAuthStrategy(email, password, "blogger")); + assertNotNull(atomService); + + } +} diff --git a/src/test/java/com/rometools/propono/atom/common/AtomServiceTest.java b/src/test/java/com/rometools/propono/atom/common/AtomServiceTest.java new file mode 100644 index 0000000..86f6f6b --- /dev/null +++ b/src/test/java/com/rometools/propono/atom/common/AtomServiceTest.java @@ -0,0 +1,148 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.common; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.input.SAXBuilder; + +/** + * Tests reading and writing of service document, no server needed. + */ +public class AtomServiceTest extends TestCase { + + public AtomServiceTest(final String testName) { + super(testName); + } + + @Override + protected void setUp() throws Exception { + } + + @Override + protected void tearDown() throws Exception { + } + + public static Test suite() { + final TestSuite suite = new TestSuite(AtomServiceTest.class); + + return suite; + } + + /** + * Test of documentToService method, of class AtomService. + */ + public void testDocumentToService() { + try { + // Load service document from disk + final SAXBuilder builder = new SAXBuilder(); + final Document document = builder.build(this.getClass().getResourceAsStream("/servicedoc1.xml")); + assertNotNull(document); + final AtomService service = AtomService.documentToService(document); + + int workspaceCount = 0; + + // Verify that service contains expected workspaces, collections and categories + for (final Object element : service.getWorkspaces()) { + final Workspace space = (Workspace) element; + assertNotNull(space.getTitle()); + workspaceCount++; + for (final Object element2 : space.getCollections()) { + final Collection col = (Collection) element2; + assertNotNull(col.getTitle()); + assertNotNull(col.getHrefResolved()); + int catCount = 0; + if (!col.getCategories().isEmpty()) { + for (final Object element3 : col.getCategories()) { + final Categories cats = (Categories) element3; + catCount += cats.getCategories().size(); + assertTrue(catCount > 0); + } + } + } + } + + assertTrue(workspaceCount > 0); + + } catch (final Exception e) { + e.printStackTrace(); + fail(); + } + } + + /** + * Test of documentToService method, of class AtomService. + */ + public void testServiceToDocument() { + try { + // Create service with workspace and collections + final AtomService service = new AtomService(); + + final Workspace workspace1 = new Workspace("workspace1", null); + final Workspace workspace2 = new Workspace("workspace1", null); + service.addWorkspace(workspace1); + service.addWorkspace(workspace2); + + final Collection collection11 = new Collection("collection11", null, "http://example.com/app/col11"); + final Collection collection12 = new Collection("collection12", null, "http://example.com/app/col12"); + workspace1.addCollection(collection11); + workspace1.addCollection(collection12); + + final Collection collection21 = new Collection("collection21", null, "http://example.com/app/col21"); + final 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 + final Document document = service.serviceToDocument(); + + // verify that JDOM document contains service, workspace and collection + assertEquals("service", document.getRootElement().getName()); + int workspaceCount = 0; + for (final Object element : document.getRootElement().getChildren()) { + final Element elem = (Element) element; + if ("workspace".equals(elem.getName())) { + workspaceCount++; + } + boolean workspaceTitle = false; + int collectionCount = 0; + for (final Object element2 : elem.getChildren()) { + final Element colelem = (Element) element2; + 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 (final Exception e) { + e.printStackTrace(); + fail(); + } + } +} diff --git a/src/test/java/com/rometools/propono/atom/common/CollectionTest.java b/src/test/java/com/rometools/propono/atom/common/CollectionTest.java new file mode 100644 index 0000000..267f85c --- /dev/null +++ b/src/test/java/com/rometools/propono/atom/common/CollectionTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.rometools.propono.atom.common; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import junit.framework.TestCase; + +/** + * Tests Collection class, no server needed. + */ +public class CollectionTest extends TestCase { + + public CollectionTest(final String testName) { + super(testName); + } + + @Override + protected void setUp() throws Exception { + } + + @Override + protected void tearDown() throws Exception { + } + + /** + * Test of accepts method, of class com.rometools.rome.propono.atom.common.Collection. + */ + public void testAccepts() { + + final 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")); + + final 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/rometools/propono/atom/server/AtomClientServerTest.java b/src/test/java/com/rometools/propono/atom/server/AtomClientServerTest.java new file mode 100644 index 0000000..c68752d --- /dev/null +++ b/src/test/java/com/rometools/propono/atom/server/AtomClientServerTest.java @@ -0,0 +1,428 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.rometools.propono.atom.server; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertTrue; + +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mortbay.http.HttpContext; +import org.mortbay.http.HttpServer; +import org.mortbay.http.SocketListener; +import org.mortbay.jetty.servlet.ServletHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.rometools.propono.atom.client.AtomClientFactory; +import com.rometools.propono.atom.client.BasicAuthStrategy; +import com.rometools.propono.atom.client.ClientAtomService; +import com.rometools.propono.atom.client.ClientCollection; +import com.rometools.propono.atom.client.ClientEntry; +import com.rometools.propono.atom.client.ClientMediaEntry; +import com.rometools.propono.atom.client.ClientWorkspace; +import com.rometools.propono.atom.common.Categories; +import com.rometools.propono.atom.common.Collection; +import com.rometools.propono.atom.common.Workspace; +import com.rometools.propono.utils.ProponoException; +import com.rometools.rome.feed.atom.Category; +import com.rometools.rome.feed.atom.Content; + +/** + * 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 { + + private static final Logger LOG = LoggerFactory.getLogger(AtomClientServerTest.class); + + private HttpServer server = null; + 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"; + + private static ClientAtomService service = null; + + public String getEndpoint() { + return ENDPOINT; + } + + public String getUsername() { + return USERNAME; + } + + public String getPassword() { + return PASSWORD; + } + + protected HttpServer getServer() { + return server; + } + + @Before + public void setUpClass() throws Exception { + + LOG.info("---------------------------------------------"); + LOG.info("Starting Jetty"); + LOG.info("---------------------------------------------"); + + setupServer(); + final HttpContext context = createContext(); + final ServletHandler servlets = createServletHandler(); + context.addHandler(servlets); + server.addContext(context); + server.start(); + + service = AtomClientFactory.getAtomService(getEndpoint(), new BasicAuthStrategy(getUsername(), getPassword())); + } + + @After + public void tearDownClass() throws Exception { + if (server != null) { + LOG.info("Stoping Jetty"); + server.stop(); + server.destroy(); + server = null; + } + } + + private void setupServer() throws InterruptedException { + // Create the server + if (server != null) { + server.stop(); + server = null; + } + server = new HttpServer(); + + // Create a port listener + final SocketListener listener = new SocketListener(); + listener.setPort(TESTPORT); + server.addListener(listener); + } + + private ServletHandler createServletHandler() { + System.setProperty("com.rometools.propono.atom.server.AtomHandlerFactory", "com.rometools.propono.atom.server.TestAtomHandlerFactory"); + final ServletHandler servlets = new ServletHandler(); + servlets.addServlet("app", "/app/*", "com.rometools.propono.atom.server.AtomServlet"); + return servlets; + } + + private HttpContext createContext() { + final HttpContext context = new HttpContext(); + context.setContextPath("/rome/*"); + return context; + } + + /** + * Tests that server has introspection doc with at least one workspace. + */ + @Test + public void testGetAtomService() throws Exception { + assertNotNull(service); + assertTrue(!service.getWorkspaces().isEmpty()); + for (final Workspace workspace : service.getWorkspaces()) { + final ClientWorkspace space = (ClientWorkspace) workspace; + assertNotNull(space.getTitle()); + LOG.debug("Workspace: {}", space.getTitle()); + for (final Object element : space.getCollections()) { + final ClientCollection col = (ClientCollection) element; + LOG.debug(" Collection: {} Accepts: {}", col.getTitle(), 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. + */ + @Test + public void testSimpleEntryPostAndRemove() throws Exception { + assertNotNull(service); + assertTrue(!service.getWorkspaces().isEmpty()); + int count = 0; + for (final Object element : service.getWorkspaces()) { + final ClientWorkspace space = (ClientWorkspace) element; + assertNotNull(space.getTitle()); + + for (final Object element2 : space.getCollections()) { + final ClientCollection col = (ClientCollection) element2; + if (col.accepts(Collection.ENTRY_TYPE)) { + + // we found a collection that accepts entries, so post one + final ClientEntry m1 = col.createEntry(); + m1.setTitle("Test post"); + final 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 + final 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 (final 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. + */ + @Test + public void testSimpleEntryPostUpdateAndRemove() throws Exception { + assertNotNull(service); + assertTrue(!service.getWorkspaces().isEmpty()); + int count = 0; + for (final Object element : service.getWorkspaces()) { + final ClientWorkspace space = (ClientWorkspace) element; + assertNotNull(space.getTitle()); + + for (final Object element2 : space.getCollections()) { + final ClientCollection col = (ClientCollection) element2; + if (col.accepts(Collection.ENTRY_TYPE)) { + + // we found a collection that accepts entries, so post one + final ClientEntry m1 = col.createEntry(); + m1.setTitle(col.getTitle() + ": Test post"); + final 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 + final ClientEntry m2 = col.getEntry(m1.getEditURI()); + assertNotNull(m2); + + m2.setTitle(col.getTitle() + ": Updated title"); + m2.update(); + + // entry should now be updated on server + final ClientEntry m3 = 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 (final ProponoException e) { + failed = true; + } + assertTrue(failed); + count++; + } + } + } + assertTrue(count > 0); + } + + @Test + public void testFindWorkspace() throws Exception { + assertNotNull(service); + final ClientWorkspace ws = (ClientWorkspace) service.findWorkspace("adminblog"); + if (ws != null) { + final ClientCollection col = (ClientCollection) ws.findCollection(null, "entry"); + final 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 + final ClientEntry saved = 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 (final 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. + */ + @Test + public void testEntryPostWithCategories() throws Exception { + assertNotNull(service); + assertTrue(!service.getWorkspaces().isEmpty()); + int count = 0; + for (final Object element2 : service.getWorkspaces()) { + final ClientWorkspace space = (ClientWorkspace) element2; + assertNotNull(space.getTitle()); + + for (final Object element3 : space.getCollections()) { + final ClientCollection col = (ClientCollection) element3; + if (col.accepts(Collection.ENTRY_TYPE)) { + + // we found a collection that accepts GIF, so post one + final ClientEntry m1 = col.createEntry(); + m1.setTitle("Test post"); + final 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; + final List entryCats = new ArrayList(); + for (int i = 0; i < col.getCategories().size(); i++) { + final Categories cats = col.getCategories().get(i); + if (cats.isFixed() && fixedCat == null) { + final String scheme = cats.getScheme(); + fixedCat = cats.getCategories().get(0); + if (fixedCat.getScheme() == null) { + fixedCat.setScheme(scheme); + } + entryCats.add(fixedCat); + } else if (!cats.isFixed() && unfixedCat == null) { + final String scheme = cats.getScheme(); + unfixedCat = new Category(); + unfixedCat.setScheme(scheme); + unfixedCat.setTerm("tagster"); + entryCats.add(unfixedCat); + } + } + m1.setCategories(entryCats); + col.addEntry(m1); + + // entry should now exist on server + final ClientEntry m2 = col.getEntry(m1.getEditURI()); + assertNotNull(m2); + + if (fixedCat != null) { + // we added a fixed category, let's make sure it's there + boolean foundCat = false; + for (final Object element : m2.getCategories()) { + final Category cat = (Category) element; + if (cat.getTerm().equals(fixedCat.getTerm())) { + foundCat = true; + } + } + assertTrue(foundCat); + } + + if (unfixedCat != null) { + // we added an unfixed category, let's make sure it's there + boolean foundCat = false; + for (final Object element : m2.getCategories()) { + final Category cat = (Category) element; + if (cat.getTerm().equals(unfixedCat.getTerm())) { + foundCat = true; + } + } + assertTrue(foundCat); + } + + // remove entry + m2.remove(); + + // fetching entry now should result in exception + boolean failed = false; + try { + col.getEntry(m1.getEditURI()); + } catch (final ProponoException e) { + failed = true; + } + assertTrue(failed); + count++; + } + } + } + assertTrue(count > 0); + } + + /** + * Post media entry to every media colletion avialable on server, then cleanup. + */ + public void testMediaPost() throws Exception { + assertNotNull(service); + assertTrue(!service.getWorkspaces().isEmpty()); + int count = 0; + for (final Object element : service.getWorkspaces()) { + final ClientWorkspace space = (ClientWorkspace) element; + assertNotNull(space.getTitle()); + + for (final Object element2 : space.getCollections()) { + final ClientCollection col = (ClientCollection) element2; + if (col.accepts("image/gif")) { + + // we found a collection that accepts GIF, so post one + final 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 + final 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 (final ProponoException e) { + failed = true; + } + assertTrue(failed); + count++; + } + } + } + assertTrue(count > 0); + } + +} diff --git a/src/test/java/com/rometools/propono/atom/server/TestAtomHandlerFactory.java b/src/test/java/com/rometools/propono/atom/server/TestAtomHandlerFactory.java new file mode 100644 index 0000000..eec2e59 --- /dev/null +++ b/src/test/java/com/rometools/propono/atom/server/TestAtomHandlerFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.rometools.propono.atom.server; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class TestAtomHandlerFactory extends AtomHandlerFactory { + + @Override + public AtomHandler newAtomHandler(final HttpServletRequest req, final HttpServletResponse res) { + return new TestAtomHandlerImpl(req, "target/testuploaddir"); + } +} diff --git a/src/test/java/com/rometools/propono/atom/server/TestAtomHandlerImpl.java b/src/test/java/com/rometools/propono/atom/server/TestAtomHandlerImpl.java new file mode 100644 index 0000000..c1eb257 --- /dev/null +++ b/src/test/java/com/rometools/propono/atom/server/TestAtomHandlerImpl.java @@ -0,0 +1,33 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.rometools.propono.atom.server; + +import javax.servlet.http.HttpServletRequest; + +import com.rometools.propono.atom.server.impl.FileBasedAtomHandler; + +public class TestAtomHandlerImpl extends FileBasedAtomHandler { + + public TestAtomHandlerImpl(final HttpServletRequest req, final String uploaddir) { + super(req, uploaddir); + } + + @Override + public boolean validateUser(final String loginId, final String password) { + return AtomClientServerTest.USERNAME.equals(loginId) && AtomClientServerTest.PASSWORD.equals(password); + } +} \ No newline at end of file diff --git a/src/test/java/com/rometools/propono/blogclient/SimpleBlogClientTest.java b/src/test/java/com/rometools/propono/blogclient/SimpleBlogClientTest.java new file mode 100644 index 0000000..55ae45c --- /dev/null +++ b/src/test/java/com/rometools/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.rometools.propono.blogclient; + +import java.io.File; +import java.util.Iterator; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import org.junit.Ignore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.rometools.propono.blogclient.Blog.Collection; +import com.rometools.propono.utils.Utilities; +import com.rometools.rome.io.impl.Atom10Parser; + +/** + * Tests Atom and MetaWeblog API CRUD via BlogClient. Exclude this from automated tests because it + * requires a live blog server. + */ +@Ignore +public class SimpleBlogClientTest extends TestCase { + + private static final Logger LOG = LoggerFactory.getLogger(SimpleBlogClientTest.class); + + private final String metaweblogEndpoint = "http://localhost:8080/roller/roller-services/xmlrpc"; + // private String atomEndpoint = "http://localhost:8080/roller/roller-services/app"; + private final String atomEndpoint = "http://localhost:8080/sample-atomserver/app"; + + // private final String endpoint = "http://localhost:8080/atom-fileserver/app"; + private final String username = "admin"; + private final String password = "admin"; + + public SimpleBlogClientTest(final String testName) { + super(testName); + } + + @Override + protected void setUp() throws Exception { + } + + @Override + protected void tearDown() throws Exception { + } + + public void testBlogClientAtom() throws Exception { + testBlogClient("atom", atomEndpoint); + } + + public void testBlogClientMetaWeblog() throws Exception { + testBlogClient("metaweblog", metaweblogEndpoint); + } + + public void testBlogClient(final String type, final String endpoint) throws Exception { + final BlogConnection conn = BlogConnectionFactory.getBlogConnection(type, endpoint, username, password); + + int blogCount = 0; + for (final Blog blog : conn.getBlogs()) { + LOG.debug(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(final String type, final String endpoint) throws Exception { + final BlogConnection conn = BlogConnectionFactory.getBlogConnection(type, endpoint, username, password); + assertNotNull(conn); + + final String title1 = "Test content"; + final String content1 = "Test content"; + + final Blog blog = conn.getBlogs().get(0); + BlogEntry entry = blog.newEntry(); + entry.setTitle(title1); + entry.setContent(new BlogEntry.Content(content1)); + entry.save(); + final 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 (final Exception e) { + notFound = true; + } + assertTrue(notFound); + } + + /** + * Post media entry to every media colletion avialable on server, then cleanup. + */ + public void testMediaPost(final String type, final String endpoint) throws Exception { + final BlogConnection conn = BlogConnectionFactory.getBlogConnection(type, endpoint, username, password); + assertNotNull(conn); + + assertTrue(!conn.getBlogs().isEmpty()); + int count = 0; + for (final Blog blog2 : conn.getBlogs()) { + final Blog blog = blog2; + assertNotNull(blog.getName()); + + for (final Collection collection : blog.getCollections()) { + final Blog.Collection col = collection; + if (col.accepts("image/gif")) { + + // we found a collection that accepts GIF, so post one + final 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 + final 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 (final 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(final String type, final String endpoint) throws Exception { + final BlogConnection conn = BlogConnectionFactory.getBlogConnection(type, endpoint, username, password); + assertNotNull(conn); + + final String title1 = "Test content"; + final String content1 = "Test content"; + + final Blog blog = conn.getBlogs().get(0); + + for (int i = 0; i < 10; i++) { + final BlogEntry entry = blog.newEntry(); + entry.setTitle(title1); + entry.setContent(new BlogEntry.Content(content1)); + entry.save(); + final String token = entry.getToken(); + assertTrue(Atom10Parser.isAbsoluteURI(token)); + assertNotNull(token); + } + + for (final Iterator it = blog.getEntries(); it.hasNext();) { + final BlogEntry blogEntry = it.next(); + assertTrue(Atom10Parser.isAbsoluteURI(blogEntry.getToken())); + blogEntry.delete(); + } + } + + public static Test suite() { + return new TestSuite(SimpleBlogClientTest.class); + } + +} 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/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 0000000..44dea42 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,13 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + + \ No newline at end of file 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..0e47e22 --- /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.rometools.propono.atom=debug +org.apache.commons.logging.simplelog.log.com.rometools.propono.atom.server.impl.FileBasedAtomHandler=debug +org.apache.commons.logging.simplelog.log.com.rometools.propono.atom.client.AtomClientServerTest=debug