Merge pull request #422 from apache/WW-5080-plain-result
[WW-5080] Defines a new result type plain to use directly with Java code
diff --git a/core/src/main/java/org/apache/struts2/result/PlainResult.java b/core/src/main/java/org/apache/struts2/result/PlainResult.java
new file mode 100644
index 0000000..b398b93
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/result/PlainResult.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. 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.
+ */
+package org.apache.struts2.result;
+
+import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.Result;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.message.ParameterizedMessage;
+import org.apache.struts2.StrutsException;
+import org.apache.struts2.result.plain.HttpHeader;
+import org.apache.struts2.result.plain.ResponseBuilder;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * This result can only be used in code, as a result of action's method, eg.:
+ * <p>
+ * public PlainResult execute() {
+ * return response -> response.write("");
+ * }
+ * <p>
+ * Please notice the result type of the method is a PlainResult not a String.
+ */
+public interface PlainResult extends Result {
+
+ Logger LOG = LogManager.getLogger(PlainResult.class);
+
+ @Override
+ default void execute(ActionInvocation invocation) throws Exception {
+ LOG.debug("Executing plain result");
+ ResponseBuilder builder = new ResponseBuilder();
+ write(builder);
+
+ HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
+
+ if (response.isCommitted()) {
+ if (ignoreCommitted()) {
+ LOG.warn("Http response already committed, ignoring & skipping!");
+ return;
+ } else {
+ throw new StrutsException("Http response already committed, cannot modify it!");
+ }
+ }
+
+ for (HttpHeader<String> header : builder.getStringHeaders()) {
+ LOG.debug(new ParameterizedMessage("A string header: {} = {}", header.getName(), header.getValue()));
+ response.addHeader(header.getName(), header.getValue());
+ }
+ for (HttpHeader<Long> header : builder.getDateHeaders()) {
+ LOG.debug(new ParameterizedMessage("A date header: {} = {}", header.getName(), header.getValue()));
+ response.addDateHeader(header.getName(), header.getValue());
+ }
+ for (HttpHeader<Integer> header : builder.getIntHeaders()) {
+ LOG.debug(new ParameterizedMessage("An int header: {} = {}", header.getName(), header.getValue()));
+ response.addIntHeader(header.getName(), header.getValue());
+ }
+
+ for (Cookie cookie : builder.getCookies()) {
+ LOG.debug(new ParameterizedMessage("A cookie: {} = {}", cookie.getName(), cookie.getValue()));
+ response.addCookie(cookie);
+ }
+
+ response.getWriter().write(builder.getBody());
+ response.flushBuffer();
+ }
+
+ /**
+ * Implement this method in action using lambdas
+ *
+ * @param response a response builder used to build a Http response
+ */
+ void write(ResponseBuilder response);
+
+ /**
+ * Controls if result should ignore already committed Http response
+ * If set to true only a warning will be issued and the rest of the result
+ * will be skipped
+ *
+ * @return boolean false by default which means an exception will be thrown
+ */
+ default boolean ignoreCommitted() {
+ return false;
+ }
+
+}
+
diff --git a/core/src/main/java/org/apache/struts2/result/plain/BodyWriter.java b/core/src/main/java/org/apache/struts2/result/plain/BodyWriter.java
new file mode 100644
index 0000000..ff22769
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/result/plain/BodyWriter.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. 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.
+ */
+package org.apache.struts2.result.plain;
+
+import java.io.StringWriter;
+
+class BodyWriter {
+
+ private final StringWriter body = new StringWriter();
+
+ public BodyWriter write(String out) {
+ body.write(out);
+ return this;
+ }
+
+ public BodyWriter writeLine(String out) {
+ body.write(out);
+ body.write("\n");
+ return this;
+ }
+
+ public String getBody() {
+ return body.toString();
+ }
+}
diff --git a/core/src/main/java/org/apache/struts2/result/plain/DateHttpHeader.java b/core/src/main/java/org/apache/struts2/result/plain/DateHttpHeader.java
new file mode 100644
index 0000000..906860b
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/result/plain/DateHttpHeader.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. 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.
+ */
+package org.apache.struts2.result.plain;
+
+class DateHttpHeader implements HttpHeader<Long> {
+
+ private final String name;
+ private final Long value;
+
+ public DateHttpHeader(String name, Long value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Long getValue() {
+ return value;
+ }
+}
diff --git a/core/src/main/java/org/apache/struts2/result/plain/HttpCookies.java b/core/src/main/java/org/apache/struts2/result/plain/HttpCookies.java
new file mode 100644
index 0000000..22c105b
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/result/plain/HttpCookies.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. 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.
+ */
+package org.apache.struts2.result.plain;
+
+import javax.servlet.http.Cookie;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+class HttpCookies {
+
+ private final List<Cookie> cookies = new ArrayList<>();
+
+ public HttpCookies add(String name, String value) {
+ cookies.add(new Cookie(name, value));
+ return this;
+ }
+
+ public List<Cookie> getCookies() {
+ return Collections.unmodifiableList(cookies);
+ }
+
+}
diff --git a/core/src/main/java/org/apache/struts2/result/plain/HttpHeader.java b/core/src/main/java/org/apache/struts2/result/plain/HttpHeader.java
new file mode 100644
index 0000000..90de858
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/result/plain/HttpHeader.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. 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.
+ */
+package org.apache.struts2.result.plain;
+
+public interface HttpHeader<T> {
+
+ String getName();
+
+ T getValue();
+
+}
diff --git a/core/src/main/java/org/apache/struts2/result/plain/HttpHeaders.java b/core/src/main/java/org/apache/struts2/result/plain/HttpHeaders.java
new file mode 100644
index 0000000..31715c6
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/result/plain/HttpHeaders.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. 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.
+ */
+package org.apache.struts2.result.plain;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+class HttpHeaders {
+
+ private final List<HttpHeader<String>> stringHeaders = new ArrayList<>();
+ private final List<HttpHeader<Long>> dateHeaders = new ArrayList<>();
+ private final List<HttpHeader<Integer>> intHeaders = new ArrayList<>();
+
+ public HttpHeaders add(String name, String value) {
+ stringHeaders.add(new StringHttpHeader(name, value));
+ return this;
+ }
+
+ public HttpHeaders add(String name, Long value) {
+ dateHeaders.add(new DateHttpHeader(name, value));
+ return this;
+ }
+
+ public HttpHeaders add(String name, Integer value) {
+ intHeaders.add(new IntHttpHeader(name, value));
+ return this;
+ }
+
+ public List<HttpHeader<String>> getStringHeaders() {
+ return Collections.unmodifiableList(stringHeaders);
+ }
+
+ public List<HttpHeader<Long>> getDateHeaders() {
+ return Collections.unmodifiableList(dateHeaders);
+ }
+
+ public List<HttpHeader<Integer>> getIntHeaders() {
+ return Collections.unmodifiableList(intHeaders);
+ }
+
+}
diff --git a/core/src/main/java/org/apache/struts2/result/plain/IntHttpHeader.java b/core/src/main/java/org/apache/struts2/result/plain/IntHttpHeader.java
new file mode 100644
index 0000000..c190617
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/result/plain/IntHttpHeader.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. 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.
+ */
+package org.apache.struts2.result.plain;
+
+class IntHttpHeader implements HttpHeader<Integer> {
+
+ private final String name;
+ private final Integer value;
+
+ public IntHttpHeader(String name, Integer value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Integer getValue() {
+ return value;
+ }
+
+}
diff --git a/core/src/main/java/org/apache/struts2/result/plain/ResponseBuilder.java b/core/src/main/java/org/apache/struts2/result/plain/ResponseBuilder.java
new file mode 100644
index 0000000..5e1ae20
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/result/plain/ResponseBuilder.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. 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.
+ */
+package org.apache.struts2.result.plain;
+
+import javax.servlet.http.Cookie;
+
+public class ResponseBuilder {
+
+ public static final String CONTENT_TYPE = "Content-Type";
+
+ public static final String TEXT_PLAIN = "text/plain";
+ public static final String TEXT_HTML = "text/html";
+ public static final String APPLICATION_JSON = "application/json";
+
+ private final BodyWriter body;
+ private final HttpHeaders headers;
+ private final HttpCookies cookies;
+
+ public ResponseBuilder() {
+ this.body = new BodyWriter();
+ this.headers = new HttpHeaders().add(CONTENT_TYPE, TEXT_PLAIN + "; charset=UTF-8");
+ this.cookies = new HttpCookies();
+ }
+
+ public ResponseBuilder write(String out) {
+ body.write(out);
+ return this;
+ }
+
+ public ResponseBuilder writeLine(String out) {
+ body.writeLine(out);
+ return this;
+ }
+
+ public ResponseBuilder withHeader(String name, String value) {
+ headers.add(name, value);
+ return this;
+ }
+
+ public ResponseBuilder withHeader(String name, Long value) {
+ headers.add(name, value);
+ return this;
+ }
+
+ public ResponseBuilder withHeader(String name, Integer value) {
+ headers.add(name, value);
+ return this;
+ }
+
+ public ResponseBuilder withContentTypeTextPlain() {
+ headers.add(CONTENT_TYPE, TEXT_PLAIN + "; charset=UTF-8");
+ return this;
+ }
+
+ public ResponseBuilder withContentTypeTextHtml() {
+ headers.add(CONTENT_TYPE, TEXT_HTML + "; charset=UTF-8");
+ return this;
+ }
+
+ public ResponseBuilder withContentTypeJson() {
+ headers.add(CONTENT_TYPE, APPLICATION_JSON);
+ return this;
+ }
+
+ public ResponseBuilder withContentType(String contentType) {
+ headers.add(CONTENT_TYPE, contentType);
+ return this;
+ }
+
+ public ResponseBuilder withCookie(String name, String value) {
+ cookies.add(name, value);
+ return this;
+ }
+
+ public Iterable<HttpHeader<String>> getStringHeaders() {
+ return headers.getStringHeaders();
+ }
+
+ public Iterable<HttpHeader<Long>> getDateHeaders() {
+ return headers.getDateHeaders();
+ }
+
+ public Iterable<HttpHeader<Integer>> getIntHeaders() {
+ return headers.getIntHeaders();
+ }
+
+ public Iterable<Cookie> getCookies() {
+ return cookies.getCookies();
+ }
+
+ public String getBody() {
+ return body.getBody();
+ }
+
+}
diff --git a/core/src/main/java/org/apache/struts2/result/plain/StringHttpHeader.java b/core/src/main/java/org/apache/struts2/result/plain/StringHttpHeader.java
new file mode 100644
index 0000000..e4e3e03
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/result/plain/StringHttpHeader.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. 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.
+ */
+package org.apache.struts2.result.plain;
+
+class StringHttpHeader implements HttpHeader<String> {
+
+ private final String name;
+ private final String value;
+
+ public StringHttpHeader(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+}
diff --git a/core/src/test/java/org/apache/struts2/result/PlainResultTest.java b/core/src/test/java/org/apache/struts2/result/PlainResultTest.java
new file mode 100644
index 0000000..4cd193c
--- /dev/null
+++ b/core/src/test/java/org/apache/struts2/result/PlainResultTest.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. 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.
+ */
+package org.apache.struts2.result;
+
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.mock.MockActionInvocation;
+import org.apache.struts2.StrutsException;
+import org.apache.struts2.StrutsInternalTestCase;
+import org.apache.struts2.result.plain.ResponseBuilder;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+public class PlainResultTest extends StrutsInternalTestCase {
+
+ private MockHttpServletResponse response;
+ private MockActionInvocation invocation;
+
+ public void testWritePlainText() throws Exception {
+ PlainResult result = (PlainResult) response ->
+ response.write("test").withContentTypeTextPlain();
+
+ result.execute(invocation);
+
+ assertEquals("test", response.getContentAsString());
+ assertEquals("text/plain; charset=UTF-8", response.getContentType());
+ }
+
+ public void testWritePlainHtml() throws Exception {
+ PlainResult result = (PlainResult) response ->
+ response.write("<b>test</b>").withContentTypeTextHtml();
+
+ result.execute(invocation);
+
+ assertEquals("<b>test</b>", response.getContentAsString());
+ assertEquals("text/html; charset=UTF-8", response.getContentType());
+ }
+
+ public void testWriteJson() throws Exception {
+ PlainResult result = (PlainResult) response ->
+ response.write("{ 'value': 'test' }").withContentTypeJson();
+
+ result.execute(invocation);
+
+ assertEquals("{ 'value': 'test' }", response.getContentAsString());
+ assertEquals("application/json", response.getContentType());
+ }
+
+ public void testWriteContentTypeCsvWithCookie() throws Exception {
+ PlainResult result = (PlainResult) response ->
+ response.writeLine("name;value")
+ .withContentType("text/csv")
+ .withCookie("X-Test", "test")
+ .writeLine("line;1")
+ .write("line;2");
+
+ result.execute(invocation);
+
+ assertEquals("name;value\nline;1\nline;2", response.getContentAsString());
+ assertEquals("text/csv", response.getContentType());
+ }
+
+ public void testHeaders() throws Exception {
+ PlainResult result = (PlainResult) response ->
+ response.withHeader("X-String", "test")
+ .withHeader("X-Date", 0L)
+ .withHeader("X-Number", 100)
+ .write("");
+
+ result.execute(invocation);
+
+ assertEquals("", response.getContentAsString());
+ assertEquals("text/plain; charset=UTF-8", response.getContentType());
+ assertEquals("test", response.getHeader("X-String"));
+ assertEquals("Thu, 01 Jan 1970 00:00:00 GMT", response.getHeader("X-Date"));
+ assertEquals("100", response.getHeader("X-NUmber"));
+ }
+
+ public void testExceptionOnCommitted() throws Exception {
+ response.setCommitted(true);
+
+ PlainResult result = (PlainResult) response ->
+ response.write("");
+
+ try {
+ result.execute(invocation);
+ fail("Exception was expected!");
+ } catch (StrutsException e) {
+ assertEquals("Http response already committed, cannot modify it!", e.getMessage());
+ }
+ }
+
+ public void testNoExceptionOnCommitted() throws Exception {
+ response.setCommitted(true);
+
+ PlainResult result = new PlainResult() {
+ @Override
+ public void write(ResponseBuilder response) {
+ response.write("");
+ }
+
+ @Override
+ public boolean ignoreCommitted() {
+ return true;
+ }
+ };
+
+ try {
+ result.execute(invocation);
+ assertTrue(true);
+ } catch (StrutsException e) {
+ fail(e.getMessage());
+ }
+ }
+
+ public void setUp() throws Exception {
+ super.setUp();
+ invocation = new MockActionInvocation();
+ response = new MockHttpServletResponse();
+ invocation.setInvocationContext(ActionContext.getContext());
+
+ ActionContext.getContext().withServletResponse(response).withActionInvocation(invocation);
+ }
+}
\ No newline at end of file