diff --git a/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPage.java b/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPage.java index d59a5d54d2d..095f5a098c3 100644 --- a/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPage.java +++ b/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPage.java @@ -23,7 +23,6 @@ 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; @@ -84,6 +83,7 @@ public abstract class GroovyPage extends Script { public static final String LINK_NAMESPACE = "link"; public static final String TEMPLATE_NAMESPACE = "tmpl"; public static final String PAGE_SCOPE = "pageScope"; + private OutputEncodingStackAttributes.Builder attributesBuilder; public static final Collection RESERVED_NAMES = CollectionUtils.newSet( OUT, @@ -130,23 +130,30 @@ 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(); + public void initCommonRun(GroovyPageMetaInfo metaInfo) { + attributesBuilder = new OutputEncodingStackAttributes.Builder(); if (metaInfo != null) { - setJspTags(metaInfo.getJspTags()); - setJspTagLibraryResolver(metaInfo.getJspTagLibraryResolver()); - setGspTagLibraryLookup(metaInfo.getTagLibraryLookup()); - setHtmlParts(metaInfo.getHtmlParts()); - setPluginContextPath(metaInfo.getPluginPath()); attributesBuilder.outEncoder(metaInfo.getOutEncoder()); attributesBuilder.staticEncoder(metaInfo.getStaticEncoder()); attributesBuilder.expressionEncoder(metaInfo.getExpressionEncoder()); attributesBuilder.defaultTaglibEncoder(metaInfo.getTaglibEncoder()); + setHtmlParts(metaInfo.getHtmlParts()); + setHtmlPartsSet(metaInfo.getHtmlPartsSet()); + setJspTags(metaInfo.getJspTags()); + setJspTagLibraryResolver(metaInfo.getJspTagLibraryResolver()); + setGspTagLibraryLookup(metaInfo.getTagLibraryLookup()); + setPluginContextPath(metaInfo.getPluginPath()); + } + attributesBuilder.allowCreate(true).autoSync(false).pushTop(true); + attributesBuilder.inheritPreviousEncoders(false); + } + + public void initRun(Writer target, OutputContext outputContext, GroovyPageMetaInfo metaInfo) { + if (metaInfo != null) { applyModelFieldsFromBinding(metaInfo.getModelFields()); } - attributesBuilder.allowCreate(true).topWriter(target).autoSync(false).pushTop(true); attributesBuilder.outputContext(outputContext); - attributesBuilder.inheritPreviousEncoders(false); + attributesBuilder.topWriter(target); outputStack = OutputEncodingStack.currentStack(attributesBuilder.build()); out = outputStack.getOutWriter(); @@ -507,6 +514,14 @@ public final void printHtmlPart(final int partNumber) { staticOut.write(htmlParts[partNumber]); } + /** + * Shorthand for printHtmlPart to reduce class size + * @param partNumber + */ + public final void h(final int partNumber) { + staticOut.write(htmlParts[partNumber]); + } + /** * Sets the JSP tags used by this GroovyPage instance * @@ -527,14 +542,10 @@ protected boolean isHtmlPart(String htmlPart) { public void setHtmlParts(String[] htmlParts) { this.htmlParts = htmlParts; - this.htmlPartsSet = new HashSet<>(); - if (htmlParts != null) { - for (String htmlPart : htmlParts) { - if (htmlPart != null) { - htmlPartsSet.add(System.identityHashCode(htmlPart)); - } - } - } + } + + public void setHtmlPartsSet(Set htmlPartsSet) { + this.htmlPartsSet = htmlPartsSet; } public final OutputEncodingStack getOutputStack() { diff --git a/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPageMetaInfo.java b/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPageMetaInfo.java index 0cd8efd2989..f8f50c4f16f 100644 --- a/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPageMetaInfo.java +++ b/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPageMetaInfo.java @@ -22,7 +22,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.Writer; +import java.lang.ref.SoftReference; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.net.URL; import java.net.URLConnection; @@ -67,14 +70,17 @@ public class GroovyPageMetaInfo implements GrailsApplicationAware { private static final Log LOG = LogFactory.getLog(GroovyPageMetaInfo.class); private TagLibraryLookup tagLibraryLookup; private TagLibraryResolver jspTagLibraryResolver; + private ThreadLocal> pageInstance = new ThreadLocal<>(); private boolean precompiledMode = false; private Class pageClass; + private Constructor pageClassConstructor; private long lastModified; private InputStream groovySource; private String contentType; private int[] lineNumbers; private String[] htmlParts; + private Set htmlPartsSet; @SuppressWarnings("rawtypes") private Map jspTags = Collections.emptyMap(); private GroovyPagesException compilationException; @@ -115,6 +121,11 @@ public GroovyPageMetaInfo(Class pageClass) { this(); precompiledMode = true; this.pageClass = pageClass; + try { + this.pageClassConstructor = pageClass.getConstructor(); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } contentType = (String) ReflectionUtils.getField(ReflectionUtils.findField(pageClass, GroovyPageParser.CONSTANT_NAME_CONTENT_TYPE), null); jspTags = (Map) ReflectionUtils.getField(ReflectionUtils.findField(pageClass, GroovyPageParser.CONSTANT_NAME_JSP_TAGS), null); lastModified = (Long) ReflectionUtils.getField(ReflectionUtils.findField(pageClass, GroovyPageParser.CONSTANT_NAME_LAST_MODIFIED), null); @@ -133,8 +144,7 @@ public GroovyPageMetaInfo(Class pageClass) { try { readHtmlData(); - } - catch (IOException e) { + } catch (IOException e) { throw new RuntimeException("Problem reading html data for page class " + pageClass, e); } } @@ -217,8 +227,7 @@ private void readHtmlData() throws IOException { htmlParts[i] = input.readUTF(); } } - } - finally { + } finally { IOUtils.closeQuietly(input); } } @@ -239,8 +248,7 @@ private void readLineNumbers() throws IOException { for (int i = 0; i < arrayLen; i++) { lineNumbers[i] = input.readInt(); } - } - finally { + } finally { IOUtils.closeQuietly(input); } } @@ -282,8 +290,28 @@ public Class getPageClass() { return pageClass; } + public GroovyPage getPageClassInstance() throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + SoftReference pageSoftRef = pageInstance.get(); + GroovyPage pageCacheEntry = pageSoftRef != null ? pageSoftRef.get() : null; + if (pageCacheEntry == null) { + pageCacheEntry = (GroovyPage) pageClassConstructor.newInstance(); + pageCacheEntry.initCommonRun(this); + if (!isModelFieldsMode()) { + pageInstance.set(new SoftReference<>(pageCacheEntry)); + } + } + return pageCacheEntry; + } + public void setPageClass(Class pageClass) { this.pageClass = pageClass; + + try { + this.pageClassConstructor = pageClass.getConstructor(); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + pageInstance.set(null); initializePluginPath(); } @@ -323,8 +351,7 @@ private synchronized int[] getPrecompiledLineNumbers() { if (lineNumbers == null) { try { readLineNumbers(); - } - catch (IOException e) { + } catch (IOException e) { LOG.warn("Problem reading precompiled linenumbers", e); } } @@ -359,6 +386,18 @@ public String[] getHtmlParts() { public void setHtmlParts(String[] htmlParts) { this.htmlParts = htmlParts; + this.htmlPartsSet = new HashSet<>(); + if (htmlParts != null) { + for (String htmlPart : htmlParts) { + if (htmlPart != null) { + htmlPartsSet.add(System.identityHashCode(htmlPart)); + } + } + } + } + + Set getHtmlPartsSet() { + return this.htmlPartsSet; } public void applyLastModifiedFromResource(Resource resource) { @@ -394,22 +433,18 @@ private long establishLastModified(Resource resource) { urlc.setDoInput(false); urlc.setDoOutput(false); last = urlc.getLastModified(); - } - catch (FileNotFoundException fnfe) { + } catch (FileNotFoundException fnfe) { last = -1; - } - catch (IOException e) { + } catch (IOException e) { last = -1; - } - finally { + } finally { if (urlc != null) { try { InputStream is = urlc.getInputStream(); if (is != null) { is.close(); } - } - catch (IOException e) { + } catch (IOException e) { // ignore } } diff --git a/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPageWritable.java b/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPageWritable.java index 6fb297db3c8..522010a4173 100644 --- a/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPageWritable.java +++ b/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPageWritable.java @@ -30,8 +30,8 @@ import groovy.lang.Binding; import groovy.lang.Writable; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import grails.util.Environment; import org.grails.taglib.AbstractTemplateVariableBinding; @@ -47,7 +47,7 @@ * @since 0.5 */ public class GroovyPageWritable implements Writable { - private static final Log LOG = LogFactory.getLog(GroovyPageWritable.class); + private static final Logger LOG = LoggerFactory.getLogger(GroovyPageWritable.class); private static final String GSP_NONE_CODEC_NAME = "none"; private GroovyPageMetaInfo metaInfo; private OutputContextLookup outputContextLookup; @@ -101,8 +101,7 @@ protected Writer doWriteTo(OutputContext outputContext, Writer out) throws IOExc // Set it to TEXT outputContext.setContentType(GROOVY_SOURCE_CONTENT_TYPE); // must come before response.getOutputStream() writeGroovySourceToResponse(metaInfo, out); - } - else { + } else { // Set it to HTML by default if (metaInfo.getCompilationException() != null) { throw metaInfo.getCompilationException(); @@ -125,9 +124,7 @@ protected Writer doWriteTo(OutputContext outputContext, Writer out) throws IOExc // only try to set content type when evaluating top level GSP boolean contentTypeAlreadySet = outputContext.isContentTypeAlreadySet(); if (!contentTypeAlreadySet) { - if (LOG.isDebugEnabled()) { - LOG.debug("Writing output with content type: " + metaInfo.getContentType()); - } + LOG.debug("Writing output with content type: {}", metaInfo.getContentType()); outputContext.setContentType(metaInfo.getContentType()); // must come before response.getWriter() } } @@ -136,13 +133,15 @@ protected Writer doWriteTo(OutputContext outputContext, Writer out) throws IOExc if (hasRequest) { outputContext.setBinding(binding); } - + Binding existingBinding = null; GroovyPage page = null; try { - page = (GroovyPage) metaInfo.getPageClass().newInstance(); + page = metaInfo.getPageClassInstance(); + existingBinding = page.getBinding(); } catch (Exception e) { throw new GroovyPagesException("Problem instantiating page class", e); } + page.setBinding(binding); binding.setOwner(page); @@ -150,8 +149,10 @@ protected Writer doWriteTo(OutputContext outputContext, Writer out) throws IOExc try { page.run(); - } - finally { + } finally { + if (existingBinding != null) { + page.setBinding(existingBinding); + } page.cleanup(); if (hasRequest) { if (newParentCreated) { @@ -220,13 +221,12 @@ protected void writeInputStreamToResponse(InputStream in, Writer out) throws IOE Reader reader = new InputStreamReader(in, "UTF-8"); char[] buf = new char[8192]; - for (;;) { + for (; ; ) { int read = reader.read(buf); if (read <= 0) break; out.write(buf, 0, read); } - } - finally { + } finally { out.close(); in.close(); } @@ -249,8 +249,7 @@ protected void writeGroovySourceToResponse(GroovyPageMetaInfo info, Writer out) try { try { in.reset(); - } - catch (IOException e) { + } catch (IOException e) { // ignore } BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8")); @@ -288,8 +287,7 @@ protected void writeGroovySourceToResponse(GroovyPageMetaInfo info, Writer out) out.write(line); out.write('\n'); } - } - finally { + } finally { out.close(); in.close(); } diff --git a/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPagesTemplateEngine.java b/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPagesTemplateEngine.java index db5e83eda9d..f82e642ff1e 100644 --- a/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPagesTemplateEngine.java +++ b/grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPagesTemplateEngine.java @@ -418,6 +418,27 @@ public Template createTemplate(String txt, String pageName) throws IOException { return createTemplate(new ByteArrayResource(txt.getBytes(StandardCharsets.UTF_8), pageName), pageName, pageName != null); } + /** + * Creates a Template using the given text for the Template and the given name. The name + * of the template is required + * + * @param txt The URI of the page to create the template for + * @param pageName The name of the page being parsed + * @param cache If the template should be cached + * + * @return The Template instance + * @throws CompilationFailedException + * @throws IOException Thrown if an IO exception occurs creating the Template + */ + public Template createTemplate(String txt, String pageName, boolean cache) throws IOException { + Assert.hasLength(txt, "Argument [txt] cannot be null or blank"); + Assert.hasLength(pageName, "Argument [pageName] cannot be null or blank"); + + return createTemplate(new ByteArrayResource(txt.getBytes(StandardCharsets.UTF_8), pageName), pageName, cache); + } + + /** + * Creates a Template using the given text for the Template and the given name. The name /** * Creates a Template for the given file * diff --git a/grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GroovyPageCompiler.groovy b/grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GroovyPageCompiler.groovy index 3a9917a4154..9ebc8e1c144 100644 --- a/grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GroovyPageCompiler.groovy +++ b/grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GroovyPageCompiler.groovy @@ -30,21 +30,22 @@ import groovy.transform.CompileStatic import org.codehaus.groovy.control.CompilationUnit import org.codehaus.groovy.control.CompilerConfiguration import org.codehaus.groovy.control.Phases - -import org.apache.commons.logging.Log -import org.apache.commons.logging.LogFactory - +import org.slf4j.LoggerFactory +import org.slf4j.Logger import org.springframework.core.CollectionFactory - import grails.config.ConfigMap import org.apache.grails.gradle.common.PropertyFileUtils import org.grails.config.CodeGenConfig import org.grails.gsp.GroovyPageMetaInfo import org.grails.gsp.compiler.transform.GroovyPageInjectionOperation import org.grails.taglib.encoder.OutputEncodingSettings +import org.grails.gsp.GroovyPage /** - * Used to compile GSP files into a specified target directory. + * Used to compile GSP files into a specified target directory. The compiler creates 3 files per page. + * Firstly, it generates a {@link GroovyPage} derived class which is then compiled to a .class file. + * It also will generate a "_html.data" and a "_linenumbers.data" file which contain the static HTML parts of the page. + * These are read at runtime by the {@link org.grails.gsp.GroovyPagesTemplateEngine} class. * * @author Graeme Rocher * @since 1.2 @@ -52,7 +53,7 @@ import org.grails.taglib.encoder.OutputEncodingSettings @CompileStatic class GroovyPageCompiler { - private static final Log LOG = LogFactory.getLog(GroovyPageCompiler) + private static final Logger LOG = LoggerFactory.getLogger(GroovyPageCompiler) private Map compileGSPRegistry = [:] private Object mutexObject = new Object() @@ -83,8 +84,8 @@ class GroovyPageCompiler { } /** - * Compiles the given GSP pages and returns a Map of URI to classname mappings - */ + * Compiles the given GSP pages and returns a Map of URI to classname mappings + */ Map compile() { if (srcFiles && targetDir && viewsDir) { if (!generatedGroovyPagesDirectory) { @@ -100,8 +101,7 @@ class GroovyPageCompiler { if (f.exists()) { if (f.name.endsWith('.yml')) { codeGenConfig.loadYml(f) - } - else if (f.name.endsWith('.groovy')) { + } else if (f.name.endsWith('.groovy')) { codeGenConfig.loadGroovy(f) } } @@ -206,8 +206,7 @@ class GroovyPageCompiler { String fullClassName if (packageName) { fullClassName = packageName + '.' + className - } - else { + } else { fullClassName = className } @@ -225,11 +224,11 @@ class GroovyPageCompiler { gpp.generateGsp(gsptarget) gsptarget.flush() // write static html parts to data file (read from classpath at runtime) - File htmlDataFile = new File(new File(targetDir, packageDir), className + GroovyPageMetaInfo.HTML_DATA_POSTFIX) + File htmlDataFile = new File(new File(targetDir, packageDir), className + GroovyPageMetaInfo.HTML_DATA_POSTFIX) htmlDataFile.parentFile.mkdirs() gpp.writeHtmlParts(htmlDataFile) // write linenumber mapping info to data file - File lineNumbersDataFile = new File(new File(targetDir, packageDir), className + GroovyPageMetaInfo.LINENUMBERS_DATA_POSTFIX) + File lineNumbersDataFile = new File(new File(targetDir, packageDir), className + GroovyPageMetaInfo.LINENUMBERS_DATA_POSTFIX) gpp.writeLineNumbers(lineNumbersDataFile) // register viewuri -> classname mapping @@ -238,11 +237,9 @@ class GroovyPageCompiler { CompilationUnit unit = new CompilationUnit(compilerConfig, null, classLoader) unit.addPhaseOperation(operation, Phases.CANONICALIZATION) unit.addSource(gspgroovyfile.name, gsptarget.toString()) - // unit.addSource(gspgroovyfile) unit.compile() } - } - else { + } else { compileGSPResults[viewuri] = fullClassName } @@ -271,8 +268,7 @@ class GroovyPageCompiler { if (ch == '/') { nextMustBeStartChar = true sb.append(ch) - } - else { + } else { // package or class name cannot start with a number if (nextMustBeStartChar && !Character.isJavaIdentifierStart(ch)) { sb.append('_') diff --git a/grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GroovyPageParser.java b/grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GroovyPageParser.java index 2f7040d24a2..92b56720334 100644 --- a/grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GroovyPageParser.java +++ b/grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GroovyPageParser.java @@ -66,7 +66,10 @@ /** * NOTE: Based on work done by the GSP standalone project (https://gsp.dev.java.net/). *

- * Parsing implementation for GSP files + * Parsing implementation for GSP files. This class is responsible for parsing .gsp extension files + * and converting them to Groovy source code that extends the {@link GroovyPage} base class. It also gathers + * taglib references and html parts (contants with no modification) and writes them to a separate file. + * For improved debugging, line number references are also stored for easier exception tracing. * * @author Troy Heninger * @author Graeme Rocher @@ -643,7 +646,7 @@ private void htmlPartPrintlnToResponse(int partNumber) { } private void htmlPartPrintlnRaw(int partNumber) { - out.print("printHtmlPart("); + out.print("h("); out.print(String.valueOf(partNumber)); out.print(")"); out.println(); diff --git a/grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/gsp/GroovyPagesTemplateRenderer.java b/grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/gsp/GroovyPagesTemplateRenderer.java index f04ab985877..7c704b82ba9 100644 --- a/grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/gsp/GroovyPagesTemplateRenderer.java +++ b/grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/gsp/GroovyPagesTemplateRenderer.java @@ -88,8 +88,9 @@ public class GroovyPagesTemplateRenderer implements InitializingBean { public void afterPropertiesSet() throws Exception { if (scaffoldingTemplateGenerator != null) { // use reflection to locate method (would cause cyclic dependency otherwise) - generateViewMethod = ReflectionUtils.findMethod(scaffoldingTemplateGenerator.getClass(), "generateView", new Class[] { - GrailsDomainClass.class, String.class, Writer.class}); + generateViewMethod = ReflectionUtils.findMethod(scaffoldingTemplateGenerator.getClass(), "generateView", new Class[]{ + GrailsDomainClass.class, String.class, Writer.class + }); } } @@ -131,12 +132,12 @@ public void render(GrailsWebRequest webRequest, TemplateVariableBinding pageScop // required for binary compatibility: GRAILS-11598 private Template findAndCacheTemplate(Object controller, GrailsWebRequest webRequest, GroovyPageBinding pageScope, String templateName, - String contextPath, String pluginName, String uri) throws IOException { + String contextPath, String pluginName, String uri) throws IOException { return findAndCacheTemplate(controller, pageScope, templateName, contextPath, pluginName, uri); } private Template findAndCacheTemplate(Object controller, TemplateVariableBinding pageScope, String templateName, - String contextPath, String pluginName, final String uri) throws IOException { + String contextPath, String pluginName, final String uri) throws IOException { String templatePath = GrailsStringUtils.isNotEmpty(contextPath) ? GrailsResourceUtils.appendPiecesForUri(contextPath, templateName) : templateName; final GroovyPageScriptSource scriptSource; @@ -152,8 +153,7 @@ private Template findAndCacheTemplate(Object controller, TemplateVariableBinding } else { if (pluginName != null) { cacheKey = contextPath + pluginName + scriptSource.getURI(); - } - else { + } else { cacheKey = scriptSource.getURI(); } } @@ -204,7 +204,7 @@ protected Template updateValue(Template oldValue, Callable