org.grails.gsp.GroovyPage Maven / Gradle / Ivy
/*
* Copyright 2004-2023 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
*
* https://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.grails.gsp;
import java.io.Writer;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import groovy.lang.Binding;
import groovy.lang.Closure;
import groovy.lang.GroovyObject;
import groovy.lang.Script;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.groovy.runtime.InvokerHelper;
import grails.core.GrailsApplication;
import grails.util.CollectionUtils;
import org.grails.buffer.GrailsPrintWriter;
import org.grails.encoder.Encoder;
import org.grails.exceptions.ExceptionUtils;
import org.grails.gsp.jsp.JspTag;
import org.grails.gsp.jsp.JspTagLib;
import org.grails.gsp.jsp.TagLibraryResolver;
import org.grails.taglib.AbstractTemplateVariableBinding;
import org.grails.taglib.GrailsTagException;
import org.grails.taglib.GroovyPageAttributes;
import org.grails.taglib.TagBodyClosure;
import org.grails.taglib.TagInvocationContext;
import org.grails.taglib.TagInvocationContextCustomizer;
import org.grails.taglib.TagLibraryLookup;
import org.grails.taglib.TagOutput;
import org.grails.taglib.encoder.OutputContext;
import org.grails.taglib.encoder.OutputEncodingStack;
import org.grails.taglib.encoder.OutputEncodingStackAttributes;
import org.grails.taglib.encoder.WithCodecHelper;
/**
* NOTE: Based on work done by on the GSP standalone project (https://gsp.dev.java.net/)
*
* Base class for a GroovyPage (at the moment there is nothing in here but could be useful for
* providing utility methods etc.
*
* @author Troy Heninger
* @author Graeme Rocher
* @author Lari Hotari
*/
public abstract class GroovyPage extends Script {
private static final Log logger = LogFactory.getLog(GroovyPage.class);
private static final String APPLY_CODEC_TAG_NAME = "applyCodec";
public static final String ENCODE_AS_ATTRIBUTE_NAME = "encodeAs";
public static final Closure> EMPTY_BODY_CLOSURE = TagOutput.EMPTY_BODY_CLOSURE;
public static final String OUT = "out";
public static final String EXPRESSION_OUT = "expressionOut";
public static final String EXPRESSION_OUT_STATEMENT = EXPRESSION_OUT; // "getCodecOut()";
public static final String OUT_STATEMENT = OUT; // "getOut()";
public static final String CODEC_VARNAME = "Codec";
public static final String PLUGIN_CONTEXT_PATH = "pluginContextPath";
public static final String EXTENSION = ".gsp";
public static final String DEFAULT_NAMESPACE = "g";
public static final String PAGE_SCOPE = "pageScope";
public static final Collection RESERVED_NAMES = CollectionUtils.newSet(
OUT,
EXPRESSION_OUT,
CODEC_VARNAME,
PLUGIN_CONTEXT_PATH,
PAGE_SCOPE);
private static final String BINDING = "binding";
private static final String BLANK_STRING = "";
@SuppressWarnings("rawtypes")
private Map jspTags = Collections.emptyMap();
private TagLibraryResolver jspTagLibraryResolver;
protected TagLibraryLookup gspTagLibraryLookup;
private String[] htmlParts;
private Set htmlPartsSet;
private GrailsPrintWriter out;
private GrailsPrintWriter staticOut;
private GrailsPrintWriter expressionOut;
private OutputEncodingStack outputStack;
protected OutputContext outputContext;
private String pluginContextPath;
private Encoder rawEncoder;
private final List> bodyClosures = new ArrayList<>(15);
private List tagInvocationContextCustomizers = new ArrayList<>();
public GroovyPage() {
init();
}
protected void init() {
// do nothing
}
public final Writer getOut() {
return this.out;
}
public final Writer getExpressionOut() {
return this.expressionOut;
}
public void setOut(Writer newWriter) {
throw new IllegalStateException("Setting out in page isn't allowed.");
}
public void initRun(Writer target, OutputContext outputContext, GroovyPageMetaInfo metaInfo) {
OutputEncodingStackAttributes.Builder attributesBuilder = new OutputEncodingStackAttributes.Builder();
if (metaInfo != null) {
setJspTags(metaInfo.getJspTags());
setJspTagLibraryResolver(metaInfo.getJspTagLibraryResolver());
setGspTagLibraryLookup(metaInfo.getTagLibraryLookup());
setHtmlParts(metaInfo.getHtmlParts());
setPluginContextPath(metaInfo.getPluginPath());
setTagInvocationContextCustomizers(metaInfo.getTagInvocationContextCustomizers());
attributesBuilder.outEncoder(metaInfo.getOutEncoder());
attributesBuilder.staticEncoder(metaInfo.getStaticEncoder());
attributesBuilder.expressionEncoder(metaInfo.getExpressionEncoder());
attributesBuilder.defaultTaglibEncoder(metaInfo.getTaglibEncoder());
applyModelFieldsFromBinding(metaInfo.getModelFields());
}
attributesBuilder.allowCreate(true).topWriter(target).autoSync(false).pushTop(true);
attributesBuilder.outputContext(outputContext);
attributesBuilder.inheritPreviousEncoders(false);
this.outputStack = OutputEncodingStack.currentStack(attributesBuilder.build());
this.out = this.outputStack.getOutWriter();
this.staticOut = this.outputStack.getStaticWriter();
this.expressionOut = this.outputStack.getExpressionWriter();
this.outputContext = outputContext;
if (outputContext != null) {
outputContext.setCurrentWriter(this.out);
GrailsApplication grailsApplication = outputContext.getGrailsApplication();
if (grailsApplication != null) {
this.rawEncoder = WithCodecHelper.lookupEncoder(grailsApplication, "Raw");
}
}
setVariableDirectly(OUT, this.out);
setVariableDirectly(EXPRESSION_OUT, this.expressionOut);
}
private void applyModelFieldsFromBinding(Iterable modelFields) {
for (Field field : modelFields) {
try {
Object value = getProperty(field.getName());
if (value != null) {
field.set(this, value);
}
}
catch (IllegalAccessException e) {
throw new GroovyPagesException("Error setting model field '" + field.getName() + "'", e, -1, getGroovyPageFileName());
}
}
}
public Object raw(Object value) {
if (this.rawEncoder == null) {
return InvokerHelper.invokeMethod(value, "encodeAsRaw", null);
}
return this.rawEncoder.encode(value);
}
@SuppressWarnings("unchecked")
private void setVariableDirectly(String name, Object value) {
Binding binding = getBinding();
if (binding instanceof AbstractTemplateVariableBinding) {
((AbstractTemplateVariableBinding) binding).setVariableDirectly(name, value);
}
else {
binding.getVariables().put(name, value);
}
}
public String getPluginContextPath() {
return this.pluginContextPath != null ? this.pluginContextPath : BLANK_STRING;
}
public void setPluginContextPath(String pluginContextPath) {
this.pluginContextPath = pluginContextPath;
}
public void cleanup() {
this.outputStack.pop(true);
}
public final void createClosureForHtmlPart(int partNumber, int bodyClosureIndex) {
final String htmlPart = this.htmlParts[partNumber];
setBodyClosure(bodyClosureIndex, new TagOutput.ConstantClosure(htmlPart));
}
public final void setBodyClosure(int index, Closure> bodyClosure) {
while (index >= this.bodyClosures.size()) {
this.bodyClosures.add(null);
}
this.bodyClosures.set(index, bodyClosure);
}
public final Closure> getBodyClosure(int index) {
if (index >= 0) {
return this.bodyClosures.get(index);
}
return null;
}
/**
* Sets the JSP tag library resolver to use to resolve JSP tags
*
* @param jspTagLibraryResolver The JSP tag resolve
*/
public void setJspTagLibraryResolver(TagLibraryResolver jspTagLibraryResolver) {
this.jspTagLibraryResolver = jspTagLibraryResolver;
}
/**
* Sets the GSP tag library lookup class
*
* @param gspTagLibraryLookup The class used to lookup a GSP tag library
*/
public void setGspTagLibraryLookup(TagLibraryLookup gspTagLibraryLookup) {
this.gspTagLibraryLookup = gspTagLibraryLookup;
}
/**
* Obtains a reference to the JSP tag library resolver instance
*
* @return The JSP TagLibraryResolver instance
*/
TagLibraryResolver getTagLibraryResolver() {
return this.jspTagLibraryResolver;
}
/**
* Set the customizers
* @param tagInvocationContextCustomizers the customizer
* @since 2022.0.0
*/
void setTagInvocationContextCustomizers(List tagInvocationContextCustomizers) {
this.tagInvocationContextCustomizers = tagInvocationContextCustomizers;
}
/**
* In the development environment this method is used to evaluate expressions and improve error reporting
*
* @param exprText The expression text
* @param lineNumber The line number
* @param outerIt The other reference to the variable 'it'
* @param evaluator The expression evaluator
* @return The result
*/
public Object evaluate(String exprText, int lineNumber, Object outerIt, Closure> evaluator) {
try {
return evaluator.call(outerIt);
}
catch (Exception e) {
throw new GroovyPagesException("Error evaluating expression [" + exprText + "] on line [" +
lineNumber + "]: " + e.getMessage(), e, lineNumber, getGroovyPageFileName());
}
}
public abstract String getGroovyPageFileName();
@Override
public Object getProperty(String property) {
if (OUT.equals(property)) {
return this.out;
}
if (EXPRESSION_OUT.equals(property)) {
return this.expressionOut;
}
// in GSP we assume if a property doesn't exist that
// it is null rather than throw an error this works nicely
// with the Groovy Truth
if (BINDING.equals(property)) {
return getBinding();
}
return resolveProperty(property);
}
protected Object resolveProperty(String property) {
Object value = getBinding().getVariable(property);
if (value != null) {
return value;
}
// check if a taglib can be found
value = lookupTagDispatcher(property);
if (value == null) {
value = lookupJspTagLib(property);
}
if (value != null) {
// cache lookup for next execution
setVariableDirectly(property, value);
}
return value;
}
protected Object lookupTagDispatcher(String namespace) {
return this.gspTagLibraryLookup != null ? this.gspTagLibraryLookup.lookupNamespaceDispatcher(namespace) : null;
}
protected JspTagLib lookupJspTagLib(String jspTagLibName) {
String uri = (String) this.jspTags.get(jspTagLibName);
if (uri != null) {
TagLibraryResolver tagResolver = getTagLibraryResolver();
return tagResolver.resolveTagLibrary(uri);
}
return null;
}
/* Static call for jsp tags resolution
*
* @param uri tag uri
* @param name tag name
* @return resolved tag if any
*/
public JspTag getJspTag(String uri, String name) {
if (this.jspTagLibraryResolver == null) {
return null;
}
JspTagLib tagLib = this.jspTagLibraryResolver.resolveTagLibrary(uri);
return tagLib != null ? tagLib.getTag(name) : null;
}
/**
* Attempts to invoke a dynamic tag
*
* @param tagName The name of the tag
* @param tagNamespace The taglib's namespace
* @param lineNumber GSP source lineNumber
* @param attrs The tags attributes
* @param bodyClosureIndex The index of the body variable
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public final void invokeTag(String tagName, String tagNamespace, int lineNumber, Map attrs, int bodyClosureIndex) {
// Handling custom namespace and tags
TagInvocationContext tagInvocationContext = new TagInvocationContext(tagNamespace, tagName, attrs);
applyTagInvocationContextCustomizers(tagInvocationContext);
String theNamespace = tagInvocationContext.getNamespace();
String theTagName = tagInvocationContext.getTagName();
Map theAttrs = tagInvocationContext.getAttrs();
Closure body = getBodyClosure(bodyClosureIndex);
try {
GroovyObject tagLib = getTagLib(theNamespace, theTagName);
if (tagLib != null || (this.gspTagLibraryLookup != null && this.gspTagLibraryLookup.hasNamespace(theNamespace))) {
if (tagLib != null) {
boolean returnsObject = this.gspTagLibraryLookup.doesTagReturnObject(theNamespace, theTagName);
Object tagLibClosure = tagLib.getProperty(theTagName);
if (tagLibClosure instanceof Closure) {
Map encodeAsForTag = this.gspTagLibraryLookup.getEncodeAsForTag(theNamespace, theTagName);
invokeTagLibClosure(theTagName, theNamespace, (Closure) tagLibClosure, theAttrs, body, returnsObject, encodeAsForTag);
}
else {
throw new GrailsTagException("Tag [" + theTagName + "] does not exist in tag library [" + tagLib.getClass().getName() + "]",
getGroovyPageFileName(), lineNumber);
}
}
else {
throw new GrailsTagException("Tag [" + theTagName + "] does not exist. No tag library found for namespace: " + theNamespace,
getGroovyPageFileName(), lineNumber);
}
}
else {
this.staticOut.append('<').append(theNamespace).append(':').append(theTagName);
for (Object o : theAttrs.entrySet()) {
Map.Entry entry = (Map.Entry) o;
this.staticOut.append(' ');
this.staticOut.append(entry.getKey()).append('=');
String value = String.valueOf(entry.getValue());
// handle attribute value quotes & possible escaping " -> "
boolean containsQuotes = (value.indexOf('"') > -1);
boolean containsSingleQuote = (value.indexOf('\'') > -1);
if (containsQuotes && !containsSingleQuote) {
this.staticOut.append('\'').append(value).append('\'');
}
else if (containsQuotes & containsSingleQuote) {
this.staticOut.append('\"').append(value.replaceAll("\"", """)).append('\"');
}
else {
this.staticOut.append('\"').append(value).append('\"');
}
}
if (body == null) {
this.staticOut.append("/>");
}
else {
this.staticOut.append('>');
Object bodyOutput = body.call();
if (bodyOutput != null) {
this.staticOut.print(bodyOutput);
}
this.staticOut.append("").append(theNamespace).append(':').append(theTagName).append('>');
}
}
}
catch (Throwable e) {
if (logger.isTraceEnabled()) {
logger.trace("Full exception for problem at " + getGroovyPageFileName() + ":" + lineNumber, e);
}
// The capture* tags are internal tags and not to be displayed to the user
// hence we don't wrap the exception and simple rethrow it
if (theTagName.matches("capture(Body|Head|Meta|Title|Component)")) {
RuntimeException rte = ExceptionUtils.getFirstRuntimeException(e);
if (rte == null) {
throwRootCause(theTagName, theNamespace, lineNumber, e);
}
else {
throw rte;
}
}
else {
throwRootCause(theTagName, theNamespace, lineNumber, e);
}
}
}
private void invokeTagLibClosure(String tagName, String tagNamespace, Closure> tagLibClosure, Map, ?> attrs, Closure> body,
boolean returnsObject, Map defaultEncodeAs) {
Closure> tag = (Closure>) tagLibClosure.clone();
if (!(attrs instanceof GroovyPageAttributes)) {
attrs = new GroovyPageAttributes(attrs);
}
((GroovyPageAttributes) attrs).setGspTagSyntaxCall(true);
boolean encodeAsPushedToStack = false;
try {
Map codecSettings = TagOutput.createCodecSettings(tagNamespace, tagName, attrs, defaultEncodeAs);
if (codecSettings != null) {
this.outputStack.push(WithCodecHelper.createOutputStackAttributesBuilder(codecSettings, this.outputContext.getGrailsApplication())
.build());
encodeAsPushedToStack = true;
}
Object tagresult = null;
switch (tag.getParameterTypes().length) {
case 1:
tagresult = tag.call(new Object[] { attrs });
outputTagResult(returnsObject, tagresult);
if (body != null && body != TagOutput.EMPTY_BODY_CLOSURE) {
body.call();
}
break;
case 2:
tagresult = tag.call(new Object[] { attrs, (body != null) ? body : TagOutput.EMPTY_BODY_CLOSURE });
outputTagResult(returnsObject, tagresult);
break;
}
}
finally {
if (encodeAsPushedToStack) {
this.outputStack.pop();
}
}
}
private void outputTagResult(boolean returnsObject, Object tagresult) {
if (returnsObject && tagresult != null && !(tagresult instanceof Writer)) {
if (tagresult instanceof String && isHtmlPart((String) tagresult)) {
this.staticOut.print(tagresult);
}
else {
this.outputStack.getTaglibWriter().print(tagresult);
}
}
}
private void throwRootCause(String tagName, String tagNamespace, int lineNumber, Throwable e) {
Throwable cause = ExceptionUtils.getRootCause(e);
if (cause instanceof GrailsTagException) {
// catch and rethrow with context
throw new GrailsTagException(cause.getMessage(), getGroovyPageFileName(), lineNumber);
}
throw new GrailsTagException("Error executing tag <" + tagNamespace + ":" + tagName +
">: " + e.getMessage(), e, getGroovyPageFileName(), lineNumber);
}
private GroovyObject getTagLib(String namespace, String tagName) {
return TagOutput.lookupCachedTagLib(this.gspTagLibraryLookup, namespace, tagName);
}
/**
* Return whether the given name cannot be used within the binding of a GSP
*
* @param name True if it can't
* @return A boolean true or false
*/
public static final boolean isReservedName(String name) {
return RESERVED_NAMES.contains(name);
}
public final void printHtmlPart(final int partNumber) {
this.staticOut.write(this.htmlParts[partNumber]);
}
/**
* Sets the JSP tags used by this GroovyPage instance
*
* @param jspTags The JSP tags used
*/
@SuppressWarnings("rawtypes")
public void setJspTags(Map jspTags) {
this.jspTags = jspTags;
}
public String[] getHtmlParts() {
return this.htmlParts;
}
protected boolean isHtmlPart(String htmlPart) {
return this.htmlPartsSet != null && htmlPart != null && this.htmlPartsSet.contains(System.identityHashCode(htmlPart));
}
public void setHtmlParts(String[] htmlParts) {
this.htmlParts = htmlParts;
this.htmlPartsSet = new HashSet();
if (htmlParts != null) {
for (String htmlPart : htmlParts) {
if (htmlPart != null) {
this.htmlPartsSet.add(System.identityHashCode(htmlPart));
}
}
}
}
public final OutputEncodingStack getOutputStack() {
return this.outputStack;
}
public OutputContext getOutputContext() {
return this.outputContext;
}
public void applyTagInvocationContextCustomizers(TagInvocationContext tagInvocationContext) {
for (TagInvocationContextCustomizer customizer : this.tagInvocationContextCustomizers) {
customizer.customize(tagInvocationContext);
}
}
public final void registerSitemeshPreprocessMode() {
/*
TODO: grails-gsp refactoring
if (request == null) {
return;
}
GSPSitemeshPage page = (GSPSitemeshPage) request.getAttribute(GrailsLayoutView.GSP_SITEMESH_PAGE);
if (page != null) {
page.setUsed(true);
}
*/
}
public final void createTagBody(int bodyClosureIndex, Closure> bodyClosure) {
TagBodyClosure tagBody = new TagBodyClosure(this, this.outputContext, bodyClosure, true);
setBodyClosure(bodyClosureIndex, tagBody);
}
public void changeItVariable(Object value) {
setVariableDirectly("it", value);
}
}