/*
* Copyright 2003-2013 the original author or authors.
*
* 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 groovy.servlet;
import groovy.lang.Binding;
import groovy.xml.MarkupBuilder;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.runtime.MethodClosure;
import java.io.*;
import java.lang.reflect.Constructor;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
/**
* Servlet-specific binding extension to lazy load the writer or the output
* stream from the response.
* <p>
* <h3>Eager variables</h3>
* <ul>
* <li><tt>"request"</tt> : the <code>HttpServletRequest</code> object</li>
* <li><tt>"response"</tt> : the <code>HttpServletRequest</code> object</li>
* <li><tt>"context"</tt> : the <code>ServletContext</code> object</li>
* <li><tt>"application"</tt> : same as context</li>
* <li><tt>"session"</tt> : shorthand for <code>request.getSession(<tt>false</tt>)</code> - can be null!</li>
* <li><tt>"params"</tt> : map of all form parameters - can be empty</li>
* <li><tt>"headers"</tt> : map of all <tt>request</tt> header fields</li>
* </ul>
* <p>
* <h3>Lazy variables</h3>
* <ul>
* <li><tt>"out"</tt> : <code>response.getWriter()</code></li>
* <li><tt>"sout"</tt> : <code>response.getOutputStream()</code></li>
* <li><tt>"html"</tt> : <code>new MarkupBuilder(response.getWriter())</code> - <code>expandEmptyElements</code> flag is set to true</li>
* <li><tt>"json"</tt> : <code>new JsonBuilder()</code></li>
* </ul>
* As per the Servlet specification, a call to <code>response.getWriter()</code> should not be
* done if a call to <code>response.getOutputStream()</code> has already occurred or the other way
* around. You may wonder then how the above lazy variables can possibly be provided - since
* setting them up would involve calling both of the above methods. The trick is catered for
* behind the scenes using lazy variables. Lazy bound variables can be requested without side
* effects; under the covers the writer and stream are wrapped. That means
* <code>response.getWriter()</code> is never directly called until some output is done using
* 'out' or 'html'. Once a write method call is done using either of these variable, then an attempt
* to write using 'sout' will cause an <code>IllegalStateException</code>. Similarly, if a write method
* call on 'sout' has been done already, then any further write method call on 'out' or 'html' will cause an
* <code>IllegalStateException</code>.
* <p>
* <h3>Reserved internal variable names (see "Methods" below)</h3>
* <ul>
* <li><tt>"forward"</tt></li>
* <li><tt>"include"</tt></li>
* <li><tt>"redirect"</tt></li>
* </ul>
*
* If <code>response.getWriter()</code> is called directly (without using out), then a write method
* call on 'sout' will not cause the <code>IllegalStateException</code>, but it will still be invalid.
* It is the responsibility of the user of this class, to not to mix these different usage
* styles. The same applies to calling <code>response.getOutputStream()</code> and using 'out' or 'html'.
*
* <h3>Methods</h3>
* <ul>
* <li><tt>"forward(String path)"</tt> : <code>request.getRequestDispatcher(path).forward(request, response)</code></li>
* <li><tt>"include(String path)"</tt> : <code>request.getRequestDispatcher(path).include(request, response)</code></li>
* <li><tt>"redirect(String location)"</tt> : <code>response.sendRedirect(location)</code></li>
* </ul>
*
* @author Guillaume Laforge
* @author Christian Stein
* @author Jochen Theodorou
*/
public class ServletBinding extends Binding {
/**
* A OutputStream dummy that will throw a GroovyBugError for any
* write method call to it.
*
* @author Jochen Theodorou
*/
private static class InvalidOutputStream extends OutputStream {
/**
* Will always throw a GroovyBugError
* @see java.io.OutputStream#write(int)
*/
public void write(int b) {
throw new GroovyBugError("Any write calls to this stream are invalid!");
}
}
/**
* A class to manage the response output stream and writer.
* If the stream have been 'used', then using the writer will cause
* a IllegalStateException. If the writer have been 'used', then
* using the stream will cause a IllegalStateException. 'used' means
* any write method has been called. Simply requesting the objects will
* not cause an exception.
*
* @author Jochen Theodorou
*/
private static class ServletOutput {
private HttpServletResponse response;
private ServletOutputStream outputStream;
private PrintWriter writer;
public ServletOutput(HttpServletResponse response) {
this.response = response;
}
private ServletOutputStream getResponseStream() throws IOException {
if (writer!=null) throw new IllegalStateException("The variable 'out' or 'html' have been used already. Use either out/html or sout, not both.");
if (outputStream==null) outputStream = response.getOutputStream();
return outputStream;
}
public ServletOutputStream getOutputStream() {
return new ServletOutputStream() {
public void write(int b) throws IOException {
getResponseStream().write(b);
}
public void close() throws IOException {
getResponseStream().close();
}
public void flush() throws IOException {
getResponseStream().flush();
}
public void write(byte[] b) throws IOException {
getResponseStream().write(b);
}
public void write(byte[] b, int off, int len) throws IOException {
getResponseStream().write(b, off, len);
}
};
}
private PrintWriter getResponseWriter() {
if (outputStream!=null) throw new IllegalStateException("The variable 'sout' have been used already. Use either out/html or sout, not both.");
if (writer==null) {
try {
writer = response.getWriter();
} catch (IOException ioe) {
writer = new PrintWriter(new ByteArrayOutputStream());
throw new IllegalStateException("unable to get response writer",ioe);
}
}
return writer;
}
public PrintWriter getWriter() {
return new PrintWriter(new InvalidOutputStream()) {
public boolean checkError() {
return getResponseWriter().checkError();
}
public void close() {
getResponseWriter().close();
}
public void flush() {
getResponseWriter().flush();
}
public void write(char[] buf) {
getResponseWriter().write(buf);
}
public void write(char[] buf, int off, int len) {
getResponseWriter().write(buf, off, len);
}
public void write(int c) {
getResponseWriter().write(c);
}
public void write(String s, int off, int len) {
getResponseWriter().write(s, off, len);
}
public void println() {
getResponseWriter().println();
}
public PrintWriter format(String format, Object... args) {
getResponseWriter().format(format, args);
return this;
}
public PrintWriter format(Locale l, String format, Object... args) {
getResponseWriter().format(l, format, args);
return this;
}
};
}
}
private boolean initialized;
/**
* Initializes a servlet binding.
*
* @param request the HttpServletRequest object
* @param response the HttpServletRequest object
* @param context the ServletContext object
*/
public ServletBinding(HttpServletRequest request, HttpServletResponse response, ServletContext context) {
/*
* Bind the default variables.
*/
super.setVariable("request", request);
super.setVariable("response", response);
super.setVariable("context", context);
super.setVariable("application", context);
/*
* Bind the HTTP session object - if there is one.
* Note: we don't create one here!
*/
super.setVariable("session", request.getSession(false));
/*
* Bind form parameter key-value hash map.
*
* If there are multiple, they are passed as an array.
*/
Map params = collectParams(request);
super.setVariable("params", params);
/*
* Bind request header key-value hash map.
*/
Map<String, String> headers = new LinkedHashMap<String, String>();
for (Enumeration names = request.getHeaderNames(); names.hasMoreElements();) {
String headerName = (String) names.nextElement();
String headerValue = request.getHeader(headerName);
headers.put(headerName, headerValue);
}
super.setVariable("headers", headers);
}
@SuppressWarnings("unchecked")
private Map collectParams(HttpServletRequest request) {
Map params = new LinkedHashMap();
for (Enumeration names = request.getParameterNames(); names.hasMoreElements();) {
String name = (String) names.nextElement();
if (!super.getVariables().containsKey(name)) {
String[] values = request.getParameterValues(name);
if (values.length == 1) {
params.put(name, values[0]);
} else {
params.put(name, values);
}
}
}
return params;
}
@Override
public void setVariable(String name, Object value) {
lazyInit();
validateArgs(name, "Can't bind variable to");
excludeReservedName(name, "out");
excludeReservedName(name, "sout");
excludeReservedName(name, "html");
excludeReservedName(name, "json");
excludeReservedName(name, "forward");
excludeReservedName(name, "include");
excludeReservedName(name, "redirect");
super.setVariable(name, value);
}
@Override
public Map getVariables() {
lazyInit();
return super.getVariables();
}
/**
* @return a writer, an output stream, a markup builder or another requested object
*/
@Override
public Object getVariable(String name) {
lazyInit();
validateArgs(name, "No variable with");
return super.getVariable(name);
}
private void lazyInit() {
if (initialized) return;
initialized = true;
HttpServletResponse response = (HttpServletResponse) super.getVariable("response");
ServletOutput output = new ServletOutput(response);
super.setVariable("out", output.getWriter());
super.setVariable("sout", output.getOutputStream());
MarkupBuilder builder = new MarkupBuilder(output.getWriter());
builder.setExpandEmptyElements(true);
super.setVariable("html", builder);
try {
Class jsonBuilderClass = this.getClass().getClassLoader().loadClass("groovy.json.StreamingJsonBuilder");
Constructor writerConstructor = jsonBuilderClass.getConstructor(Writer.class);
super.setVariable("json", writerConstructor.newInstance(output.getWriter()));
} catch (Throwable t) {
t.printStackTrace();
}
// bind forward method
MethodClosure c = new MethodClosure(this, "forward");
super.setVariable("forward", c);
// bind include method
c = new MethodClosure(this, "include");
super.setVariable("include", c);
// bind redirect method
c = new MethodClosure(this, "redirect");
super.setVariable("redirect", c);
}
private void validateArgs(String name, String message) {
if (name == null) {
throw new IllegalArgumentException(message + " null key.");
}
if (name.length() == 0) {
throw new IllegalArgumentException(message + " blank key name. [length=0]");
}
}
private void excludeReservedName(String name, String reservedName) {
if (reservedName.equals(name)) {
throw new IllegalArgumentException("Can't bind variable to key named '" + name + "'.");
}
}
public void forward(String path) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) super.getVariable("request");
HttpServletResponse response = (HttpServletResponse) super.getVariable("response");
RequestDispatcher dispatcher = request.getRequestDispatcher(path);
dispatcher.forward(request, response);
}
public void include(String path) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) super.getVariable("request");
HttpServletResponse response = (HttpServletResponse) super.getVariable("response");
RequestDispatcher dispatcher = request.getRequestDispatcher(path);
dispatcher.include(request, response);
}
public void redirect(String location) throws IOException {
HttpServletResponse response = (HttpServletResponse) super.getVariable("response");
response.sendRedirect(location);
}
}