diff --git a/app/pom.xml b/app/pom.xml
index e40aef0d15..ce9373340a 100644
--- a/app/pom.xml
+++ b/app/pom.xml
@@ -54,7 +54,7 @@ limitations under the License.
3.4.03.5.21.0b3
- 1.19.0
+ 1.19.02.0.165.3.395.8.14
@@ -522,11 +522,12 @@ limitations under the License.
-
+
- com.rometools
- rome-propono
- ${rome.version}
+ commons-httpclient
+ commons-httpclient
+ 3.1compile
@@ -592,6 +593,15 @@ limitations under the License.
test
+
+
+ com.thaiopensource
+ jing
+ 20091111
+ test
+
+
diff --git a/app/src/main/java/org/apache/roller/weblogger/util/WSSEUtilities.java b/app/src/main/java/org/apache/roller/weblogger/util/WSSEUtilities.java
deleted file mode 100644
index e63032b442..0000000000
--- a/app/src/main/java/org/apache/roller/weblogger/util/WSSEUtilities.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright 2005, Dave Johnson
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.roller.weblogger.util;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-
-import org.apache.commons.codec.binary.Base64;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-/**
- * Utilties to support WSSE authentication.
- * @author Dave Johnson
- */
-public class WSSEUtilities {
- public static synchronized String generateDigest(
- byte[] nonce, byte[] created, byte[] password) {
- String result = null;
- try {
- MessageDigest digester = MessageDigest.getInstance("SHA");
- digester.reset();
- digester.update(nonce);
- digester.update(created);
- digester.update(password);
- byte[] digest = digester.digest();
- result = base64Encode(digest);
- }
- catch (NoSuchAlgorithmException e) {
- result = null;
- }
- return result;
- }
- public static byte[] base64Decode(String value) throws IOException {
- return Base64.decodeBase64(value.getBytes(UTF_8));
- }
- public static String base64Encode(byte[] value) {
- return new String(Base64.encodeBase64(value));
- }
- public static String generateWSSEHeader(String userName, String password)
- throws UnsupportedEncodingException {
-
- byte[] nonceBytes = Long.toString(new Date().getTime()).getBytes();
- String nonce = WSSEUtilities.base64Encode(nonceBytes);
-
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
- String created = sdf.format(new Date());
-
- String digest = WSSEUtilities.generateDigest(
- nonceBytes, created.getBytes(UTF_8), password.getBytes(UTF_8));
-
- StringBuilder header = new StringBuilder("UsernameToken Username=\"");
- header.append(userName);
- header.append("\", ");
- header.append("PasswordDigest=\"");
- header.append(digest);
- header.append("\", ");
- header.append("Nonce=\"");
- header.append(nonce);
- header.append("\", ");
- header.append("Created=\"");
- header.append(created);
- header.append("\"");
- return header.toString();
- }
-}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomCategories.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomCategories.java
new file mode 100644
index 0000000000..02e04c33ec
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomCategories.java
@@ -0,0 +1,53 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An APP categories element (app:categories) describing the categories a
+ * collection accepts. When {@code fixed} is true the listed categories are the
+ * only ones allowed.
+ */
+public class AtomCategories {
+
+ private boolean fixed;
+ private String scheme;
+ private final List categories = new ArrayList<>();
+
+ public boolean isFixed() {
+ return fixed;
+ }
+
+ public void setFixed(boolean fixed) {
+ this.fixed = fixed;
+ }
+
+ public String getScheme() {
+ return scheme;
+ }
+
+ public void setScheme(String scheme) {
+ this.scheme = scheme;
+ }
+
+ public List getCategories() {
+ return categories;
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomCategory.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomCategory.java
new file mode 100644
index 0000000000..3a59950009
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomCategory.java
@@ -0,0 +1,53 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+/**
+ * An atom:category element. A null {@code scheme} signifies a free-form tag;
+ * a scheme that matches the weblog category scheme signifies a weblog category.
+ */
+public class AtomCategory {
+
+ private String term;
+ private String scheme;
+ private String label;
+
+ public String getTerm() {
+ return term;
+ }
+
+ public void setTerm(String term) {
+ this.term = term;
+ }
+
+ public String getScheme() {
+ return scheme;
+ }
+
+ public void setScheme(String scheme) {
+ this.scheme = scheme;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public void setLabel(String label) {
+ this.label = label;
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomCollection.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomCollection.java
new file mode 100644
index 0000000000..2b4507f20a
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomCollection.java
@@ -0,0 +1,60 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An APP collection (app:collection) within a workspace.
+ */
+public class AtomCollection {
+
+ private String title;
+ private String href;
+ private List accepts = new ArrayList<>();
+ private final List categories = new ArrayList<>();
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getHref() {
+ return href;
+ }
+
+ public void setHref(String href) {
+ this.href = href;
+ }
+
+ public List getAccepts() {
+ return accepts;
+ }
+
+ public void setAccepts(List accepts) {
+ this.accepts = accepts;
+ }
+
+ public List getCategories() {
+ return categories;
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomConstants.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomConstants.java
new file mode 100644
index 0000000000..07cf3d4609
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomConstants.java
@@ -0,0 +1,42 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+/**
+ * Constants shared by the StAX-based AtomPub implementation.
+ */
+public final class AtomConstants {
+
+ private AtomConstants() {
+ }
+
+ /** Atom Syndication Format namespace (RFC 4287). */
+ public static final String ATOM_NS = "http://www.w3.org/2005/Atom";
+
+ /** Atom Publishing Protocol namespace (RFC 5023). */
+ public static final String APP_NS = "http://www.w3.org/2007/app";
+
+ /** Media type for an Atom entry. */
+ public static final String ENTRY_MEDIA_TYPE = "application/atom+xml;type=entry";
+
+ /** Media type for an Atom feed/collection. */
+ public static final String FEED_MEDIA_TYPE = "application/atom+xml;type=feed;charset=utf-8";
+
+ /** Media type for an APP service document. */
+ public static final String SERVICE_MEDIA_TYPE = "application/atomsvc+xml;charset=utf-8";
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomContent.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomContent.java
new file mode 100644
index 0000000000..2740f97203
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomContent.java
@@ -0,0 +1,54 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+/**
+ * An Atom text/content construct (atom:content or atom:summary). For inline
+ * content {@code value} holds the text; for out-of-line media content
+ * {@code src} holds the source URI instead.
+ */
+public class AtomContent {
+
+ private String type;
+ private String value;
+ private String src;
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ public String getSrc() {
+ return src;
+ }
+
+ public void setSrc(String src) {
+ this.src = src;
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomEntry.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomEntry.java
new file mode 100644
index 0000000000..96b0005676
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomEntry.java
@@ -0,0 +1,141 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * An atom:entry. The {@code draft} and {@code edited} fields carry the APP
+ * control extension (app:control/app:draft and app:edited).
+ */
+public class AtomEntry {
+
+ private String id;
+ private String title;
+ private AtomContent content;
+ private AtomContent summary;
+ private Date published;
+ private Date updated;
+ private Date edited;
+ private boolean draft;
+ private List authors = new ArrayList<>();
+ private List categories = new ArrayList<>();
+ private List links = new ArrayList<>();
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public AtomContent getContent() {
+ return content;
+ }
+
+ public void setContent(AtomContent content) {
+ this.content = content;
+ }
+
+ public AtomContent getSummary() {
+ return summary;
+ }
+
+ public void setSummary(AtomContent summary) {
+ this.summary = summary;
+ }
+
+ public Date getPublished() {
+ return published;
+ }
+
+ public void setPublished(Date published) {
+ this.published = published;
+ }
+
+ public Date getUpdated() {
+ return updated;
+ }
+
+ public void setUpdated(Date updated) {
+ this.updated = updated;
+ }
+
+ public Date getEdited() {
+ return edited;
+ }
+
+ public void setEdited(Date edited) {
+ this.edited = edited;
+ }
+
+ public boolean isDraft() {
+ return draft;
+ }
+
+ public void setDraft(boolean draft) {
+ this.draft = draft;
+ }
+
+ public List getAuthors() {
+ return authors;
+ }
+
+ public void setAuthors(List authors) {
+ this.authors = authors;
+ }
+
+ public List getCategories() {
+ return categories;
+ }
+
+ public void setCategories(List categories) {
+ this.categories = categories;
+ }
+
+ public List getLinks() {
+ return links;
+ }
+
+ public void setLinks(List links) {
+ this.links = links;
+ }
+
+ /** Return the href of the first link with the given rel, or null. */
+ public String getLinkHref(String rel) {
+ if (links != null) {
+ for (AtomLink link : links) {
+ if (rel.equals(link.getRel())) {
+ return link.getHref();
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomException.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomException.java
new file mode 100644
index 0000000000..25fb18cfa1
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomException.java
@@ -0,0 +1,47 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Base exception for the AtomPub implementation. Carries the HTTP status code
+ * that the dispatcher servlet should return to the client.
+ */
+public class AtomException extends Exception {
+
+ private final int status;
+
+ public AtomException(String msg) {
+ this(msg, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null);
+ }
+
+ public AtomException(String msg, Throwable cause) {
+ this(msg, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, cause);
+ }
+
+ protected AtomException(String msg, int status, Throwable cause) {
+ super(msg, cause);
+ this.status = status;
+ }
+
+ /** HTTP status code to send to the client for this error. */
+ public int getStatus() {
+ return status;
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomFeed.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomFeed.java
new file mode 100644
index 0000000000..d253f100cd
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomFeed.java
@@ -0,0 +1,74 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * An atom:feed used to represent an AtomPub collection.
+ */
+public class AtomFeed {
+
+ private String id;
+ private String title;
+ private Date updated;
+ private List links = new ArrayList<>();
+ private List entries = new ArrayList<>();
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public Date getUpdated() {
+ return updated;
+ }
+
+ public void setUpdated(Date updated) {
+ this.updated = updated;
+ }
+
+ public List getLinks() {
+ return links;
+ }
+
+ public void setLinks(List links) {
+ this.links = links;
+ }
+
+ public List getEntries() {
+ return entries;
+ }
+
+ public void setEntries(List entries) {
+ this.entries = entries;
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomLink.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomLink.java
new file mode 100644
index 0000000000..2d17d5a918
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomLink.java
@@ -0,0 +1,60 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+/**
+ * An atom:link element.
+ */
+public class AtomLink {
+
+ private String rel;
+ private String href;
+ private String type;
+
+ public AtomLink() {
+ }
+
+ public AtomLink(String rel, String href) {
+ this.rel = rel;
+ this.href = href;
+ }
+
+ public String getRel() {
+ return rel;
+ }
+
+ public void setRel(String rel) {
+ this.rel = rel;
+ }
+
+ public String getHref() {
+ return href;
+ }
+
+ public void setHref(String href) {
+ this.href = href;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomMediaResource.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomMediaResource.java
new file mode 100644
index 0000000000..724ef30794
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomMediaResource.java
@@ -0,0 +1,63 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import java.io.InputStream;
+import java.util.Date;
+
+/**
+ * Holder for the binary data of a media resource, returned when a client GETs
+ * the edit-media URI so the dispatcher servlet can stream the bytes.
+ */
+public class AtomMediaResource {
+
+ private final String name;
+ private final long contentLength;
+ private final String contentType;
+ private final Date lastModified;
+ private final InputStream inputStream;
+
+ public AtomMediaResource(String name, long contentLength, String contentType,
+ Date lastModified, InputStream inputStream) {
+ this.name = name;
+ this.contentLength = contentLength;
+ this.contentType = contentType;
+ this.lastModified = lastModified;
+ this.inputStream = inputStream;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public long getContentLength() {
+ return contentLength;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public Date getLastModified() {
+ return lastModified;
+ }
+
+ public InputStream getInputStream() {
+ return inputStream;
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomHandlerFactory.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomNotAuthorizedException.java
similarity index 60%
rename from app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomHandlerFactory.java
rename to app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomNotAuthorizedException.java
index 4b3e1f3611..f568c5b13d 100644
--- a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomHandlerFactory.java
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomNotAuthorizedException.java
@@ -15,27 +15,17 @@
* copyright in this work, please see the NOTICE file in the top level
* directory of this distribution.
*/
-
package org.apache.roller.weblogger.webservices.atomprotocol;
-import com.rometools.propono.atom.server.AtomHandlerFactory;
-import com.rometools.propono.atom.server.AtomHandler;
-import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
- * Extends {@link com.rometools.propono.atom.server.AtomHandlerFactory} to create and return
- * {@link com.rometools.propono.atom.server.impl.FileBasedAtomHandler}.
+ * Thrown when an authenticated user is not permitted to perform the requested
+ * operation (HTTP 401, matching the previous ROME Propono behavior).
*/
-public class RollerAtomHandlerFactory extends AtomHandlerFactory {
-
- /**
- * Create new AtomHandler.
- */
- @Override
- public AtomHandler newAtomHandler(
- HttpServletRequest req, HttpServletResponse res) {
- return new RollerAtomHandler(req, res);
- }
+public class AtomNotAuthorizedException extends AtomException {
+
+ public AtomNotAuthorizedException(String msg) {
+ super(msg, HttpServletResponse.SC_UNAUTHORIZED, null);
+ }
}
-
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomNotFoundException.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomNotFoundException.java
new file mode 100644
index 0000000000..9f0a3e8670
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomNotFoundException.java
@@ -0,0 +1,30 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Thrown when a requested resource cannot be found (HTTP 404).
+ */
+public class AtomNotFoundException extends AtomException {
+
+ public AtomNotFoundException(String msg) {
+ super(msg, HttpServletResponse.SC_NOT_FOUND, null);
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomPerson.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomPerson.java
new file mode 100644
index 0000000000..2b713b222b
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomPerson.java
@@ -0,0 +1,43 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+/**
+ * An Atom person construct (atom:author).
+ */
+public class AtomPerson {
+
+ private String name;
+ private String email;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomReader.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomReader.java
new file mode 100644
index 0000000000..f3bf21ce57
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomReader.java
@@ -0,0 +1,146 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import static org.apache.roller.weblogger.webservices.atomprotocol.AtomConstants.APP_NS;
+import static org.apache.roller.weblogger.webservices.atomprotocol.AtomConstants.ATOM_NS;
+
+import java.io.InputStream;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.util.Date;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+
+/**
+ * Parses an incoming AtomPub request body (an atom:entry) into the wire model
+ * using the JDK StAX API. Replaces ROME's Atom parser.
+ *
+ *
DTD processing and external entities are disabled to protect against XXE
+ * attacks.
+ */
+public class AtomReader {
+
+ private final XMLInputFactory factory;
+
+ public AtomReader() {
+ factory = XMLInputFactory.newInstance();
+ factory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
+ factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
+ }
+
+ static Date parseDate(String text) {
+ if (text == null || text.isBlank()) {
+ return null;
+ }
+ String trimmed = text.trim();
+ try {
+ return Date.from(OffsetDateTime.parse(trimmed).toInstant());
+ } catch (Exception ignored) {
+ try {
+ return Date.from(Instant.parse(trimmed));
+ } catch (Exception ignored2) {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Parse an atom:entry from the given stream. The author is intentionally not
+ * read; the server sets the entry's creator from the authenticated user.
+ */
+ public AtomEntry parseEntry(InputStream in) throws AtomException {
+ XMLStreamReader r = null;
+ try {
+ r = factory.createXMLStreamReader(in, "UTF-8");
+ AtomEntry entry = new AtomEntry();
+ while (r.hasNext()) {
+ if (r.next() != XMLStreamConstants.START_ELEMENT) {
+ continue;
+ }
+ String ns = r.getNamespaceURI();
+ String name = r.getLocalName();
+ if (ATOM_NS.equals(ns)) {
+ switch (name) {
+ case "id":
+ entry.setId(r.getElementText());
+ break;
+ case "title":
+ entry.setTitle(r.getElementText());
+ break;
+ case "summary":
+ entry.setSummary(readContent(r));
+ break;
+ case "content":
+ if (entry.getContent() == null) {
+ entry.setContent(readContent(r));
+ }
+ break;
+ case "published":
+ entry.setPublished(parseDate(r.getElementText()));
+ break;
+ case "updated":
+ entry.setUpdated(parseDate(r.getElementText()));
+ break;
+ case "category":
+ entry.getCategories().add(readCategory(r));
+ break;
+ default:
+ break;
+ }
+ } else if (APP_NS.equals(ns) && "draft".equals(name)) {
+ String value = r.getElementText();
+ entry.setDraft(value != null && value.trim().equalsIgnoreCase("yes"));
+ }
+ }
+ return entry;
+ } catch (XMLStreamException ex) {
+ throw new AtomException("Error parsing Atom entry", ex);
+ } finally {
+ if (r != null) {
+ try {
+ r.close();
+ } catch (XMLStreamException ignored) {
+ // nothing useful to do on close failure
+ }
+ }
+ }
+ }
+
+ private AtomContent readContent(XMLStreamReader r) throws XMLStreamException {
+ AtomContent content = new AtomContent();
+ content.setType(r.getAttributeValue(null, "type"));
+ String src = r.getAttributeValue(null, "src");
+ content.setSrc(src);
+ if (src == null) {
+ content.setValue(r.getElementText());
+ }
+ return content;
+ }
+
+ private AtomCategory readCategory(XMLStreamReader r) {
+ AtomCategory cat = new AtomCategory();
+ cat.setTerm(r.getAttributeValue(null, "term"));
+ cat.setScheme(r.getAttributeValue(null, "scheme"));
+ cat.setLabel(r.getAttributeValue(null, "label"));
+ return cat;
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomRequest.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomRequest.java
new file mode 100644
index 0000000000..cd6e620038
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomRequest.java
@@ -0,0 +1,65 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Lightweight wrapper around an {@link HttpServletRequest} for AtomPub handlers.
+ * The dispatcher servlet reads any request body once into a byte array and
+ * passes it here so handlers can read it (and so {@code getPathInfo()} never
+ * returns null for the service-document URI).
+ */
+public class AtomRequest {
+
+ private static final byte[] EMPTY = new byte[0];
+
+ private final HttpServletRequest request;
+ private final byte[] body;
+
+ public AtomRequest(HttpServletRequest request, byte[] body) {
+ this.request = request;
+ this.body = (body != null) ? body : EMPTY;
+ }
+
+ /** Path info relative to the AtomPub servlet, never null ("" for the service doc). */
+ public String getPathInfo() {
+ String pathInfo = request.getPathInfo();
+ return (pathInfo != null) ? pathInfo : "";
+ }
+
+ public String getHeader(String name) {
+ return request.getHeader(name);
+ }
+
+ public String getContentType() {
+ return request.getContentType();
+ }
+
+ /** A fresh stream over the buffered request body. */
+ public InputStream getInputStream() {
+ return new ByteArrayInputStream(body);
+ }
+
+ public HttpServletRequest getRequest() {
+ return request;
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomServiceDoc.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomServiceDoc.java
new file mode 100644
index 0000000000..4640ac792a
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomServiceDoc.java
@@ -0,0 +1,33 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An APP service document (app:service).
+ */
+public class AtomServiceDoc {
+
+ private final List workspaces = new ArrayList<>();
+
+ public List getWorkspaces() {
+ return workspaces;
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomWorkspace.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomWorkspace.java
new file mode 100644
index 0000000000..9ea26e9afc
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomWorkspace.java
@@ -0,0 +1,42 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An APP workspace (app:workspace) within a service document.
+ */
+public class AtomWorkspace {
+
+ private String title;
+ private final List collections = new ArrayList<>();
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public List getCollections() {
+ return collections;
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomWriter.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomWriter.java
new file mode 100644
index 0000000000..1366a883a0
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomWriter.java
@@ -0,0 +1,235 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import static org.apache.roller.weblogger.webservices.atomprotocol.AtomConstants.APP_NS;
+import static org.apache.roller.weblogger.webservices.atomprotocol.AtomConstants.ATOM_NS;
+
+import java.io.OutputStream;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+
+/**
+ * Serializes the AtomPub wire model ({@link AtomEntry}, {@link AtomFeed},
+ * {@link AtomServiceDoc}) to XML using the JDK StAX API. Replaces the ROME and
+ * Propono serialization the AtomPub server previously relied on.
+ *
+ *
Atom entries and feeds are written with the Atom namespace as the default
+ * namespace (so atom elements are unprefixed) and the APP namespace bound to the
+ * {@code app} prefix. Service documents use the reverse: APP as the default
+ * namespace and {@code atom} for atom elements.
+ */
+public class AtomWriter {
+
+ private static final DateTimeFormatter RFC3339 =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC);
+
+ private final XMLOutputFactory factory = XMLOutputFactory.newInstance();
+
+ static String formatDate(Date date) {
+ return date == null ? null : RFC3339.format(date.toInstant());
+ }
+
+ public void writeEntry(OutputStream out, AtomEntry entry) throws AtomException {
+ try {
+ XMLStreamWriter w = factory.createXMLStreamWriter(out, "UTF-8");
+ w.writeStartDocument("UTF-8", "1.0");
+ w.setDefaultNamespace(ATOM_NS);
+ w.setPrefix("app", APP_NS);
+ w.writeStartElement(ATOM_NS, "entry");
+ w.writeDefaultNamespace(ATOM_NS);
+ w.writeNamespace("app", APP_NS);
+ writeEntryBody(w, entry);
+ w.writeEndElement();
+ w.writeEndDocument();
+ w.flush();
+ w.close();
+ } catch (XMLStreamException ex) {
+ throw new AtomException("Error serializing Atom entry", ex);
+ }
+ }
+
+ public void writeFeed(OutputStream out, AtomFeed feed) throws AtomException {
+ try {
+ XMLStreamWriter w = factory.createXMLStreamWriter(out, "UTF-8");
+ w.writeStartDocument("UTF-8", "1.0");
+ w.setDefaultNamespace(ATOM_NS);
+ w.setPrefix("app", APP_NS);
+ w.writeStartElement(ATOM_NS, "feed");
+ w.writeDefaultNamespace(ATOM_NS);
+ w.writeNamespace("app", APP_NS);
+ writeAtomText(w, "id", feed.getId());
+ writeAtomText(w, "title", feed.getTitle());
+ writeAtomText(w, "updated", formatDate(feed.getUpdated()));
+ for (AtomLink link : feed.getLinks()) {
+ writeLink(w, link);
+ }
+ for (AtomEntry entry : feed.getEntries()) {
+ w.writeStartElement(ATOM_NS, "entry");
+ writeEntryBody(w, entry);
+ w.writeEndElement();
+ }
+ w.writeEndElement();
+ w.writeEndDocument();
+ w.flush();
+ w.close();
+ } catch (XMLStreamException ex) {
+ throw new AtomException("Error serializing Atom feed", ex);
+ }
+ }
+
+ public void writeServiceDoc(OutputStream out, AtomServiceDoc service) throws AtomException {
+ try {
+ XMLStreamWriter w = factory.createXMLStreamWriter(out, "UTF-8");
+ w.writeStartDocument("UTF-8", "1.0");
+ w.setDefaultNamespace(APP_NS);
+ w.setPrefix("atom", ATOM_NS);
+ w.writeStartElement(APP_NS, "service");
+ w.writeDefaultNamespace(APP_NS);
+ w.writeNamespace("atom", ATOM_NS);
+ for (AtomWorkspace workspace : service.getWorkspaces()) {
+ w.writeStartElement(APP_NS, "workspace");
+ writeAtomText(w, "title", workspace.getTitle());
+ for (AtomCollection collection : workspace.getCollections()) {
+ w.writeStartElement(APP_NS, "collection");
+ if (collection.getHref() != null) {
+ w.writeAttribute("href", collection.getHref());
+ }
+ writeAtomText(w, "title", collection.getTitle());
+ for (String accept : collection.getAccepts()) {
+ w.writeStartElement(APP_NS, "accept");
+ w.writeCharacters(accept);
+ w.writeEndElement();
+ }
+ for (AtomCategories cats : collection.getCategories()) {
+ w.writeStartElement(APP_NS, "categories");
+ w.writeAttribute("fixed", cats.isFixed() ? "yes" : "no");
+ if (cats.getScheme() != null) {
+ w.writeAttribute("scheme", cats.getScheme());
+ }
+ for (AtomCategory cat : cats.getCategories()) {
+ writeCategory(w, cat);
+ }
+ w.writeEndElement();
+ }
+ w.writeEndElement();
+ }
+ w.writeEndElement();
+ }
+ w.writeEndElement();
+ w.writeEndDocument();
+ w.flush();
+ w.close();
+ } catch (XMLStreamException ex) {
+ throw new AtomException("Error serializing service document", ex);
+ }
+ }
+
+ private void writeEntryBody(XMLStreamWriter w, AtomEntry entry) throws XMLStreamException {
+ writeAtomText(w, "id", entry.getId());
+ writeAtomText(w, "title", entry.getTitle());
+ writeAtomText(w, "published", formatDate(entry.getPublished()));
+ writeAtomText(w, "updated", formatDate(entry.getUpdated()));
+ for (AtomPerson author : entry.getAuthors()) {
+ w.writeStartElement(ATOM_NS, "author");
+ writeAtomText(w, "name", author.getName());
+ writeAtomText(w, "email", author.getEmail());
+ w.writeEndElement();
+ }
+ for (AtomCategory cat : entry.getCategories()) {
+ writeCategory(w, cat);
+ }
+ if (entry.getSummary() != null) {
+ writeContent(w, "summary", entry.getSummary());
+ }
+ if (entry.getContent() != null) {
+ writeContent(w, "content", entry.getContent());
+ }
+ for (AtomLink link : entry.getLinks()) {
+ writeLink(w, link);
+ }
+ // APP control extension (RFC 5023)
+ w.writeStartElement(APP_NS, "control");
+ w.writeStartElement(APP_NS, "draft");
+ w.writeCharacters(entry.isDraft() ? "yes" : "no");
+ w.writeEndElement();
+ if (entry.getEdited() != null) {
+ w.writeStartElement(APP_NS, "edited");
+ w.writeCharacters(formatDate(entry.getEdited()));
+ w.writeEndElement();
+ }
+ w.writeEndElement();
+ }
+
+ private void writeContent(XMLStreamWriter w, String name, AtomContent content)
+ throws XMLStreamException {
+ w.writeStartElement(ATOM_NS, name);
+ if (content.getType() != null) {
+ w.writeAttribute("type", content.getType());
+ }
+ if (content.getSrc() != null) {
+ w.writeAttribute("src", content.getSrc());
+ } else if (content.getValue() != null) {
+ w.writeCharacters(content.getValue());
+ }
+ w.writeEndElement();
+ }
+
+ private void writeLink(XMLStreamWriter w, AtomLink link) throws XMLStreamException {
+ w.writeStartElement(ATOM_NS, "link");
+ if (link.getRel() != null) {
+ w.writeAttribute("rel", link.getRel());
+ }
+ if (link.getHref() != null) {
+ w.writeAttribute("href", link.getHref());
+ }
+ if (link.getType() != null) {
+ w.writeAttribute("type", link.getType());
+ }
+ w.writeEndElement();
+ }
+
+ private void writeCategory(XMLStreamWriter w, AtomCategory cat) throws XMLStreamException {
+ w.writeStartElement(ATOM_NS, "category");
+ if (cat.getTerm() != null) {
+ w.writeAttribute("term", cat.getTerm());
+ }
+ if (cat.getScheme() != null) {
+ w.writeAttribute("scheme", cat.getScheme());
+ }
+ if (cat.getLabel() != null) {
+ w.writeAttribute("label", cat.getLabel());
+ }
+ w.writeEndElement();
+ }
+
+ private void writeAtomText(XMLStreamWriter w, String name, String text)
+ throws XMLStreamException {
+ if (text == null) {
+ return;
+ }
+ w.writeStartElement(ATOM_NS, name);
+ w.writeCharacters(text);
+ w.writeEndElement();
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/EntryCollection.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/EntryCollection.java
index e637ffc7d1..d65daaa825 100644
--- a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/EntryCollection.java
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/EntryCollection.java
@@ -1,13 +1,13 @@
/*
* Copyright 2007 Sun Microsystems, Inc. All rights reserved.
* Use is subject to license terms.
- *
+ *
* 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.
@@ -18,21 +18,6 @@
package org.apache.roller.weblogger.webservices.atomprotocol;
-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.AtomNotAuthorizedException;
-import com.rometools.propono.atom.server.AtomNotFoundException;
-import com.rometools.propono.atom.server.AtomRequest;
-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.atom.Person;
-import com.rometools.rome.feed.module.Module;
-import com.rometools.rome.feed.synd.SyndPerson;
-
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
@@ -70,26 +55,26 @@ public class EntryCollection {
private Weblogger roller;
private User user;
private static final int MAX_ENTRIES = 20;
- private final String atomURL;
-
+ private final String atomURL;
+
private static Log log =
LogFactory.getFactory().getInstance(EntryCollection.class);
-
-
+
+
public EntryCollection(User user, String atomURL) {
this.user = user;
this.atomURL = atomURL;
this.roller = WebloggerFactory.getWeblogger();
}
-
-
- public Entry postEntry(AtomRequest areq, Entry entry) throws AtomException {
+
+
+ public AtomEntry postEntry(AtomRequest areq, AtomEntry entry) throws AtomException {
log.debug("Entering");
String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
try {
// authenticated client posted a weblog entry
String handle = pathInfo[0];
- Weblog website =
+ Weblog website =
roller.getWeblogManager().getWeblogByHandle(handle);
if (website == null) {
throw new AtomNotFoundException("Cannot find weblog: " + handle);
@@ -97,9 +82,9 @@ public Entry postEntry(AtomRequest areq, Entry entry) throws AtomException {
if (!RollerAtomHandler.canEdit(user, website)) {
throw new AtomNotAuthorizedException("Not authorized to access website: " + handle);
}
-
+
RollerAtomHandler.oneSecondThrottle();
-
+
// Save it and commit it
WeblogEntryManager mgr = roller.getWeblogEntryManager();
WeblogEntry rollerEntry = new WeblogEntry();
@@ -116,13 +101,10 @@ public Entry postEntry(AtomRequest areq, Entry entry) throws AtomException {
}
rollerEntry = mgr.getWeblogEntry(rollerEntry.getId());
- Entry newEntry = createAtomEntry(rollerEntry);
- for (Object objLink : newEntry.getOtherLinks()) {
- Link link = (Link) objLink;
- if ("edit".equals(link.getRel())) {
- log.debug("Exiting");
- return createAtomEntry(rollerEntry);
- }
+ AtomEntry newEntry = createAtomEntry(rollerEntry);
+ if (newEntry.getLinkHref("edit") != null) {
+ log.debug("Exiting");
+ return newEntry;
}
log.error("ERROR: no edit link found in saved media entry");
log.debug("Exiting via exception");
@@ -132,9 +114,9 @@ public Entry postEntry(AtomRequest areq, Entry entry) throws AtomException {
}
throw new AtomException("Posting entry");
}
-
-
- public Entry getEntry(AtomRequest areq) throws AtomException {
+
+
+ public AtomEntry getEntry(AtomRequest areq) throws AtomException {
try {
String entryid = Utilities.stringToStringArray(areq.getPathInfo(),"/")[2];
WeblogEntry entry = roller.getWeblogEntryManager().getWeblogEntry(entryid);
@@ -150,9 +132,9 @@ public Entry getEntry(AtomRequest areq) throws AtomException {
throw new AtomException("ERROR fetching entry", ex);
}
}
-
-
- public Feed getCollection(AtomRequest areq) throws AtomException {
+
+
+ public AtomFeed getCollection(AtomRequest areq) throws AtomException {
log.debug("Entering");
String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
try {
@@ -165,7 +147,7 @@ public Feed getCollection(AtomRequest areq) throws AtomException {
} catch (Exception e) {
log.warn("Unparsable range: " + pathInfo[2]);
}
- }
+ }
String handle = pathInfo[0];
String absUrl = WebloggerRuntimeConfig.getAbsoluteContextURL();
Weblog website = roller.getWeblogManager().getWeblogByHandle(handle);
@@ -181,37 +163,37 @@ public Feed getCollection(AtomRequest areq) throws AtomException {
wesc.setOffset(start);
wesc.setMaxResults(max + 1);
List entries = roller.getWeblogEntryManager().getWeblogEntries(wesc);
- Feed feed = new Feed();
+ AtomFeed feed = new AtomFeed();
feed.setId(atomURL
+"/"+website.getHandle() + "/entries/" + start);
feed.setTitle(website.getName());
- Link link = new Link();
+ List links = new ArrayList<>();
+ AtomLink link = new AtomLink();
link.setHref(absUrl + "/" + website.getHandle());
link.setRel("alternate");
link.setType("text/html");
- feed.setAlternateLinks(Collections.singletonList(link));
+ links.add(link);
- List atomEntries = new ArrayList<>();
+ List atomEntries = new ArrayList<>();
int count = 0;
for (WeblogEntry rollerEntry : entries) {
if (count++ >= MAX_ENTRIES) {
break;
}
- Entry entry = createAtomEntry(rollerEntry);
+ AtomEntry entry = createAtomEntry(rollerEntry);
atomEntries.add(entry);
if (count == 1) {
// first entry is most recent
feed.setUpdated(entry.getUpdated());
}
}
- List links = new ArrayList<>();
if (entries.size() > max) {
// add next link
int nextOffset = start + max;
String url = atomURL+"/"
+ website.getHandle() + "/entries/" + nextOffset;
- Link nextLink = new Link();
+ AtomLink nextLink = new AtomLink();
nextLink.setRel("next");
nextLink.setHref(url);
links.add(nextLink);
@@ -221,27 +203,25 @@ public Feed getCollection(AtomRequest areq) throws AtomException {
int prevOffset = start > max ? start - max : 0;
String url = atomURL+"/"
+website.getHandle() + "/entries/" + prevOffset;
- Link prevLink = new Link();
+ AtomLink prevLink = new AtomLink();
prevLink.setRel("previous");
prevLink.setHref(url);
links.add(prevLink);
}
- if (!links.isEmpty()) {
- feed.setOtherLinks(links);
- }
+ feed.setLinks(links);
// Use collection URI as id
feed.setEntries(atomEntries);
-
+
log.debug("Exiting");
return feed;
-
+
} catch (WebloggerException re) {
throw new AtomException("Getting entry collection");
}
}
-
-
- public void putEntry(AtomRequest areq, Entry entry) throws AtomException {
+
+
+ public void putEntry(AtomRequest areq, AtomEntry entry) throws AtomException {
log.debug("Entering");
String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
try {
@@ -252,18 +232,18 @@ public void putEntry(AtomRequest areq, Entry entry) throws AtomException {
roller.getWeblogEntryManager().getWeblogEntry(pathInfo[2]);
if (rollerEntry == null) {
throw new AtomNotFoundException(
- "Cannot find specified entry/resource");
+ "Cannot find specified entry/resource");
}
if (RollerAtomHandler.canEdit(user, rollerEntry)) {
-
+
RollerAtomHandler.oneSecondThrottle();
-
+
WeblogEntryManager mgr = roller.getWeblogEntryManager();
copyToRollerEntry(entry, rollerEntry);
rollerEntry.setUpdateTime(new Timestamp(new Date().getTime()));
mgr.saveWeblogEntry(rollerEntry);
roller.flush();
-
+
CacheManager.invalidate(rollerEntry.getWebsite());
if (rollerEntry.isPublished()) {
roller.getIndexManager().addEntryReIndexOperation(rollerEntry);
@@ -274,13 +254,13 @@ public void putEntry(AtomRequest areq, Entry entry) throws AtomException {
throw new AtomNotAuthorizedException("ERROR not authorized to update entry");
}
throw new AtomNotFoundException("Cannot find specified entry/resource");
-
+
} catch (WebloggerException re) {
throw new AtomException("Updating entry");
}
}
-
-
+
+
public void deleteEntry(AtomRequest areq) throws AtomException {
try {
String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/");
@@ -297,98 +277,89 @@ public void deleteEntry(AtomRequest areq) throws AtomException {
roller.flush();
return;
}
- log.debug("Not authorized to delete entry");
- log.debug("Exiting via exception");
-
+ log.debug("Not authorized to delete entry");
+ log.debug("Exiting via exception");
+
} catch (WebloggerException ex) {
throw new AtomException("ERROR deleting entry",ex);
}
throw new AtomNotAuthorizedException("Not authorized to delete entry");
}
-
+
/**
- * Create a Rome Atom entry based on a Weblogger entry.
+ * Create an Atom entry based on a Weblogger entry.
* Content is escaped.
* Link is stored as rel=alternate link.
*/
- private Entry createAtomEntry(WeblogEntry entry) {
- Entry atomEntry = new Entry();
-
+ private AtomEntry createAtomEntry(WeblogEntry entry) {
+ AtomEntry atomEntry = new AtomEntry();
+
atomEntry.setId( entry.getPermalink());
atomEntry.setTitle( entry.getTitle());
atomEntry.setPublished( entry.getPubTime());
atomEntry.setUpdated( entry.getUpdateTime());
-
- Content content = new Content();
- content.setType(Content.HTML);
+
+ AtomContent content = new AtomContent();
+ content.setType("html");
content.setValue(entry.getText());
- List contents = new ArrayList<>();
- contents.add(content);
-
- atomEntry.setContents(contents);
-
+ atomEntry.setContent(content);
+
if (StringUtils.isNotEmpty(entry.getSummary())) {
- Content summary = new Content();
- summary.setType(Content.HTML);
+ AtomContent summary = new AtomContent();
+ summary.setType("html");
summary.setValue(entry.getSummary());
atomEntry.setSummary(summary);
}
-
+
User creator = entry.getCreator();
- SyndPerson author = new Person();
+ AtomPerson author = new AtomPerson();
author.setName( creator.getUserName());
author.setEmail( creator.getEmailAddress());
- atomEntry.setAuthors(Collections.singletonList(author));
-
+ atomEntry.setAuthors(new ArrayList<>(Collections.singletonList(author)));
+
// Add Atom category for Weblogger category, using category scheme
- List categories = new ArrayList<>();
- Category atomCat = new Category();
+ List categories = new ArrayList<>();
+ AtomCategory atomCat = new AtomCategory();
atomCat.setScheme(RollerAtomService.getWeblogCategoryScheme(entry.getWebsite()));
atomCat.setTerm(entry.getCategory().getName());
categories.add(atomCat);
-
+
// Add Atom categories for each Weblogger tag with null scheme
Set tmp = new TreeSet<>(new WeblogEntryTagComparator());
tmp.addAll(entry.getTags());
for (WeblogEntryTag tag : tmp) {
- Category newcat = new Category();
+ AtomCategory newcat = new AtomCategory();
newcat.setTerm(tag.getName());
categories.add(newcat);
- }
+ }
atomEntry.setCategories(categories);
-
- Link altlink = new Link();
+
+ List links = new ArrayList<>();
+ AtomLink altlink = new AtomLink();
altlink.setRel("alternate");
altlink.setHref(entry.getPermalink());
- List altlinks = new ArrayList<>();
- altlinks.add(altlink);
- atomEntry.setAlternateLinks(altlinks);
-
- Link editlink = new Link();
+ links.add(altlink);
+
+ AtomLink editlink = new AtomLink();
editlink.setRel("edit");
editlink.setHref(
atomURL
+"/"+entry.getWebsite().getHandle() + "/entry/" + entry.getId());
- List otherlinks = new ArrayList<>();
- otherlinks.add(editlink);
- atomEntry.setOtherLinks(otherlinks);
-
- List modules = new ArrayList<>();
- AppModule app = new AppModuleImpl();
- app.setDraft(!WeblogEntry.PubStatus.PUBLISHED.equals(entry.getStatus()));
- app.setEdited(entry.getUpdateTime());
- modules.add(app);
- atomEntry.setModules(modules);
-
+ links.add(editlink);
+ atomEntry.setLinks(links);
+
+ atomEntry.setDraft(!WeblogEntry.PubStatus.PUBLISHED.equals(entry.getStatus()));
+ atomEntry.setEdited(entry.getUpdateTime());
+
return atomEntry;
}
-
+
/**
- * Copy fields from ROME entry to Weblogger entry.
+ * Copy fields from Atom entry to Weblogger entry.
*/
- private void copyToRollerEntry(Entry entry, WeblogEntry rollerEntry) throws WebloggerException {
-
+ private void copyToRollerEntry(AtomEntry entry, WeblogEntry rollerEntry) throws WebloggerException {
+
Timestamp current = new Timestamp(System.currentTimeMillis());
Timestamp pubTime = current;
Timestamp updateTime = current;
@@ -399,31 +370,28 @@ private void copyToRollerEntry(Entry entry, WeblogEntry rollerEntry) throws Webl
updateTime = new Timestamp( entry.getUpdated().getTime() );
}
rollerEntry.setTitle(entry.getTitle());
- if (entry.getContents() != null && !entry.getContents().isEmpty()) {
- Content content = entry.getContents().get(0);
- rollerEntry.setText(content.getValue());
+ if (entry.getContent() != null) {
+ rollerEntry.setText(entry.getContent().getValue());
}
if (entry.getSummary() != null) {
rollerEntry.setSummary(entry.getSummary().getValue());
}
rollerEntry.setPubTime(pubTime);
rollerEntry.setUpdateTime(updateTime);
-
- AppModule control =
- (AppModule)entry.getModule(AppModule.URI);
- if (control!=null && control.getDraft()) {
+
+ if (entry.isDraft()) {
rollerEntry.setStatus(PubStatus.DRAFT);
} else {
rollerEntry.setStatus(PubStatus.PUBLISHED);
}
-
+
// Process incoming categories:
// Atom categories with weblog-level scheme are Weblogger categories.
// Atom supports multiple cats, but Weblogger supports one/entry
// so here we take accept the first category that exists.
- List categories = entry.getCategories();
+ List categories = entry.getCategories();
if (categories != null && !categories.isEmpty()) {
- for (Category cat : categories) {
+ for (AtomCategory cat : categories) {
if (cat.getScheme() != null && cat.getScheme().equals(
RollerAtomService.getWeblogCategoryScheme(rollerEntry.getWebsite()))) {
String catString = cat.getTerm();
@@ -444,31 +412,31 @@ private void copyToRollerEntry(Entry entry, WeblogEntry rollerEntry) throws Webl
// Didn't find a category? Fall back to the default Blogger API category.
rollerEntry.setCategory(rollerEntry.getWebsite().getBloggerCategory());
}
-
+
// Now process incoming categories that are tags:
// Atom categories with no scheme are considered tags.
String tags = "";
StringBuilder buff = new StringBuilder();
if (categories != null && !categories.isEmpty()) {
- for (Category cat : categories) {
+ for (AtomCategory cat : categories) {
if (cat.getScheme() == null) {
buff.append(" ").append(cat.getTerm());
- }
+ }
}
tags = buff.toString();
}
- rollerEntry.setTagsAsString(tags);
+ rollerEntry.setTagsAsString(tags);
}
private void reindexEntry(WeblogEntry entry) throws WebloggerException {
IndexManager manager = roller.getIndexManager();
-
+
// TODO: figure out what's up here and at WeblogEntryFormAction line 696
//manager.removeEntryIndexOperation(entry);
-
+
// if published, index the entry
if (entry.isPublished()) {
manager.addEntryReIndexOperation(entry);
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/MediaCollection.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/MediaCollection.java
index e7e963482e..e31190bdc8 100644
--- a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/MediaCollection.java
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/MediaCollection.java
@@ -1,13 +1,13 @@
/*
* Copyright 2007 Sun Microsystems, Inc. All rights reserved.
* Use is subject to license terms.
- *
+ *
* 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.
@@ -17,17 +17,6 @@
package org.apache.roller.weblogger.webservices.atomprotocol;
-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.AtomNotAuthorizedException;
-import com.rometools.propono.atom.server.AtomNotFoundException;
-import com.rometools.propono.atom.server.AtomRequest;
-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 java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -35,7 +24,6 @@
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
@@ -70,20 +58,20 @@ public class MediaCollection {
private Weblogger roller;
private User user;
private static final int MAX_ENTRIES = 20;
- private final String atomURL;
-
+ private final String atomURL;
+
private static Log log =
LogFactory.getFactory().getInstance(EntryCollection.class);
-
-
+
+
public MediaCollection(User user, String atomURL) {
this.user = user;
this.atomURL = atomURL;
this.roller = WebloggerFactory.getWeblogger();
- }
-
-
- public Entry postMedia(AtomRequest areq, Entry entry) throws AtomException {
+ }
+
+
+ public AtomEntry postMedia(AtomRequest areq, AtomEntry entry) throws AtomException {
log.debug("Entering");
String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
@@ -91,11 +79,11 @@ public Entry postMedia(AtomRequest areq, Entry entry) throws AtomException {
// get incoming slug from HTTP header
String slug = areq.getHeader("Slug");
- Content content = entry.getContents().get(0);
+ AtomContent content = entry.getContent();
String contentType = content.getType();
InputStream is = areq.getInputStream();
String title = entry.getTitle() != null ? entry.getTitle() : slug;
-
+
// authenticated client posted a weblog entry
File tempFile = null;
String handle = pathInfo[0];
@@ -106,14 +94,14 @@ public Entry postMedia(AtomRequest areq, Entry entry) throws AtomException {
}
if (pathInfo.length > 1) {
// Save to temp file
- String fileName = createFileName(website,
+ String fileName = createFileName(website,
(slug != null) ? slug : Utilities.replaceNonAlphanumeric(title,' '), contentType);
try {
tempFile = File.createTempFile(fileName, "tmp");
FileOutputStream fos = new FileOutputStream(tempFile);
Utilities.copyInputToOutput(is, fos);
fos.close();
-
+
// Parse pathinfo to determine file path
String path = filePathFromPathInfo(pathInfo);
String justPath = path;
@@ -147,20 +135,17 @@ public Entry postMedia(AtomRequest areq, Entry entry) throws AtomException {
}
roller.flush();
-
+
fis.close();
-
+
MediaFile stored = fileMgr.getMediaFile(mf.getId());
- Entry mediaEntry = createAtomResourceEntry(website, stored);
- for (Object objLink : mediaEntry.getOtherLinks()) {
- Link link = (Link) objLink;
- if ("edit".equals(link.getRel())) {
- log.debug("Exiting");
- return mediaEntry;
- }
+ AtomEntry mediaEntry = createAtomResourceEntry(website, stored);
+ if (mediaEntry.getLinkHref("edit") != null) {
+ log.debug("Exiting");
+ return mediaEntry;
}
log.error("ERROR: no edit link found in saved media entry");
-
+
} catch (FileIOException fie) {
throw new AtomException(
"File upload disabled, over-quota or other error", fie);
@@ -171,16 +156,16 @@ public Entry postMedia(AtomRequest areq, Entry entry) throws AtomException {
}
}
throw new AtomException("Error saving media entry");
-
+
} catch (WebloggerException re) {
throw new AtomException("Posting media", re);
} catch (IOException ioe) {
throw new AtomException("Posting media", ioe);
}
}
-
-
- public Entry getEntry(AtomRequest areq) throws AtomException {
+
+
+ public AtomEntry getEntry(AtomRequest areq) throws AtomException {
try {
String[] pathInfo = Utilities.stringToStringArray(areq.getPathInfo(), "/");
@@ -196,14 +181,14 @@ public Entry getEntry(AtomRequest areq) throws AtomException {
if (mf != null) {
return createAtomResourceEntry(website, mf);
}
-
+
} catch (WebloggerException ex) {
throw new AtomException("ERROR fetching entry",ex);
}
throw new AtomNotFoundException("ERROR resource not found");
}
-
-
+
+
public AtomMediaResource getMediaResource(AtomRequest areq) throws AtomException {
log.debug("Entering");
String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
@@ -216,13 +201,14 @@ public AtomMediaResource getMediaResource(AtomRequest areq) throws AtomException
throw new AtomNotAuthorizedException("Not authorized to edit weblog: " + handle);
}
if (pathInfo.length > 1) {
- try {
+ try {
// Parse pathinfo to determine file path
String filePath = filePathFromPathInfo(pathInfo);
MediaFile mf = fmgr.getMediaFileByOriginalPath(website, filePath);
return new AtomMediaResource(
mf.getName(),
mf.getLength(),
+ Utilities.getContentTypeFromFileName(mf.getName()),
new Date(mf.getLastModified()),
mf.getInputStream());
} catch (Exception e) {
@@ -231,14 +217,14 @@ public AtomMediaResource getMediaResource(AtomRequest areq) throws AtomException
}
}
throw new AtomException("Incorrect path information");
-
+
} catch (WebloggerException re) {
throw new AtomException("Posting media");
}
}
-
-
- public Feed getCollection(AtomRequest areq) throws AtomException {
+
+
+ public AtomFeed getCollection(AtomRequest areq) throws AtomException {
log.debug("Entering");
String[] rawPathInfo = StringUtils.split(areq.getPathInfo(),"/");
try {
@@ -256,7 +242,7 @@ public Feed getCollection(AtomRequest areq) throws AtomException {
if (!path.isEmpty()) {
path = path + File.separator;
}
-
+
String handle = pathInfo[0];
String absUrl = WebloggerRuntimeConfig.getAbsoluteContextURL();
Weblog website = roller.getWeblogManager().getWeblogByHandle(handle);
@@ -267,16 +253,17 @@ public Feed getCollection(AtomRequest areq) throws AtomException {
throw new AtomNotAuthorizedException("Not authorized to access website");
}
- Feed feed = new Feed();
+ AtomFeed feed = new AtomFeed();
feed.setId(atomURL
- +"/"+website.getHandle() + "/resources/" + path + start);
+ +"/"+website.getHandle() + "/resources/" + path + start);
feed.setTitle(website.getName());
- Link link = new Link();
+ List links = new ArrayList<>();
+ AtomLink link = new AtomLink();
link.setHref(absUrl + "/" + website.getHandle());
link.setRel("alternate");
link.setType("text/html");
- feed.setAlternateLinks(Collections.singletonList(link));
+ links.add(link);
MediaFileManager fmgr = roller.getMediaFileManager();
MediaFileDirectory dir;
@@ -305,7 +292,7 @@ else if (f1.getLastModified() == f2.getLastModified()) {
}
}
});
-
+
if (files != null && start < files.size()) {
for (MediaFile mf : files) {
sortedSet.add(mf);
@@ -313,9 +300,9 @@ else if (f1.getLastModified() == f2.getLastModified()) {
int count = 0;
MediaFile[] sortedResources =
sortedSet.toArray(MediaFile[]::new);
- List atomEntries = new ArrayList<>();
+ List atomEntries = new ArrayList<>();
for (int i=start; i<(start + max) && i<(sortedResources.length); i++) {
- Entry entry = createAtomResourceEntry(website, sortedResources[i]);
+ AtomEntry entry = createAtomResourceEntry(website, sortedResources[i]);
atomEntries.add(entry);
if (count == 0) {
// first entry is most recent
@@ -324,28 +311,26 @@ else if (f1.getLastModified() == f2.getLastModified()) {
count++;
}
- List otherLinks = new ArrayList<>();
if (start + count < files.size()) {
// add next link
int nextOffset = start + max;
String url = atomURL
+"/"+ website.getHandle() + "/resources/" + path + nextOffset;
- Link nextLink = new Link();
+ AtomLink nextLink = new AtomLink();
nextLink.setRel("next");
nextLink.setHref(url);
- otherLinks.add(nextLink);
+ links.add(nextLink);
}
if (start > 0) {
// add previous link
int prevOffset = start > max ? start - max : 0;
String url = atomURL
+"/"+website.getHandle() + "/resources/" + path + prevOffset;
- Link prevLink = new Link();
+ AtomLink prevLink = new AtomLink();
prevLink.setRel("previous");
prevLink.setHref(url);
- otherLinks.add(prevLink);
+ links.add(prevLink);
}
- feed.setOtherLinks(otherLinks);
feed.setEntries(atomEntries);
log.debug("Collection contains: " + count);
@@ -353,23 +338,24 @@ else if (f1.getLastModified() == f2.getLastModified()) {
} else {
log.debug("Returning empty collection");
}
-
+
+ feed.setLinks(links);
log.debug("Exiting");
return feed;
-
+
} catch (WebloggerException re) {
throw new AtomException("Getting resource collection", re);
}
}
-
-
+
+
public void putMedia(AtomRequest areq) throws AtomException {
String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
String contentType = areq.getContentType();
try {
InputStream is = areq.getInputStream();
-
+
// authenticated client posted a weblog entry
File tempFile = null;
String handle = pathInfo[0];
@@ -386,12 +372,12 @@ public void putMedia(AtomRequest areq) throws AtomException {
FileOutputStream fos = new FileOutputStream(tempFile);
Utilities.copyInputToOutput(is, fos);
fos.close();
-
+
FileInputStream fis = new FileInputStream(tempFile);
// Parse pathinfo to determine file path
String path = filePathFromPathInfo(pathInfo);
-
+
// Attempt to load file, to ensure it exists
MediaFile mf = fmgr.getMediaFileByPath(website, path);
mf.setContentType(contentType);
@@ -403,7 +389,7 @@ public void putMedia(AtomRequest areq) throws AtomException {
roller.flush();
fis.close();
-
+
log.debug("Exiting");
return;
@@ -420,15 +406,13 @@ public void putMedia(AtomRequest areq) throws AtomException {
}
}
throw new AtomException("Incorrect path information");
-
+
} catch (WebloggerException re) {
throw new AtomException("Posting media");
- } catch (IOException ioe) {
- throw new AtomException("Posting media", ioe);
}
}
-
-
+
+
public void deleteEntry(AtomRequest areq) throws AtomException {
try {
String[] pathInfo = StringUtils.split(areq.getPathInfo(), "/");
@@ -446,23 +430,23 @@ public void deleteEntry(AtomRequest areq) throws AtomException {
fmgr.removeMediaFile(website, mf);
log.debug("Deleted media entry: " + fileName);
return;
-
+
} catch (Exception e) {
String msg = "ERROR deleting media entry";
log.error(msg, e);
throw new AtomException(msg);
}
}
- log.debug("Not authorized to delete media entry");
- log.debug("Exiting via exception");
+ log.debug("Not authorized to delete media entry");
+ log.debug("Exiting via exception");
} catch (WebloggerException ex) {
throw new AtomException("ERROR deleting media entry",ex);
}
throw new AtomNotAuthorizedException("Not authorized to delete entry");
}
-
-
+
+
private String filePathFromPathInfo(String[] pathInfo) {
String path = null;
if (pathInfo.length > 2) {
@@ -479,76 +463,69 @@ private String filePathFromPathInfo(String[] pathInfo) {
}
return path;
}
-
- private Entry createAtomResourceEntry(Weblog website, MediaFile file) {
+
+ private AtomEntry createAtomResourceEntry(Weblog website, MediaFile file) {
String filePath = file.getPath().endsWith("/")
? file.getPath() + file.getName()
: file.getPath() + "/" + file.getName();
- String editURI =
+ String editURI =
atomURL+"/"+website.getHandle()
+ "/resource/" + filePath + ".media-link";
- String editMediaURI =
+ String editMediaURI =
atomURL+"/"+ website.getHandle()
+ "/resource/" + filePath;
String contentType = Utilities.getContentTypeFromFileName(file.getName());
-
- Entry entry = new Entry();
+
+ AtomEntry entry = new AtomEntry();
entry.setId(editMediaURI);
entry.setTitle(file.getName());
entry.setUpdated(new Date(file.getLastModified()));
-
- Link altlink = new Link();
+
+ List links = new ArrayList<>();
+ AtomLink altlink = new AtomLink();
altlink.setRel("alternate");
altlink.setHref(file.getPermalink());
- List altlinks = new ArrayList<>();
- altlinks.add(altlink);
- entry.setAlternateLinks(altlinks);
-
- List otherlinks = new ArrayList<>();
- entry.setOtherLinks(otherlinks);
- Link editlink = new Link();
- editlink.setRel("edit");
- editlink.setHref(editURI);
- otherlinks.add(editlink);
- Link editMedialink = new Link();
- editMedialink.setRel("edit-media");
- editMedialink.setHref(editMediaURI);
- otherlinks.add(editMedialink);
-
- Content content = new Content();
+ links.add(altlink);
+
+ AtomLink editlink = new AtomLink();
+ editlink.setRel("edit");
+ editlink.setHref(editURI);
+ links.add(editlink);
+
+ AtomLink editMedialink = new AtomLink();
+ editMedialink.setRel("edit-media");
+ editMedialink.setHref(editMediaURI);
+ links.add(editMedialink);
+ entry.setLinks(links);
+
+ AtomContent content = new AtomContent();
content.setSrc(file.getPermalink());
content.setType(contentType);
- List contents = new ArrayList<>();
- contents.add(content);
- entry.setContents(contents);
-
- List modules = new ArrayList<>();
- AppModule app = new AppModuleImpl();
- app.setDraft(false);
- app.setEdited(entry.getUpdated());
- modules.add(app);
- entry.setModules(modules);
-
+ entry.setContent(content);
+
+ entry.setDraft(false);
+ entry.setEdited(entry.getUpdated());
+
return entry;
}
-
-
+
+
/**
- * Creates a file name for a file based on a weblog, title string and a
- * content-type.
- *
+ * Creates a file name for a file based on a weblog, title string and a
+ * content-type.
+ *
* @param weblog Weblog for which file name is being created
* @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
+ *
+ * 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.
+ * 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.
+ * comes after he slash.
*
* For example:
* weblog.handle = "daveblog"
@@ -563,23 +540,23 @@ private Entry createAtomResourceEntry(Weblog website, MediaFile file) {
* Might result in daveblog-200608201034.jpg
*/
private String createFileName(Weblog weblog, String title, String contentType) {
-
+
if (weblog == null) {
throw new IllegalArgumentException("weblog cannot be null");
}
if (contentType == null) {
throw new IllegalArgumentException("contentType cannot be null");
}
-
+
String fileName;
-
+
// 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
+ // The info we need to map from contentType to file extension is in
+ // JRE/lib/content-type.properties, but Java Activation doesn't provide
// a way to do a reverse mapping or to get at the data.
String[] typeTokens = contentType.split("/");
String ext = typeTokens[1];
-
+
if (title != null && !title.isBlank()) {
// We've got a title, so use it to build file name
StringTokenizer toker = new StringTokenizer(title);
@@ -595,15 +572,15 @@ private String createFileName(Weblog weblog, String title, String contentType) {
fileName = tmp + "." + ext;
} else {
fileName = tmp;
- }
- } else {
+ }
+ } else {
// No title or text, so instead we'll use the item's date
// in YYYYMMDD format to form the file name
SimpleDateFormat sdf = new SimpleDateFormat();
sdf.applyPattern("yyyyMMddHHSS");
fileName = weblog.getHandle()+"-"+sdf.format(new Date())+"."+ext;
}
-
+
return fileName;
}
}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomHandler.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomHandler.java
index 55e1c576ef..ff8537f6bf 100644
--- a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomHandler.java
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomHandler.java
@@ -16,43 +16,35 @@
* directory of this distribution.
*/
package org.apache.roller.weblogger.webservices.atomprotocol;
-import com.rometools.propono.atom.common.Categories;
-import com.rometools.propono.atom.server.AtomRequest;
+
import java.util.StringTokenizer;
import javax.servlet.http.HttpServletRequest;
-import org.apache.commons.codec.binary.Base64;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.apache.roller.util.RollerConstants;
-import org.apache.roller.weblogger.business.Weblogger;
-import org.apache.roller.weblogger.business.WebloggerFactory;
-import org.apache.roller.weblogger.pojos.User;
-import org.apache.roller.weblogger.pojos.WeblogEntry;
-import org.apache.roller.weblogger.pojos.Weblog;
-import org.apache.roller.weblogger.util.WSSEUtilities;
-import com.rometools.propono.atom.common.AtomService;
-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.AtomNotFoundException;
-import com.rometools.rome.feed.atom.Entry;
-import com.rometools.rome.feed.atom.Feed;
-import java.nio.charset.StandardCharsets;
import javax.servlet.http.HttpServletResponse;
import net.oauth.OAuthAccessor;
import net.oauth.OAuthMessage;
import net.oauth.server.OAuthServlet;
+import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.roller.util.RollerConstants;
import org.apache.roller.weblogger.WebloggerException;
import org.apache.roller.weblogger.business.OAuthManager;
+import org.apache.roller.weblogger.business.Weblogger;
+import org.apache.roller.weblogger.business.WebloggerFactory;
import org.apache.roller.weblogger.config.WebloggerConfig;
import org.apache.roller.weblogger.config.WebloggerRuntimeConfig;
+import org.apache.roller.weblogger.pojos.User;
+import org.apache.roller.weblogger.pojos.Weblog;
+import org.apache.roller.weblogger.pojos.WeblogEntry;
import org.apache.roller.weblogger.pojos.WeblogPermission;
import org.apache.roller.weblogger.ui.core.RollerContext;
/**
- * Weblogger's ROME Propono-based Atom Protocol implementation.
+ * Weblogger's Atom Publishing Protocol implementation. This implementation uses
+ * only the JDK XML (StAX) APIs for serialization and parsing — it does not
+ * depend on ROME or Propono.
*
* Each Weblogger workspace has two collections, one that accepts entries and
* that accepts everything. The entries collection represents the weblog
@@ -91,7 +83,7 @@
*
* @author David M Johnson
*/
-public class RollerAtomHandler implements AtomHandler {
+public class RollerAtomHandler {
protected Weblogger roller = null;
protected User user = null;
protected int maxEntries = 20;
@@ -120,10 +112,6 @@ public RollerAtomHandler(HttpServletRequest request, HttpServletResponse respons
String userName;
if ("oauth".equals(WebloggerRuntimeConfig.getProperty("webservices.atomPubAuth"))) {
userName = authenticationOAUTH(request, response);
-
- } else if ("wsse".equals(WebloggerRuntimeConfig.getProperty("webservices.atomPubAuth"))) {
- userName = authenticateWSSE(request);
-
} else {
// default to basic
userName = authenticateBASIC(request);
@@ -143,7 +131,6 @@ public RollerAtomHandler(HttpServletRequest request, HttpServletResponse respons
/**
* Return weblogHandle of authenticated user or null if there is none.
*/
- @Override
public String getAuthenticatedUsername() {
String ret = null;
if (this.user != null) {
@@ -158,10 +145,9 @@ public String getAuthenticatedUsername() {
* Return Atom service document for site, getting blog-name from pathInfo.
* The workspace will contain collections for entries, categories and resources.
*/
- @Override
- public AtomService getAtomService(AtomRequest areq) throws AtomException {
+ public AtomServiceDoc getAtomService(AtomRequest areq) throws AtomException {
try {
- return new RollerAtomService(user, atomURL);
+ return new RollerAtomService(user, atomURL).getServiceDoc();
} catch (WebloggerException ex) {
log.error("Unable to create Service Document", ex);
throw new AtomException("ERROR creating Service Document", ex);
@@ -173,8 +159,7 @@ public AtomService getAtomService(AtomRequest areq) throws AtomException {
/**
* Create entry in the entry collection (a Weblogger blog has only one).
*/
- @Override
- public Entry postEntry(AtomRequest areq, Entry entry) throws AtomException {
+ public AtomEntry postEntry(AtomRequest areq, AtomEntry entry) throws AtomException {
EntryCollection ecol = new EntryCollection(user, atomURL);
return ecol.postEntry(areq, entry);
}
@@ -182,12 +167,8 @@ public Entry postEntry(AtomRequest areq, Entry entry) throws AtomException {
/**
* Create new resource in generic collection (a Weblogger blog has only one).
- * TODO: can we avoid saving temporary file?
- * TODO: do we need to handle mutli-part MIME uploads?
- * TODO: use Jakarta Commons File-upload?
*/
- @Override
- public Entry postMedia(AtomRequest areq, Entry entry)
+ public AtomEntry postMedia(AtomRequest areq, AtomEntry entry)
throws AtomException {
MediaCollection mcol = new MediaCollection(user, atomURL);
return mcol.postMedia(areq, entry);
@@ -206,8 +187,7 @@ public Entry postMedia(AtomRequest areq, Entry entry)
* //resources/offset
*
*/
- @Override
- public Feed getCollection(AtomRequest areq) throws AtomException {
+ public AtomFeed getCollection(AtomRequest areq) throws AtomException {
String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
if (pathInfo.length > 0 && pathInfo[1].equals("entries")) {
@@ -222,17 +202,10 @@ public Feed getCollection(AtomRequest areq) throws AtomException {
}
- @Override
- public Categories getCategories(AtomRequest arg0) throws AtomException {
- throw new UnsupportedOperationException("Not supported yet.");
- }
-
-
/**
* Retrieve entry, URI like this /blog-name/entry/id
*/
- @Override
- public Entry getEntry(AtomRequest areq) throws AtomException {
+ public AtomEntry getEntry(AtomRequest areq) throws AtomException {
log.debug("Entering");
String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
// URI is /blogname/entries/entryid
@@ -251,7 +224,6 @@ public Entry getEntry(AtomRequest areq) throws AtomException {
/**
* Expects pathInfo of form /blog-name/resource/path/name
*/
- @Override
public AtomMediaResource getMediaResource(AtomRequest areq) throws AtomException {
MediaCollection mcol = new MediaCollection(user, atomURL);
return mcol.getMediaResource(areq);
@@ -263,8 +235,7 @@ public AtomMediaResource getMediaResource(AtomRequest areq) throws AtomException
/**
* Update entry, URI like this /blog-name/entry/id
*/
- @Override
- public void putEntry(AtomRequest areq, Entry entry) throws AtomException {
+ public void putEntry(AtomRequest areq, AtomEntry entry) throws AtomException {
EntryCollection ecol = new EntryCollection(user, atomURL);
ecol.putEntry(areq, entry);
}
@@ -274,7 +245,6 @@ public void putEntry(AtomRequest areq, Entry entry) throws AtomException {
* Update resource specified by pathInfo using data from input stream.
* Expects pathInfo of form /blog-name/resource/path/name
*/
- @Override
public void putMedia(AtomRequest areq) throws AtomException {
MediaCollection mcol = new MediaCollection(user, atomURL);
mcol.putMedia(areq);
@@ -286,7 +256,6 @@ public void putMedia(AtomRequest areq) throws AtomException {
/**
* Delete entry, URI like this /blog-name/entry/id
*/
- @Override
public void deleteEntry(AtomRequest areq) throws AtomException {
log.debug("Entering");
String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
@@ -311,7 +280,6 @@ public void deleteEntry(AtomRequest areq) throws AtomException {
/**
* True if URL is the introspection URI.
*/
- @Override
public boolean isAtomServiceURI(AtomRequest areq) {
String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
return pathInfo.length == 0;
@@ -320,7 +288,6 @@ public boolean isAtomServiceURI(AtomRequest areq) {
/**
* True if URL is a entry URI.
*/
- @Override
public boolean isEntryURI(AtomRequest areq) {
String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
if (pathInfo.length > 2 && pathInfo[1].equals("entry")) {
@@ -335,7 +302,6 @@ public boolean isEntryURI(AtomRequest areq) {
/**
* True if URL is media edit URI. Media can be updated, but not metadata.
*/
- @Override
public boolean isMediaEditURI(AtomRequest areq) {
String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
if (pathInfo.length > 1 && pathInfo[1].equals("resource")) {
@@ -347,7 +313,6 @@ public boolean isMediaEditURI(AtomRequest areq) {
/**
* True if URL is a collection URI of any sort.
*/
- @Override
public boolean isCollectionURI(AtomRequest areq) {
String[] pathInfo = StringUtils.split(areq.getPathInfo(),"/");
if (pathInfo.length > 1 && pathInfo[1].equals("entries")) {
@@ -362,11 +327,6 @@ public boolean isCollectionURI(AtomRequest areq) {
return false;
}
- @Override
- public boolean isCategoriesURI(AtomRequest arg0) {
- return false;
- }
-
//------------------------------------------------------------------ permissions
@@ -410,53 +370,6 @@ public static boolean canView(User u, Weblog website) {
//-------------------------------------------------------------- authentication
- /**
- * Perform WSSE authentication based on information in request.
- * Will not work if Weblogger password encryption is turned on.
- */
- protected String authenticateWSSE(HttpServletRequest request) {
- String wsseHeader = request.getHeader("X-WSSE");
- String ret = null;
- if (wsseHeader == null) {
- return ret;
- }
- String userName = null;
- String created = null;
- String nonce = null;
- String passwordDigest = null;
- String[] tokens = wsseHeader.split(",");
- for (int i = 0; i < tokens.length; i++) {
- int index = tokens[i].indexOf('=');
- if (index != -1) {
- String key = tokens[i].substring(0, index).trim();
- String value = tokens[i].substring(index + 1).trim();
- value = value.replace("\"", "");
- if (key.startsWith("UsernameToken")) {
- userName = value;
- } else if (key.equalsIgnoreCase("nonce")) {
- nonce = value;
- } else if (key.equalsIgnoreCase("passworddigest")) {
- passwordDigest = value;
- } else if (key.equalsIgnoreCase("created")) {
- created = value;
- }
- }
- }
- String digest = null;
- try {
- User inUser = roller.getUserManager().getUserByUserName(userName);
- digest = WSSEUtilities.generateDigest(WSSEUtilities.base64Decode(nonce),
- created.getBytes(StandardCharsets.UTF_8),
- inUser.getPassword().getBytes(StandardCharsets.UTF_8));
- if (digest.equals(passwordDigest)) {
- ret = userName;
- }
- } catch (Exception e) {
- log.error("During wsseAuthenticataion: " + e.getMessage(), e);
- }
- return ret;
- }
-
/**
* BASIC authentication.
*/
@@ -478,7 +391,7 @@ public String authenticateBASIC(HttpServletRequest request) {
User inUser = roller.getUserManager().getUserByUserName(userID);
if (inUser.getEnabled()) {
String password = userPass.substring(p+1);
- valid = RollerContext.getPasswordEncoder().matches(password, user.getPassword());
+ valid = RollerContext.getPasswordEncoder().matches(password, inUser.getPassword());
}
}
}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomService.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomService.java
index 60342c63c1..74094efbab 100644
--- a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomService.java
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomService.java
@@ -36,30 +36,24 @@
import org.apache.roller.weblogger.pojos.WeblogPermission;
import org.apache.roller.weblogger.util.Utilities;
-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.atom.server.AtomException;
-import com.rometools.rome.feed.atom.Category;
-
/**
- * Roller's Atom service.
+ * Builds Roller's APP service document. The document is assembled into an
+ * {@link AtomServiceDoc} wire-model object during construction and is available
+ * via {@link #getServiceDoc()}.
*/
-public class RollerAtomService extends AtomService {
+public class RollerAtomService {
+
+ private final AtomServiceDoc serviceDoc = new AtomServiceDoc();
- /**
- * Creates a new instance of FileBasedAtomService.
- */
public RollerAtomService(User user, String atomURL) throws WebloggerException, AtomException {
Weblogger roller = WebloggerFactory.getWeblogger();
List perms;
-
+
if (!WebloggerRuntimeConfig.getBooleanProperty("webservices.enableAtomPub")) {
throw new AtomException("AtomPub not enabled for this Roller installation");
}
-
+
try {
perms = roller.getUserManager().getWeblogPermissions(user);
} catch (WebloggerException re) {
@@ -74,38 +68,41 @@ public RollerAtomService(User user, String atomURL) throws WebloggerException, A
if (perms != null) {
for (WeblogPermission perm : perms) {
Weblog weblog = perm.getWeblog();
- Workspace workspace;
+ AtomWorkspace workspace;
try {
// Create workspace to represent weblog
- workspace = new Workspace(Utilities.removeHTML(perm.getWeblog().getName()), "text");
- addWorkspace(workspace);
+ workspace = new AtomWorkspace();
+ workspace.setTitle(Utilities.removeHTML(perm.getWeblog().getName()));
+ serviceDoc.getWorkspaces().add(workspace);
// Create collection for entries within that workspace
- Collection entryCol = new Collection("Weblog Entries", "text", atomURL + "/" + weblog.getHandle() + "/entries");
- entryCol.addAccept("application/atom+xml;type=entry");
+ AtomCollection entryCol = new AtomCollection();
+ entryCol.setTitle("Weblog Entries");
+ entryCol.setHref(atomURL + "/" + weblog.getHandle() + "/entries");
+ entryCol.getAccepts().add("application/atom+xml;type=entry");
// Add fixed categories using scheme that points to
// weblog because categories are weblog specific
weblog = perm.getWeblog();
- Categories cats = new Categories();
+ AtomCategories cats = new AtomCategories();
cats.setFixed(true);
cats.setScheme(getWeblogCategoryScheme(weblog));
List rollerCats = roller.getWeblogEntryManager().getWeblogCategories(weblog);
for (WeblogCategory rollerCat : rollerCats) {
- Category cat = new Category();
+ AtomCategory cat = new AtomCategory();
cat.setTerm(rollerCat.getName());
cat.setLabel(rollerCat.getName());
- cats.addCategory(cat);
+ cats.getCategories().add(cat);
}
- entryCol.addCategories(cats);
+ entryCol.getCategories().add(cats);
// Indicte that free form categories are allowed
- Categories tags = new Categories();
+ AtomCategories tags = new AtomCategories();
tags.setFixed(false);
- entryCol.addCategories(tags);
+ entryCol.getCategories().add(tags);
- workspace.addCollection(entryCol);
+ workspace.getCollections().add(entryCol);
} catch (Exception e) {
throw new AtomException("Creating weblog entry collection for service doc", e);
}
@@ -115,11 +112,12 @@ public RollerAtomService(User user, String atomURL) throws WebloggerException, A
MediaFileManager mgr = roller.getMediaFileManager();
List dirs = mgr.getMediaFileDirectories(weblog);
for (MediaFileDirectory dir : dirs) {
- Collection uploadSubCol = new Collection(
- "Media Files: " + dir.getName(), "text",
+ AtomCollection uploadSubCol = new AtomCollection();
+ uploadSubCol.setTitle("Media Files: " + dir.getName());
+ uploadSubCol.setHref(
atomURL + "/" + weblog.getHandle() + "/resources/" + dir.getName());
uploadSubCol.setAccepts(uploadAccepts);
- workspace.addCollection(uploadSubCol);
+ workspace.getCollections().add(uploadSubCol);
}
} catch (Exception e) {
@@ -128,15 +126,22 @@ public RollerAtomService(User user, String atomURL) throws WebloggerException, A
}
}
}
-
+
+ /**
+ * The assembled service document.
+ */
+ public AtomServiceDoc getServiceDoc() {
+ return serviceDoc;
+ }
+
/**
- * Build accept range by taking things that appear to be content-type rules
+ * Build accept range by taking things that appear to be content-type rules
* from site's file-upload allowed extensions.
*/
private List getAcceptedContentTypeRange() throws WebloggerException {
List accepts = new ArrayList<>();
Weblogger roller = WebloggerFactory.getWeblogger();
- Map config = roller.getPropertiesManager().getProperties();
+ Map config = roller.getPropertiesManager().getProperties();
String allows = config.get("uploads.types.allowed").getValue();
String[] rules = StringUtils.split(StringUtils.deleteWhitespace(allows), ",");
if (rules != null) {
@@ -147,11 +152,10 @@ private List getAcceptedContentTypeRange() throws WebloggerException {
accepts.add(rule);
}
}
- return accepts;
- }
-
+ return accepts;
+ }
+
public static String getWeblogCategoryScheme(Weblog website) {
return WebloggerFactory.getWeblogger().getUrlStrategy().getWeblogURL(website, null, true);
}
}
-
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomServlet.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomServlet.java
new file mode 100644
index 0000000000..062e538563
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomServlet.java
@@ -0,0 +1,216 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Dispatcher servlet for Roller's Atom Publishing Protocol (RFC 5023)
+ * implementation. Replaces the ROME Propono {@code AtomServlet}: it
+ * authenticates the request, routes by HTTP method and URI shape to
+ * {@link RollerAtomHandler}, and serializes/parses Atom XML via {@link AtomWriter}
+ * and {@link AtomReader}. No ROME or Propono types are involved.
+ */
+public class RollerAtomServlet extends HttpServlet {
+
+ private static final Log log =
+ LogFactory.getFactory().getInstance(RollerAtomServlet.class);
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ process(request, response, "GET");
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ process(request, response, "POST");
+ }
+
+ @Override
+ protected void doPut(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ process(request, response, "PUT");
+ }
+
+ @Override
+ protected void doDelete(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ process(request, response, "DELETE");
+ }
+
+ private void process(HttpServletRequest request, HttpServletResponse response, String method)
+ throws IOException {
+
+ RollerAtomHandler handler = new RollerAtomHandler(request, response);
+ String userName = handler.getAuthenticatedUsername();
+ if (userName == null) {
+ // The OAuth path may have already written a challenge/error response.
+ if (!response.isCommitted()) {
+ response.setHeader("WWW-Authenticate", "Basic realm=\"Roller\"");
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication error");
+ }
+ return;
+ }
+
+ byte[] body = null;
+ if ("POST".equals(method) || "PUT".equals(method)) {
+ body = readBody(request);
+ }
+ AtomRequest areq = new AtomRequest(request, body);
+
+ try {
+ switch (method) {
+ case "GET":
+ doGet(handler, areq, response);
+ break;
+ case "POST":
+ doPost(handler, areq, response);
+ break;
+ case "PUT":
+ doPut(handler, areq, response);
+ break;
+ case "DELETE":
+ handler.deleteEntry(areq);
+ response.setStatus(HttpServletResponse.SC_OK);
+ break;
+ default:
+ response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+ }
+ } catch (AtomException ae) {
+ log.debug("Returning error to client: " + ae.getMessage(), ae);
+ if (!response.isCommitted()) {
+ response.sendError(ae.getStatus(), ae.getMessage());
+ }
+ } catch (Exception e) {
+ log.error("Unexpected error handling AtomPub request", e);
+ if (!response.isCommitted()) {
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+ }
+ }
+ }
+
+ private void doGet(RollerAtomHandler handler, AtomRequest areq, HttpServletResponse response)
+ throws AtomException, IOException {
+
+ if (handler.isAtomServiceURI(areq)) {
+ AtomServiceDoc service = handler.getAtomService(areq);
+ response.setContentType(AtomConstants.SERVICE_MEDIA_TYPE);
+ new AtomWriter().writeServiceDoc(response.getOutputStream(), service);
+
+ } else if (handler.isCollectionURI(areq)) {
+ AtomFeed feed = handler.getCollection(areq);
+ response.setContentType(AtomConstants.FEED_MEDIA_TYPE);
+ new AtomWriter().writeFeed(response.getOutputStream(), feed);
+
+ } else if (handler.isEntryURI(areq)) {
+ AtomEntry entry = handler.getEntry(areq);
+ response.setContentType(AtomConstants.ENTRY_MEDIA_TYPE);
+ new AtomWriter().writeEntry(response.getOutputStream(), entry);
+
+ } else if (handler.isMediaEditURI(areq)) {
+ AtomMediaResource resource = handler.getMediaResource(areq);
+ if (resource.getContentType() != null) {
+ response.setContentType(resource.getContentType());
+ }
+ response.setContentLengthLong(resource.getContentLength());
+ if (resource.getLastModified() != null) {
+ response.setDateHeader("Last-Modified", resource.getLastModified().getTime());
+ }
+ try (InputStream in = resource.getInputStream()) {
+ in.transferTo(response.getOutputStream());
+ }
+
+ } else {
+ throw new AtomNotFoundException("Cannot find specified resource");
+ }
+ }
+
+ private void doPost(RollerAtomHandler handler, AtomRequest areq, HttpServletResponse response)
+ throws AtomException {
+
+ if (!handler.isCollectionURI(areq)) {
+ throw new AtomNotFoundException("Cannot POST to specified URI");
+ }
+
+ String contentType = areq.getContentType();
+ AtomEntry created;
+ if (contentType != null && contentType.startsWith("application/atom+xml")) {
+ AtomEntry entry = new AtomReader().parseEntry(areq.getInputStream());
+ created = handler.postEntry(areq, entry);
+ } else {
+ // Media POST: synthesize an entry carrying the request content type
+ // and Slug; the binary data is read from the request body.
+ AtomEntry mediaEntry = new AtomEntry();
+ AtomContent content = new AtomContent();
+ content.setType(contentType);
+ mediaEntry.setContent(content);
+ mediaEntry.setTitle(areq.getHeader("Slug"));
+ created = handler.postMedia(areq, mediaEntry);
+ }
+ writeCreated(response, created);
+ }
+
+ private void doPut(RollerAtomHandler handler, AtomRequest areq, HttpServletResponse response)
+ throws AtomException {
+
+ if (handler.isEntryURI(areq)) {
+ AtomEntry entry = new AtomReader().parseEntry(areq.getInputStream());
+ handler.putEntry(areq, entry);
+ response.setStatus(HttpServletResponse.SC_OK);
+ } else if (handler.isMediaEditURI(areq)) {
+ handler.putMedia(areq);
+ response.setStatus(HttpServletResponse.SC_OK);
+ } else {
+ throw new AtomNotFoundException("Cannot PUT to specified URI");
+ }
+ }
+
+ private void writeCreated(HttpServletResponse response, AtomEntry entry)
+ throws AtomException {
+ String editHref = entry.getLinkHref("edit");
+ if (editHref != null) {
+ response.setHeader("Location", editHref);
+ response.setHeader("Content-Location", editHref);
+ }
+ response.setStatus(HttpServletResponse.SC_CREATED);
+ response.setContentType(AtomConstants.ENTRY_MEDIA_TYPE);
+ try {
+ OutputStream out = response.getOutputStream();
+ new AtomWriter().writeEntry(out, entry);
+ } catch (IOException ioe) {
+ throw new AtomException("Error writing created entry", ioe);
+ }
+ }
+
+ private byte[] readBody(HttpServletRequest request) throws IOException {
+ try (InputStream in = request.getInputStream()) {
+ return in.readAllBytes();
+ }
+ }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/package-info.java b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/package-info.java
index 412ae3a322..b76e13aace 100644
--- a/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/package-info.java
+++ b/app/src/main/java/org/apache/roller/weblogger/webservices/atomprotocol/package-info.java
@@ -17,7 +17,8 @@
*/
/**
-
Atom Publising Protocol (AtomPub) implementation using ROME Propono.
+
Atom Publishing Protocol (AtomPub) implementation built on the JDK StAX XML
+ APIs. It does not depend on ROME or Propono.
End-point is at [context]/roller-services/app
*/
package org.apache.roller.weblogger.webservices.atomprotocol;
\ No newline at end of file
diff --git a/app/src/main/resources/ApplicationResources.properties b/app/src/main/resources/ApplicationResources.properties
index 66072c23f0..e8438ae0d7 100644
--- a/app/src/main/resources/ApplicationResources.properties
+++ b/app/src/main/resources/ApplicationResources.properties
@@ -338,7 +338,7 @@ configForm.editorPages=Editor Pages
configForm.webServicesSettings=Web Services Settings
configForm.enableAtomPub=Enable Atom Publishing Protocol
-configForm.AtomPubAuth=AtomPub authentication (basic, oauth, or wsse)
+configForm.AtomPubAuth=AtomPub authentication (basic or oauth)
configForm.enableXmlRpc=Enable Blogger / MetaWeblog API
configForm.weblogSettings=Weblog Rendering Settings
diff --git a/app/src/main/resources/ApplicationResources_ja.properties b/app/src/main/resources/ApplicationResources_ja.properties
index 8802be030d..34af433211 100644
--- a/app/src/main/resources/ApplicationResources_ja.properties
+++ b/app/src/main/resources/ApplicationResources_ja.properties
@@ -1383,7 +1383,7 @@ pingTargetAdd.subtitle=Ping\u30BF\u30FC\u30B2\u30C3\u30C8\u306E\u8FFD\u52A0
planetSubscription.feedUrl=\u30CB\u30E5\u30FC\u30B9\u30D5\u30A3\u30FC\u30C9URL
mediaFileSuccess.noEnclosure=\u30A8\u30F3\u30AF\u30ED\u30FC\u30B8\u30E3\u306A\u3057
userRegister.tip.ready=\u3088\u308D\u3057\u3051\u308C\u3070\u3001\u4EE5\u4E0B\u306E\u30DC\u30BF\u30F3\u3092\u30AF\u30EA\u30C3\u30AF\u3057\u3066\u304F\u3060\u3055\u3044\u3002
-configForm.AtomPubAuth=AtomPub\u8A8D\u8A3C (basic, oauth, \u307E\u305F\u306F wsse)
+configForm.AtomPubAuth=AtomPub\u8A8D\u8A3C (basic \u307E\u305F\u306F oauth)
ConfigForm.proxyPort=Feed fetcher\u304C\u4F7F\u7528\u3059\u308B\u30D7\u30ED\u30AD\u30B7\u306E\u30DD\u30FC\u30C8
mediaFileView.searchTitle=\u691C\u7D22\u7D50\u679C
weblogEdit.enclosureLength=\u9577\u3055
diff --git a/app/src/main/resources/ApplicationResources_zh_CN.properties b/app/src/main/resources/ApplicationResources_zh_CN.properties
index 93ac844968..053a850ae9 100644
--- a/app/src/main/resources/ApplicationResources_zh_CN.properties
+++ b/app/src/main/resources/ApplicationResources_zh_CN.properties
@@ -337,7 +337,7 @@ configForm.editorPages=\u7F16\u8F91\u5668\u9875\u9762
configForm.webServicesSettings=Web\u670D\u52A1\u8BBE\u7F6E
configForm.enableAtomPub=\u542F\u7528 Atom \u53D1\u5E03\u534F\u8BAE
-configForm.AtomPubAuth=AtomPub \u8BA4\u8BC1\u65B9\u5F0F (basic / oauth / wsse)
+configForm.AtomPubAuth=AtomPub \u8BA4\u8BC1\u65B9\u5F0F (basic / oauth)
configForm.enableXmlRpc=\u542F\u7528 Blogger / MetaWeblog API
configForm.weblogSettings=\u535A\u5BA2\u663E\u793A\u8BBE\u7F6E
diff --git a/app/src/main/resources/propono.properties b/app/src/main/resources/propono.properties
deleted file mode 100644
index bcd565e64b..0000000000
--- a/app/src/main/resources/propono.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-com.rometools.propono.atom.server.AtomHandlerFactory=\
-org.apache.roller.weblogger.webservices.atomprotocol.RollerAtomHandlerFactory
diff --git a/app/src/main/webapp/WEB-INF/web.xml b/app/src/main/webapp/WEB-INF/web.xml
index 0418832da1..14e4bc37a9 100644
--- a/app/src/main/webapp/WEB-INF/web.xml
+++ b/app/src/main/webapp/WEB-INF/web.xml
@@ -267,7 +267,7 @@
AtomServlet
- com.rometools.propono.atom.server.AtomServlet
+ org.apache.roller.weblogger.webservices.atomprotocol.RollerAtomServlet
diff --git a/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomEntryTest.java b/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomEntryTest.java
new file mode 100644
index 0000000000..8956a3ec17
--- /dev/null
+++ b/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomEntryTest.java
@@ -0,0 +1,55 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link AtomEntry}, in particular the link lookup helper the
+ * dispatcher servlet relies on to find the edit URI.
+ */
+public class AtomEntryTest {
+
+ @Test
+ public void testGetLinkHrefFindsMatchingRel() {
+ AtomEntry entry = new AtomEntry();
+ entry.getLinks().add(new AtomLink("alternate", "http://example.com/blog/1"));
+ entry.getLinks().add(new AtomLink("edit", "http://example.com/app/blog/entry/1"));
+
+ assertEquals("http://example.com/blog/1", entry.getLinkHref("alternate"));
+ assertEquals("http://example.com/app/blog/entry/1", entry.getLinkHref("edit"));
+ }
+
+ @Test
+ public void testGetLinkHrefReturnsNullWhenMissing() {
+ AtomEntry entry = new AtomEntry();
+ entry.getLinks().add(new AtomLink("alternate", "http://example.com/blog/1"));
+ assertNull(entry.getLinkHref("edit"));
+ }
+
+ @Test
+ public void testGetLinkHrefReturnsFirstMatch() {
+ AtomEntry entry = new AtomEntry();
+ entry.getLinks().add(new AtomLink("edit", "first"));
+ entry.getLinks().add(new AtomLink("edit", "second"));
+ assertEquals("first", entry.getLinkHref("edit"));
+ }
+}
diff --git a/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomExceptionTest.java b/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomExceptionTest.java
new file mode 100644
index 0000000000..8219cb3a60
--- /dev/null
+++ b/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomExceptionTest.java
@@ -0,0 +1,59 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for the AtomPub exception hierarchy and the HTTP status codes the
+ * dispatcher servlet maps from them.
+ */
+public class AtomExceptionTest {
+
+ @Test
+ public void testBaseExceptionDefaultsToServerError() {
+ AtomException ex = new AtomException("boom");
+ assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getStatus());
+ assertEquals("boom", ex.getMessage());
+ }
+
+ @Test
+ public void testBaseExceptionPreservesCause() {
+ Throwable cause = new IllegalStateException("root");
+ AtomException ex = new AtomException("boom", cause);
+ assertSame(cause, ex.getCause());
+ assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getStatus());
+ }
+
+ @Test
+ public void testNotFoundIs404() {
+ assertEquals(HttpServletResponse.SC_NOT_FOUND,
+ new AtomNotFoundException("missing").getStatus());
+ }
+
+ @Test
+ public void testNotAuthorizedIs401() {
+ assertEquals(HttpServletResponse.SC_UNAUTHORIZED,
+ new AtomNotAuthorizedException("nope").getStatus());
+ }
+}
diff --git a/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomReaderTest.java b/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomReaderTest.java
new file mode 100644
index 0000000000..45c2138c29
--- /dev/null
+++ b/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomReaderTest.java
@@ -0,0 +1,136 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Date;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for the StAX-based {@link AtomReader}.
+ */
+public class AtomReaderTest {
+
+ private AtomEntry parse(String xml) throws AtomException {
+ return new AtomReader().parseEntry(
+ new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
+ }
+
+ @Test
+ public void testParseFullEntry() throws Exception {
+ String xml =
+ ""
+ + ""
+ + " urn:entry:1"
+ + " Hello World"
+ + " 2026-06-03T12:34:56Z"
+ + " 2026-06-04T01:02:03Z"
+ + " A summary"
+ + " <b>Body</b> & more"
+ + " "
+ + " "
+ + " yes"
+ + "";
+
+ AtomEntry entry = parse(xml);
+
+ assertEquals("urn:entry:1", entry.getId());
+ assertEquals("Hello World", entry.getTitle());
+ assertNotNull(entry.getContent());
+ assertEquals("html", entry.getContent().getType());
+ assertEquals("Body & more", entry.getContent().getValue());
+ assertNotNull(entry.getSummary());
+ assertEquals("A summary", entry.getSummary().getValue());
+ assertEquals(Date.from(Instant.parse("2026-06-03T12:34:56Z")), entry.getPublished());
+ assertEquals(Date.from(Instant.parse("2026-06-04T01:02:03Z")), entry.getUpdated());
+ assertTrue(entry.isDraft());
+
+ assertEquals(2, entry.getCategories().size());
+ AtomCategory cat = entry.getCategories().get(0);
+ assertEquals("tech", cat.getTerm());
+ assertEquals("http://example.com/cats", cat.getScheme());
+ AtomCategory tag = entry.getCategories().get(1);
+ assertEquals("java", tag.getTerm());
+ assertNull(tag.getScheme());
+ }
+
+ @Test
+ public void testDraftDefaultsToFalseWhenNoControl() throws Exception {
+ String xml =
+ ""
+ + "No control";
+ assertFalse(parse(xml).isDraft());
+ }
+
+ @Test
+ public void testDraftNoIsNotDraft() throws Exception {
+ String xml =
+ ""
+ + "no";
+ assertFalse(parse(xml).isDraft());
+ }
+
+ @Test
+ public void testContentWithSrcHasNoValue() throws Exception {
+ String xml =
+ ""
+ + "";
+ AtomEntry entry = parse(xml);
+ assertEquals("image/png", entry.getContent().getType());
+ assertEquals("http://example.com/a.png", entry.getContent().getSrc());
+ assertNull(entry.getContent().getValue());
+ }
+
+ @Test
+ public void testParseDateAcceptsZuluAndOffset() {
+ assertEquals(Date.from(Instant.parse("2026-06-03T12:34:56Z")),
+ AtomReader.parseDate("2026-06-03T12:34:56Z"));
+ // 14:34:56+02:00 is the same instant as 12:34:56Z
+ assertEquals(Date.from(Instant.parse("2026-06-03T12:34:56Z")),
+ AtomReader.parseDate("2026-06-03T14:34:56+02:00"));
+ }
+
+ @Test
+ public void testParseDateReturnsNullForGarbage() {
+ assertNull(AtomReader.parseDate(null));
+ assertNull(AtomReader.parseDate(" "));
+ assertNull(AtomReader.parseDate("not-a-date"));
+ }
+
+ @Test
+ public void testExternalEntityIsRejected() {
+ // A DOCTYPE with an external entity must not be processed (XXE guard).
+ String xml =
+ ""
+ + " ]>"
+ + "&xxe;";
+ assertThrows(AtomException.class, () -> parse(xml));
+ }
+}
diff --git a/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomRequestTest.java b/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomRequestTest.java
new file mode 100644
index 0000000000..75181fba67
--- /dev/null
+++ b/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomRequestTest.java
@@ -0,0 +1,97 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link AtomRequest}, the lightweight request wrapper that
+ * buffers the body and normalizes a null pathInfo to "".
+ */
+public class AtomRequestTest {
+
+ @Mock
+ private HttpServletRequest request;
+
+ @BeforeEach
+ public void setUp() {
+ MockitoAnnotations.openMocks(this);
+ }
+
+ @Test
+ public void testNullPathInfoBecomesEmptyString() {
+ when(request.getPathInfo()).thenReturn(null);
+ assertEquals("", new AtomRequest(request, null).getPathInfo());
+ }
+
+ @Test
+ public void testPathInfoPassesThrough() {
+ when(request.getPathInfo()).thenReturn("/blog/entries");
+ assertEquals("/blog/entries", new AtomRequest(request, null).getPathInfo());
+ }
+
+ @Test
+ public void testHeaderAndContentTypeDelegate() {
+ when(request.getHeader("Slug")).thenReturn("my-slug");
+ when(request.getContentType()).thenReturn("application/atom+xml");
+ AtomRequest areq = new AtomRequest(request, null);
+ assertEquals("my-slug", areq.getHeader("Slug"));
+ assertEquals("application/atom+xml", areq.getContentType());
+ }
+
+ @Test
+ public void testInputStreamReturnsBufferedBody() throws Exception {
+ byte[] body = "hello body".getBytes(StandardCharsets.UTF_8);
+ AtomRequest areq = new AtomRequest(request, body);
+ try (InputStream in = areq.getInputStream()) {
+ assertArrayEquals(body, in.readAllBytes());
+ }
+ }
+
+ @Test
+ public void testInputStreamIsFreshEachCall() throws Exception {
+ byte[] body = "abc".getBytes(StandardCharsets.UTF_8);
+ AtomRequest areq = new AtomRequest(request, body);
+ try (InputStream first = areq.getInputStream();
+ InputStream second = areq.getInputStream()) {
+ assertArrayEquals(body, first.readAllBytes());
+ // second stream is independent and still readable from the start
+ assertArrayEquals(body, second.readAllBytes());
+ }
+ }
+
+ @Test
+ public void testNullBodyYieldsEmptyStream() throws Exception {
+ AtomRequest areq = new AtomRequest(request, null);
+ try (InputStream in = areq.getInputStream()) {
+ assertArrayEquals(new byte[0], in.readAllBytes());
+ }
+ }
+}
diff --git a/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomSchemaValidationTest.java b/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomSchemaValidationTest.java
new file mode 100644
index 0000000000..f5d6fdc265
--- /dev/null
+++ b/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomSchemaValidationTest.java
@@ -0,0 +1,198 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXParseException;
+
+import com.thaiopensource.util.PropertyMapBuilder;
+import com.thaiopensource.validate.ValidateProperty;
+import com.thaiopensource.validate.ValidationDriver;
+import com.thaiopensource.validate.rng.CompactSchemaReader;
+
+/**
+ * Validates the XML produced by {@link AtomWriter} against the official RELAX NG
+ * schemas from RFC 4287 (Atom) and RFC 5023 (AtomPub), using Jing. This checks
+ * that Roller's AtomPub wire format actually conforms to the specifications,
+ * not just that it is well-formed.
+ */
+public class AtomSchemaValidationTest {
+
+ private static final Date PUBLISHED = Date.from(Instant.parse("2026-06-03T12:34:56Z"));
+ private static final Date UPDATED = Date.from(Instant.parse("2026-06-04T01:02:03Z"));
+
+ /** Validate xml against a classpath RELAX NG Compact schema; return errors (empty == valid). */
+ private List validate(String schemaResource, byte[] xml) throws Exception {
+ List errors = new ArrayList<>();
+ ErrorHandler handler = new ErrorHandler() {
+ @Override public void warning(SAXParseException e) { /* ignore warnings */ }
+ @Override public void error(SAXParseException e) { errors.add(e.getMessage()); }
+ @Override public void fatalError(SAXParseException e) { errors.add(e.getMessage()); }
+ };
+ PropertyMapBuilder props = new PropertyMapBuilder();
+ props.put(ValidateProperty.ERROR_HANDLER, handler);
+ ValidationDriver driver =
+ new ValidationDriver(props.toPropertyMap(), CompactSchemaReader.getInstance());
+
+ try (InputStream schema = getClass().getResourceAsStream(schemaResource)) {
+ assertTrue(driver.loadSchema(new InputSource(schema)),
+ "schema " + schemaResource + " failed to compile: " + errors);
+ }
+ driver.validate(new InputSource(new ByteArrayInputStream(xml)));
+ return errors;
+ }
+
+ private byte[] writeEntry(AtomEntry entry) throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ new AtomWriter().writeEntry(out, entry);
+ return out.toByteArray();
+ }
+
+ private AtomEntry textEntry() {
+ AtomEntry entry = new AtomEntry();
+ entry.setId("urn:entry:1");
+ entry.setTitle("Hello World");
+ entry.setPublished(PUBLISHED);
+ entry.setUpdated(UPDATED);
+ entry.setEdited(UPDATED);
+ entry.setDraft(false);
+
+ AtomContent content = new AtomContent();
+ content.setType("html");
+ content.setValue("Body & more");
+ entry.setContent(content);
+
+ AtomContent summary = new AtomContent();
+ summary.setType("html");
+ summary.setValue("A summary");
+ entry.setSummary(summary);
+
+ AtomPerson author = new AtomPerson();
+ author.setName("alice");
+ author.setEmail("alice@example.com");
+ entry.getAuthors().add(author);
+
+ AtomCategory cat = new AtomCategory();
+ cat.setTerm("tech");
+ cat.setScheme("http://example.com/cats");
+ entry.getCategories().add(cat);
+ AtomCategory tag = new AtomCategory();
+ tag.setTerm("java");
+ entry.getCategories().add(tag);
+
+ entry.getLinks().add(new AtomLink("alternate", "http://example.com/blog/1"));
+ entry.getLinks().add(new AtomLink("edit", "http://example.com/app/blog/entry/1"));
+ return entry;
+ }
+
+ private AtomEntry mediaEntry() {
+ AtomEntry entry = new AtomEntry();
+ entry.setId("http://example.com/app/blog/resource/snapshot.png");
+ entry.setTitle("snapshot.png");
+ entry.setUpdated(UPDATED);
+ entry.setEdited(UPDATED);
+ entry.setDraft(false);
+
+ AtomContent content = new AtomContent();
+ content.setType("image/png");
+ content.setSrc("http://example.com/blog/mediaresource/snapshot.png");
+ entry.setContent(content);
+
+ entry.getLinks().add(new AtomLink("alternate", "http://example.com/blog/snapshot.png"));
+ entry.getLinks().add(new AtomLink("edit", "http://example.com/app/blog/resource/snapshot.png.media-link"));
+ entry.getLinks().add(new AtomLink("edit-media", "http://example.com/app/blog/resource/snapshot.png"));
+ return entry;
+ }
+
+ @Test
+ public void testTextEntryConformsToAtomSchema() throws Exception {
+ List errors = validate("/atompub/atom.rnc", writeEntry(textEntry()));
+ assertTrue(errors.isEmpty(), "Atom entry should be schema-valid but: " + errors);
+ }
+
+ @Test
+ public void testMediaEntryConformsToAtomSchema() throws Exception {
+ List errors = validate("/atompub/atom.rnc", writeEntry(mediaEntry()));
+ assertTrue(errors.isEmpty(), "media link entry should be schema-valid but: " + errors);
+ }
+
+ @Test
+ public void testFeedConformsToAtomSchema() throws Exception {
+ AtomFeed feed = new AtomFeed();
+ feed.setId("urn:feed:1");
+ feed.setTitle("My Blog");
+ feed.setUpdated(UPDATED);
+ feed.getLinks().add(new AtomLink("alternate", "http://example.com/blog"));
+ feed.getEntries().add(textEntry());
+ feed.getEntries().add(textEntry());
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ new AtomWriter().writeFeed(out, feed);
+
+ List errors = validate("/atompub/atom.rnc", out.toByteArray());
+ assertTrue(errors.isEmpty(), "Atom feed should be schema-valid but: " + errors);
+ }
+
+ @Test
+ public void testServiceDocConformsToAppSchema() throws Exception {
+ AtomServiceDoc service = new AtomServiceDoc();
+ AtomWorkspace workspace = new AtomWorkspace();
+ workspace.setTitle("My Weblog");
+ service.getWorkspaces().add(workspace);
+
+ AtomCollection entries = new AtomCollection();
+ entries.setTitle("Weblog Entries");
+ entries.setHref("http://example.com/app/blog/entries");
+ entries.setAccepts(Arrays.asList("application/atom+xml;type=entry"));
+ AtomCategories fixed = new AtomCategories();
+ fixed.setFixed(true);
+ fixed.setScheme("http://example.com/cats");
+ AtomCategory cat = new AtomCategory();
+ cat.setTerm("tech");
+ cat.setLabel("tech");
+ fixed.getCategories().add(cat);
+ entries.getCategories().add(fixed);
+ entries.getCategories().add(new AtomCategories()); // free-form
+ workspace.getCollections().add(entries);
+
+ AtomCollection media = new AtomCollection();
+ media.setTitle("Media Files: default");
+ media.setHref("http://example.com/app/blog/resources/default");
+ media.setAccepts(Arrays.asList("image/png", "image/jpeg"));
+ workspace.getCollections().add(media);
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ new AtomWriter().writeServiceDoc(out, service);
+
+ List errors = validate("/atompub/app-service.rnc", out.toByteArray());
+ assertTrue(errors.isEmpty(), "service document should be schema-valid but: " + errors);
+ }
+}
diff --git a/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomWriterTest.java b/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomWriterTest.java
new file mode 100644
index 0000000000..25caf2d149
--- /dev/null
+++ b/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/AtomWriterTest.java
@@ -0,0 +1,255 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import static org.apache.roller.weblogger.webservices.atomprotocol.AtomConstants.APP_NS;
+import static org.apache.roller.weblogger.webservices.atomprotocol.AtomConstants.ATOM_NS;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Date;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+/**
+ * Unit tests for the StAX-based {@link AtomWriter}. The emitted XML is parsed
+ * back with a namespace-aware DOM parser so the structure and namespaces can be
+ * asserted directly.
+ */
+public class AtomWriterTest {
+
+ private static final Date PUBLISHED = Date.from(Instant.parse("2026-06-03T12:34:56Z"));
+ private static final Date UPDATED = Date.from(Instant.parse("2026-06-04T01:02:03Z"));
+
+ private Document parse(byte[] xml) throws Exception {
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+ dbf.setNamespaceAware(true);
+ return dbf.newDocumentBuilder().parse(new ByteArrayInputStream(xml));
+ }
+
+ private String text(Document doc, String ns, String local) {
+ NodeList nl = doc.getElementsByTagNameNS(ns, local);
+ return nl.getLength() == 0 ? null : nl.item(0).getTextContent();
+ }
+
+ private Element element(Document doc, String ns, String local) {
+ NodeList nl = doc.getElementsByTagNameNS(ns, local);
+ return nl.getLength() == 0 ? null : (Element) nl.item(0);
+ }
+
+ private AtomEntry sampleEntry() {
+ AtomEntry entry = new AtomEntry();
+ entry.setId("urn:entry:1");
+ entry.setTitle("Hello World");
+ entry.setPublished(PUBLISHED);
+ entry.setUpdated(UPDATED);
+ entry.setEdited(UPDATED);
+ entry.setDraft(false);
+
+ AtomContent content = new AtomContent();
+ content.setType("html");
+ content.setValue("Body & more");
+ entry.setContent(content);
+
+ AtomContent summary = new AtomContent();
+ summary.setType("html");
+ summary.setValue("A summary");
+ entry.setSummary(summary);
+
+ AtomPerson author = new AtomPerson();
+ author.setName("alice");
+ author.setEmail("alice@example.com");
+ entry.getAuthors().add(author);
+
+ AtomCategory cat = new AtomCategory();
+ cat.setTerm("tech");
+ cat.setScheme("http://example.com/cats");
+ entry.getCategories().add(cat);
+ AtomCategory tag = new AtomCategory();
+ tag.setTerm("java");
+ entry.getCategories().add(tag);
+
+ entry.getLinks().add(new AtomLink("alternate", "http://example.com/blog/1"));
+ entry.getLinks().add(new AtomLink("edit", "http://example.com/app/blog/entry/1"));
+ return entry;
+ }
+
+ @Test
+ public void testWriteEntryStructure() throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ new AtomWriter().writeEntry(out, sampleEntry());
+ Document doc = parse(out.toByteArray());
+
+ assertEquals("entry", doc.getDocumentElement().getLocalName());
+ assertEquals(ATOM_NS, doc.getDocumentElement().getNamespaceURI());
+
+ assertEquals("urn:entry:1", text(doc, ATOM_NS, "id"));
+ assertEquals("Hello World", text(doc, ATOM_NS, "title"));
+ assertEquals(AtomWriter.formatDate(PUBLISHED), text(doc, ATOM_NS, "published"));
+ assertEquals(AtomWriter.formatDate(UPDATED), text(doc, ATOM_NS, "updated"));
+
+ Element content = element(doc, ATOM_NS, "content");
+ assertEquals("html", content.getAttribute("type"));
+ // markup is escaped on the wire but DOM gives us back the original text
+ assertEquals("Body & more", content.getTextContent());
+
+ assertEquals("A summary", text(doc, ATOM_NS, "summary"));
+ assertEquals("alice", text(doc, ATOM_NS, "name"));
+
+ // draft "no" plus app:edited present
+ assertEquals("no", text(doc, APP_NS, "draft"));
+ assertEquals(AtomWriter.formatDate(UPDATED), text(doc, APP_NS, "edited"));
+ }
+
+ @Test
+ public void testWriteEntryDraftYes() throws Exception {
+ AtomEntry entry = sampleEntry();
+ entry.setDraft(true);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ new AtomWriter().writeEntry(out, entry);
+ assertEquals("yes", text(parse(out.toByteArray()), APP_NS, "draft"));
+ }
+
+ @Test
+ public void testWriteEntryCategoriesSchemeAndTag() throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ new AtomWriter().writeEntry(out, sampleEntry());
+ Document doc = parse(out.toByteArray());
+
+ NodeList cats = doc.getElementsByTagNameNS(ATOM_NS, "category");
+ assertEquals(2, cats.getLength());
+ Element tech = (Element) cats.item(0);
+ assertEquals("tech", tech.getAttribute("term"));
+ assertEquals("http://example.com/cats", tech.getAttribute("scheme"));
+ Element java = (Element) cats.item(1);
+ assertEquals("java", java.getAttribute("term"));
+ // a tag carries no scheme attribute at all
+ assertFalse(java.hasAttribute("scheme"));
+ }
+
+ @Test
+ public void testWriteEntryEditLink() throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ new AtomWriter().writeEntry(out, sampleEntry());
+ Document doc = parse(out.toByteArray());
+
+ NodeList links = doc.getElementsByTagNameNS(ATOM_NS, "link");
+ assertEquals(2, links.getLength());
+ boolean foundEdit = false;
+ for (int i = 0; i < links.getLength(); i++) {
+ Element link = (Element) links.item(i);
+ if ("edit".equals(link.getAttribute("rel"))) {
+ assertEquals("http://example.com/app/blog/entry/1", link.getAttribute("href"));
+ foundEdit = true;
+ }
+ }
+ assertTrue(foundEdit);
+ }
+
+ @Test
+ public void testWriteFeed() throws Exception {
+ AtomFeed feed = new AtomFeed();
+ feed.setId("urn:feed:1");
+ feed.setTitle("My Blog");
+ feed.setUpdated(UPDATED);
+ feed.getLinks().add(new AtomLink("alternate", "http://example.com/blog"));
+ feed.getEntries().add(sampleEntry());
+ feed.getEntries().add(sampleEntry());
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ new AtomWriter().writeFeed(out, feed);
+ Document doc = parse(out.toByteArray());
+
+ assertEquals("feed", doc.getDocumentElement().getLocalName());
+ assertEquals(ATOM_NS, doc.getDocumentElement().getNamespaceURI());
+ assertEquals("My Blog", text(doc, ATOM_NS, "title"));
+ assertEquals(2, doc.getElementsByTagNameNS(ATOM_NS, "entry").getLength());
+ }
+
+ @Test
+ public void testWriteServiceDoc() throws Exception {
+ AtomServiceDoc service = new AtomServiceDoc();
+ AtomWorkspace workspace = new AtomWorkspace();
+ workspace.setTitle("My Weblog");
+ service.getWorkspaces().add(workspace);
+
+ AtomCollection collection = new AtomCollection();
+ collection.setTitle("Weblog Entries");
+ collection.setHref("http://example.com/app/blog/entries");
+ collection.setAccepts(Arrays.asList("application/atom+xml;type=entry"));
+ AtomCategories cats = new AtomCategories();
+ cats.setFixed(true);
+ cats.setScheme("http://example.com/cats");
+ AtomCategory cat = new AtomCategory();
+ cat.setTerm("tech");
+ cat.setLabel("tech");
+ cats.getCategories().add(cat);
+ collection.getCategories().add(cats);
+ workspace.getCollections().add(collection);
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ new AtomWriter().writeServiceDoc(out, service);
+ Document doc = parse(out.toByteArray());
+
+ assertEquals("service", doc.getDocumentElement().getLocalName());
+ assertEquals(APP_NS, doc.getDocumentElement().getNamespaceURI());
+ assertNotNull(element(doc, APP_NS, "workspace"));
+
+ Element coll = element(doc, APP_NS, "collection");
+ assertEquals("http://example.com/app/blog/entries", coll.getAttribute("href"));
+ // titles are atom:title (Atom namespace) even inside the service doc
+ assertEquals("My Weblog", text(doc, ATOM_NS, "title"));
+ assertEquals("application/atom+xml;type=entry", text(doc, APP_NS, "accept"));
+
+ Element categories = element(doc, APP_NS, "categories");
+ assertEquals("yes", categories.getAttribute("fixed"));
+ assertEquals("http://example.com/cats", categories.getAttribute("scheme"));
+ assertEquals("tech", element(doc, ATOM_NS, "category").getAttribute("term"));
+ }
+
+ @Test
+ public void testEntryRoundTripThroughReader() throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ new AtomWriter().writeEntry(out, sampleEntry());
+
+ AtomEntry parsed = new AtomReader().parseEntry(new ByteArrayInputStream(out.toByteArray()));
+
+ assertEquals("urn:entry:1", parsed.getId());
+ assertEquals("Hello World", parsed.getTitle());
+ assertEquals("Body & more", parsed.getContent().getValue());
+ assertEquals("A summary", parsed.getSummary().getValue());
+ assertEquals(PUBLISHED, parsed.getPublished());
+ assertEquals(UPDATED, parsed.getUpdated());
+ assertFalse(parsed.isDraft());
+ assertEquals(2, parsed.getCategories().size());
+ assertEquals("http://example.com/cats", parsed.getCategories().get(0).getScheme());
+ assertNull(parsed.getCategories().get(1).getScheme());
+ }
+}
diff --git a/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomProtocolTest.java b/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomProtocolTest.java
new file mode 100644
index 0000000000..ae36339fdd
--- /dev/null
+++ b/app/src/test/java/org/apache/roller/weblogger/webservices/atomprotocol/RollerAtomProtocolTest.java
@@ -0,0 +1,244 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. The ASF licenses this file to You
+* under the Apache License, Version 2.0 (the "License"); you may not
+* use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License. For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.webservices.atomprotocol;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.nio.charset.StandardCharsets;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.roller.weblogger.TestUtils;
+import org.apache.roller.weblogger.business.MediaFileManager;
+import org.apache.roller.weblogger.business.PropertiesManager;
+import org.apache.roller.weblogger.business.WeblogEntryManager;
+import org.apache.roller.weblogger.business.WebloggerFactory;
+import org.apache.roller.weblogger.pojos.MediaFile;
+import org.apache.roller.weblogger.pojos.MediaFileDirectory;
+import org.apache.roller.weblogger.pojos.RuntimeConfigProperty;
+import org.apache.roller.weblogger.pojos.User;
+import org.apache.roller.weblogger.pojos.Weblog;
+import org.apache.roller.weblogger.pojos.WeblogEntry;
+import org.apache.roller.weblogger.pojos.WeblogEntry.PubStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Integration tests that exercise the AtomPub server handlers against a real
+ * (in-memory Derby) Roller, driving the full create / retrieve / update / delete
+ * lifecycle through {@link EntryCollection}, {@link MediaCollection} and
+ * {@link RollerAtomService}. The HTTP transport and BASIC authentication are not
+ * exercised here (they require the Spring web context); those are covered by an
+ * over-the-wire exerciser such as APE run against a deployed instance.
+ */
+public class RollerAtomProtocolTest {
+
+ private static final String HANDLE = "atomtestblog";
+ private static final String ATOM_URL = "http://localhost/roller/roller-services/app";
+
+ private User testUser;
+ private Weblog testWeblog;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ TestUtils.setupWeblogger();
+ testUser = TestUtils.setupUser("atomtestuser");
+ testWeblog = TestUtils.setupWeblog(HANDLE, testUser);
+
+ // RollerAtomService requires AtomPub to be enabled; media tests need uploads enabled
+ PropertiesManager pmgr = WebloggerFactory.getWeblogger().getPropertiesManager();
+ RuntimeConfigProperty enableAtomPub = pmgr.getProperty("webservices.enableAtomPub");
+ enableAtomPub.setValue("true");
+ pmgr.saveProperty(enableAtomPub);
+ RuntimeConfigProperty enableUploads = pmgr.getProperty("uploads.enabled");
+ enableUploads.setValue("true");
+ pmgr.saveProperty(enableUploads);
+
+ TestUtils.endSession(true);
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception {
+ TestUtils.teardownWeblog(testWeblog.getId());
+ TestUtils.teardownUser(testUser.getUserName());
+ TestUtils.endSession(true);
+ }
+
+ private User managedUser() throws Exception {
+ return TestUtils.getManagedUser(testUser);
+ }
+
+ /** Build an AtomRequest backed by a mocked HttpServletRequest. */
+ private AtomRequest request(String pathInfo, String contentType, String slug, byte[] body) {
+ HttpServletRequest req = mock(HttpServletRequest.class);
+ lenient().when(req.getPathInfo()).thenReturn(pathInfo);
+ lenient().when(req.getContentType()).thenReturn(contentType);
+ lenient().when(req.getHeader("Slug")).thenReturn(slug);
+ return new AtomRequest(req, body);
+ }
+
+ private AtomEntry sampleEntry(String title, String body, boolean draft) {
+ AtomEntry entry = new AtomEntry();
+ entry.setTitle(title);
+ AtomContent content = new AtomContent();
+ content.setType("html");
+ content.setValue(body);
+ entry.setContent(content);
+ entry.setDraft(draft);
+ // a free-form tag (no scheme); the weblog category will default
+ AtomCategory tag = new AtomCategory();
+ tag.setTerm("testing");
+ entry.getCategories().add(tag);
+ return entry;
+ }
+
+ private String entryIdFromEditLink(AtomEntry entry) {
+ String edit = entry.getLinkHref("edit");
+ assertNotNull(edit, "created entry must have an edit link");
+ return edit.substring(edit.lastIndexOf("/entry/") + "/entry/".length());
+ }
+
+ @Test
+ public void testEntryLifecycle() throws Exception {
+ // ---- create ----
+ EntryCollection ecol = new EntryCollection(managedUser(), ATOM_URL);
+ AtomEntry created = ecol.postEntry(
+ request("/" + HANDLE + "/entries", "application/atom+xml", null, null),
+ sampleEntry("First Post", "