/* Copyright 2010 Ben Gunter
*
* 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.util.LinkedList;
import javax.servlet.ServletException;
import javax.servlet.jsp.PageContext;
import net.sourceforge.stripes.controller.StripesConstants;
import net.sourceforge.stripes.util.Log;
/**
* <p>
* An object that can be stuffed into a scope (page, request, application, etc.) and render a layout
* component to a string. This allows for use of EL expressions to output a component (as described
* in the book <em>Stripes ... and web development is fun again</em>) without requiring that all
* components be evaluated and buffered just in case a string representation is needed. The
* evaluation happens only when necessary, saving cycles and memory.
* </p>
* <p>
* When {@link #toString()} is called, the component renderer will evaluate the body of any
* {@link LayoutComponentTag} found in the stack of {@link LayoutContext}s maintained in the JSP
* {@link PageContext} having the same name as that passed to the constructor. The page context must
* be provided with a call to {@link #pushPageContext(PageContext)} for the renderer to work
* correctly.
* </p>
*
* @author Ben Gunter
* @since Stripes 1.5.4
*/
public class LayoutComponentRenderer {
private static final Log log = Log.getInstance(LayoutComponentRenderer.class);
private LinkedList<PageContext> pageContext;
private String component;
private LayoutContext context;
/**
* Create a new instance to render the named component to a string.
*
* @param component The name of the component to render.
*/
public LayoutComponentRenderer(String component) {
this.component = component;
}
/**
* Push a new page context onto the page context stack. The last page context pushed onto the
* stack is the one that will be used to evaluate the component tag's body.
*/
public void pushPageContext(PageContext pageContext) {
if (this.pageContext == null) {
this.pageContext = new LinkedList<PageContext>();
}
this.pageContext.addFirst(pageContext);
}
/** Pop the last page context off the stack and return it. */
public PageContext popPageContext() {
return pageContext == null ? null : pageContext.poll();
}
/** Get the last page context that was pushed onto the stack. */
public PageContext getPageContext() {
return pageContext == null ? null : pageContext.peek();
}
/** Get the path to the currently executing JSP. */
public String getCurrentPage() {
return (String) getPageContext().getRequest().getAttribute(
StripesConstants.REQ_ATTR_INCLUDE_PATH);
}
/**
* Write the component to the page context's writer, optionally buffering the output.
*
* @return True if the named component was found and it indicated that it successfully rendered;
* otherwise, false.
* @throws IOException If thrown by {@link LayoutContext#doInclude(PageContext, String)}
* @throws ServletException If thrown by {@link LayoutContext#doInclude(PageContext, String)}
*/
public boolean write() throws ServletException, IOException {
final PageContext pageContext = getPageContext();
if (pageContext == null) {
log.error("Failed to render component \"", this.component, "\" without a page context!");
return false;
}
// Grab some values from the current context so they can be restored when we're done
final LayoutContext savedContext = this.context;
final LayoutContext currentContext = LayoutContext.lookup(pageContext);
log.debug("Render component \"", this.component, "\" in ", getCurrentPage());
// Descend the stack from here, trying each context where the component is registered
for (LayoutContext context = savedContext == null ? currentContext : savedContext
.getPrevious(); context != null; context = context.getPrevious()) {
// Skip contexts where the desired component is not registered.
if (!context.getComponents().containsKey(this.component)) {
log.trace("Not rendering \"", this.component, "\" in context ",
context.getRenderPage(), " -> ", context.getDefinitionPage());
continue;
}
this.context = context;
// Take a snapshot of the context state
final LayoutContext savedNext = context.getNext();
final String savedComponent = context.getComponent();
final boolean savedComponentRenderPhase = context.isComponentRenderPhase();
final boolean savedSilent = context.getOut().isSilent();
try {
// Set up the context to render the component
context.setNext(null);
context.setComponentRenderPhase(true);
context.setComponent(this.component);
context.getOut().setSilent(true, pageContext);
log.debug("Start execute \"", this.component, "\" in ",
currentContext.getRenderPage(), " -> ", currentContext.getDefinitionPage(),
" from ", context.getRenderPage(), " -> ", context.getDefinitionPage());
context.doInclude(pageContext, context.getRenderPage());
log.debug("End execute \"", this.component, "\" in ",
currentContext.getRenderPage(), " -> ", currentContext.getDefinitionPage(),
" from ", context.getRenderPage(), " -> ", context.getDefinitionPage());
// If the component name has been cleared then the component rendered
if (context.getComponent() == null)
return true;
}
finally {
// Restore the context state
context.setNext(savedNext);
context.setComponent(savedComponent);
context.setComponentRenderPhase(savedComponentRenderPhase);
context.getOut().setSilent(savedSilent, pageContext);
// Restore the saved context
this.context = savedContext;
}
}
log.debug("Component \"", this.component, "\" evaluated to empty string in context ",
currentContext.getRenderPage(), " -> ", currentContext.getDefinitionPage());
return false;
}
/**
* Open a buffer in {@link LayoutWriter}, call {@link #write()} to render the component and then
* return the buffer contents.
*/
@Override
public String toString() {
final PageContext pageContext = getPageContext();
if (pageContext == null) {
log.error("Failed to render component \"", this.component, "\" without a page context!");
return "[Failed to render component \"" + this.component
+ "\" without a page context!]";
}
final LayoutContext context = LayoutContext.lookup(pageContext);
String contents;
context.getOut().openBuffer(pageContext);
try {
log.debug("Start stringify \"", this.component, "\" in ", context.getRenderPage(),
" -> ", context.getDefinitionPage());
write();
}
catch (Exception e) {
log.error(e, "Unhandled exception trying to render component \"", this.component,
"\" to a string in context ", context.getRenderPage(), //
" -> ", context.getDefinitionPage());
return "[Failed to render \"" + this.component + "\". See log for details.]";
}
finally {
log.debug("End stringify \"", this.component, "\" in ", context.getRenderPage(),
" -> ", context.getDefinitionPage());
contents = context.getOut().closeBuffer(pageContext);
}
return contents;
}
}