/*
* 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 groovy.servlet;
import groovy.lang.
Binding;
import groovy.xml.
MarkupBuilder;
import org.codehaus.groovy.
GroovyBugError;
import org.codehaus.groovy.runtime.
MethodClosure;
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 java.io.
ByteArrayOutputStream;
import java.io.
IOException;
import java.io.
OutputStream;
import java.io.
PrintWriter;
import java.io.
Writer;
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 final
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 static 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 static 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);
}
}