/* Copyright 2005-2006 Tim Fennell
*
* 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 net.sourceforge.stripes.tag.layout;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.servlet.jsp.PageContext;
import net.sourceforge.stripes.exception.StripesRuntimeException;
import net.sourceforge.stripes.util.Log;
/**
* Used to move contextual information about a layout rendering between a LayoutRenderTag and
* a LayoutDefinitionTag. Holds the set of overridden components and any parameters provided
* to the render tag.
*
* @author Tim Fennell, Ben Gunter
* @since Stripes 1.1
*/
public class LayoutContext {
private static final Log log = Log.getInstance(LayoutContext.class);
/** The attribute name by which the stack of layout contexts can be found in the request. */
public static final String LAYOUT_CONTEXT_KEY = LayoutContext.class.getName() + "#Context";
/**
* The attribute name by which the indicator of broken include functionality in the application
* server can be found in the application scope.
*/
public static final String BROKEN_INCLUDE_KEY = LayoutContext.class.getName()
+ "#BROKEN_INCLUDE";
/**
* Create a new layout context for the given render tag and push it onto the stack of layout
* contexts in a JSP page context.
*/
public static LayoutContext push(LayoutRenderTag renderTag) {
LayoutContext context = new LayoutContext(renderTag);
log.debug("Push context ", context.getRenderPage(), " -> ", context.getDefinitionPage());
PageContext pageContext = renderTag.getPageContext();
LayoutContext previous = lookup(pageContext);
if (previous == null) {
// Create a new layout writer and push a new body
context.out = new LayoutWriter(pageContext.getOut());
pageContext.pushBody(context.out);
}
else {
// Sanity check
if (previous.next != null) {
throw new StripesRuntimeException(
"Attempt to insert a new context into the middle of the stack");
}
// Link the two nodes
context.out = previous.out;
previous.next = context;
context.previous = previous;
}
pageContext.setAttribute(LAYOUT_CONTEXT_KEY, context);
return context;
}
/**
* Look up the current layout context in a JSP page context.
*
* @param pageContext The JSP page context to search for the layout context stack.
*/
public static LayoutContext lookup(PageContext pageContext) {
LayoutContext context = (LayoutContext) pageContext.getAttribute(LAYOUT_CONTEXT_KEY);
if (context == null) {
context = (LayoutContext) pageContext.getRequest().getAttribute(LAYOUT_CONTEXT_KEY);
if (context != null) {
for (LayoutContext c = context.getFirst(); c != context; c = c.getNext()) {
for (Entry<String, Object> entry : c.getParameters().entrySet()) {
pageContext.setAttribute(entry.getKey(), entry.getValue());
}
}
pageContext.setAttribute(LAYOUT_CONTEXT_KEY, context);
pageContext.getRequest().removeAttribute(LAYOUT_CONTEXT_KEY);
}
}
return context;
}
/**
* Remove the current layout context from the stack of layout contexts.
*
* @param pageContext The JSP page context to search for the layout context stack.
* @return The layout context that was popped off the stack, or null if the stack was not found
* or was empty.
*/
public static LayoutContext pop(PageContext pageContext) {
LayoutContext context = lookup(pageContext);
log.debug("Pop context ", context.getRenderPage(), " -> ", context.getDefinitionPage());
pageContext.setAttribute(LAYOUT_CONTEXT_KEY, context.previous);
if (context.previous == null) {
pageContext.popBody();
}
else {
context.previous.next = null;
context.previous = null;
}
return context;
}
private LayoutContext previous, next;
private LayoutRenderTag renderTag;
private LayoutWriter out;
private Map<String,LayoutComponentRenderer> components = new HashMap<String,LayoutComponentRenderer>();
private Map<String,Object> parameters = new HashMap<String,Object>();
private String renderPage, component;
private LayoutRenderTagPath componentPath;
private boolean componentRenderPhase, rendered;
/**
* A new context may be created only by a {@link LayoutRenderTag}. The tag provides all the
* information necessary to initialize the context.
*
* @param renderTag The tag that is beginning a new layout render process.
*/
public LayoutContext(LayoutRenderTag renderTag) {
this.renderTag = renderTag;
this.renderPage = renderTag.getCurrentPagePath();
this.componentPath = new LayoutRenderTagPath(renderTag);
log.debug("Path is ", this.componentPath);
}
/** Get the previous layout context from the stack. */
public LayoutContext getPrevious() { return previous; }
/** Get the next layout context from the stack. */
public LayoutContext getNext() { return next; }
/** Set the next layout context in the stack. */
public void setNext(LayoutContext next) { this.next = next; }
/** Get the first context in the list. */
public LayoutContext getFirst() {
for (LayoutContext c = this;; c = c.getPrevious()) {
if (c.getPrevious() == null)
return c;
}
}
/** Get the last context in the list. */
public LayoutContext getLast() {
for (LayoutContext c = this;; c = c.getNext()) {
if (c.getNext() == null)
return c;
}
}
/**
* <p>
* Called when a layout tag needs to execute a page in order to execute another layout tag.
* Special handling is implemented to ensure the included page is aware of the current layout
* context while also ensuring that pages included by other means (e.g., {@code jsp:include} or
* {@code c:import}) are <em>not</em> aware of the current layout context.
* </p>
* <p>
* This method calls {@link PageContext#include(String, boolean)} with {@code false} as the
* second parameter so that the response is not flushed before the include request executes.
* </p>
*/
public void doInclude(PageContext pageContext, String relativeUrlPath) throws ServletException,
IOException {
try {
pageContext.getRequest().setAttribute(LAYOUT_CONTEXT_KEY, this);
if (isIncludeBroken(pageContext))
doIncludeHack(pageContext, relativeUrlPath);
else
pageContext.include(relativeUrlPath, false);
}
finally {
pageContext.getRequest().removeAttribute(LAYOUT_CONTEXT_KEY);
}
}
/**
* Returns true if the current thread is executing in an application server that is known to
* have issues with doing normal includes using {@link PageContext#include(String, boolean)}.
*/
protected boolean isIncludeBroken(PageContext pageContext) {
Boolean b = (Boolean) pageContext.getServletContext().getAttribute(BROKEN_INCLUDE_KEY);
if (b == null) {
b = pageContext.getClass().getName().startsWith("weblogic.");
pageContext.getServletContext().setAttribute(BROKEN_INCLUDE_KEY, b);
if (b) {
log.info("This application server's include is broken so a workaround will be used.");
}
}
return b;
}
/**
* An alternative to {@link PageContext#include(String, boolean)} that works around broken
* include functionality in certain application servers, including several current and recent
* releases of WebLogic.
*/
protected void doIncludeHack(PageContext pageContext, String relativeUrlPath)
throws ServletException, IOException {
/**
* A servlet output stream implementation that decodes bytes to characters and writes the
* characters to an underlying writer.
*/
class MyServletOutputStream extends ServletOutputStream {
static final String DEFAULT_CHARSET = "UTF-8";
static final int BUFFER_SIZE = 1024;
Writer out;
String charset = DEFAULT_CHARSET;
CharsetDecoder decoder;
ByteBuffer bbuf;
CharBuffer cbuf;
/** Construct a new instance that sends output to the specified writer. */
MyServletOutputStream(Writer out) {
this.out = out;
}
/** Get the character set to which bytes will be decoded. */
String getCharset() {
return charset;
}
/** Set the character set to which bytes will be decoded. */
void setCharset(String charset) {
if (charset == null)
charset = DEFAULT_CHARSET;
// Create a new decoder only if the charset has changed
if (!charset.equals(this.charset))
decoder = null;
this.charset = charset;
}
/** Initialize the character decoder, byte buffer and character buffer. */
void initDecoder() {
if (decoder == null) {
decoder = Charset.forName(getCharset()).newDecoder();
if (bbuf == null)
bbuf = ByteBuffer.allocate(BUFFER_SIZE);
int size = (int) Math.ceil(BUFFER_SIZE * decoder.maxCharsPerByte());
if (cbuf == null || cbuf.capacity() != size)
cbuf = CharBuffer.allocate(size);
}
}
/**
* Clear the byte buffer. If the byte buffer has any data remaining to be read, then
* those bytes are shifted to the front of the buffer and the buffer's position is
* updated accordingly.
*/
void resetBuffer() {
if (bbuf.hasRemaining()) {
ByteBuffer slice = bbuf.slice();
bbuf.clear();
bbuf.put(slice);
}
else {
bbuf.clear();
}
}
/**
* Decode the contents of the byte buffer to the character buffer and then write the
* contents of the character buffer to the underlying writer.
*/
void decodeBuffer() throws IOException {
bbuf.flip();
cbuf.clear();
decoder.decode(bbuf, cbuf, false);
cbuf.flip();
out.write(cbuf.array(), cbuf.position(), cbuf.remaining());
resetBuffer();
}
@Override
public void print(char c) throws IOException {
out.write(c);
}
@Override
public void print(String s) throws IOException {
out.write(s);
}
@Override
public void write(int b) throws IOException {
initDecoder();
bbuf.put((byte) b);
decodeBuffer();
}
@Override
public void write(byte[] buf, int off, int len) throws IOException {
initDecoder();
for (int i = 0; i < len; i += bbuf.remaining()) {
int n = len - i;
if (n > bbuf.remaining())
n = bbuf.remaining();
bbuf.put(buf, i, n);
decodeBuffer();
}
}
@Override
public void write(byte[] buf) throws IOException {
write(buf, 0, buf.length);
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
}
final MyServletOutputStream os = new MyServletOutputStream(pageContext.getOut());
final PrintWriter writer = new PrintWriter(pageContext.getOut());
final HttpServletResponse response = new HttpServletResponseWrapper(
(HttpServletResponse) pageContext.getResponse()) {
@Override
public String getCharacterEncoding() {
return os.getCharset();
}
@Override
public void setCharacterEncoding(String charset) {
os.setCharset(charset);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return os;
}
@Override
public PrintWriter getWriter() throws IOException {
return writer;
}
};
pageContext.getRequest().getRequestDispatcher(relativeUrlPath)
.include(pageContext.getRequest(), response);
}
/** Get the render tag that created this context. */
public LayoutRenderTag getRenderTag() { return renderTag; }
/**
* Gets the Map of overridden components. Will return an empty Map if no components were
* overridden.
*/
public Map<String,LayoutComponentRenderer> getComponents() { return components; }
/** Gets the Map of parameters. Will return an empty Map if none were provided. */
public Map<String,Object> getParameters() { return parameters; }
/** Returns true if the layout has been rendered, false otherwise. */
public boolean isRendered() { return rendered; }
/** False initially, should be set to true when the layout is actually rendered. */
public void setRendered(final boolean rendered) { this.rendered = rendered; }
/** Get the path to the page that contains the {@link LayoutRenderTag} that created this context. */
public String getRenderPage() { return renderPage; }
/** Get the path to the page that contains the {@link LayoutDefinitionTag} referenced by the render tag. */
public String getDefinitionPage() { return getRenderTag().getName(); }
/** True if the intention of the current page execution is solely to render a component. */
public boolean isComponentRenderPhase() { return componentRenderPhase; }
/** Set the flag that indicates that the coming execution phase is solely to render a component. */
public void setComponentRenderPhase(boolean b) { this.componentRenderPhase = b; }
/** Get the name of the component to be rendered during the current phase of execution. */
public String getComponent() { return component; }
/** Set the name of the component to be rendered during the current phase of execution. */
public void setComponent(String component) { this.component = component; }
/**
* Get the list of components in the render page that must execute so that the render tag that
* created this context can execute.
*/
public LayoutRenderTagPath getComponentPath() { return componentPath; }
/** Get the layout writer to which the layout is rendered. */
public LayoutWriter getOut() { return out; }
/** To String implementation the parameters, and the component names. */
@Override
public String toString() {
return "LayoutContext{" +
"component names=" + components.keySet() +
", parameters=" + parameters +
'}';
}
}