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..3cbeb09
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/result/PlainResult.java
@@ -0,0 +1,64 @@
+/*
+ * 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.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;
+
+public interface PlainResult extends Result {
+
+    @Override
+    default void execute(ActionInvocation invocation) throws Exception {
+        ResponseBuilder builder = new ResponseBuilder();
+        write(builder);
+
+        HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
+
+        if (response.isCommitted()) {
+            throw new StrutsException("Http response already committed, cannot modify it!");
+        }
+
+        for (HttpHeader<String> header : builder.getStringHeaders()) {
+            response.addHeader(header.getName(), header.getValue());
+        }
+        for (HttpHeader<Long> header : builder.getDateHeaders()) {
+            response.addDateHeader(header.getName(), header.getValue());
+        }
+        for (HttpHeader<Integer> header : builder.getIntHeaders()) {
+            response.addIntHeader(header.getName(), header.getValue());
+        }
+
+        for (Cookie cookie : builder.getCookies()) {
+            response.addCookie(cookie);
+        }
+
+        response.getWriter().write(builder.getBody());
+        response.flushBuffer();
+    }
+
+    void write(ResponseBuilder response);
+
+}
+
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..9e53cee
--- /dev/null
+++ b/core/src/test/java/org/apache/struts2/result/PlainResultTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.StrutsInternalTestCase;
+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 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