Permalink
Browse files

Add dev mode feature for recording models for existing GSP pages

- this feature makes it convenient to migrate to use compile static GSPs
- usage:
  - make sure you commit any changes to vcs (git) before using the
    feature since the feature modifies GSP files directly
  - activate with '-Dgrails.views.gsp.modelrecording=true' system prop
    - can add temporarily to bootRun task's jvmArgs
  - start application
  - access the GSPs by using the application (integration tests etc.)
  - the changes will get written directly to GSP files at shutdown
    - debug info is written to System.err
  - use vcs (git) to compare the changes.
  • Loading branch information...
1 parent b07b3a8 commit cc60cdf3464c8f07d7f397c5f37d8df3f408e37c @lhotari lhotari committed Nov 18, 2016
@@ -295,8 +295,8 @@ protected Object lookupTagDispatcher(String namespace) {
return gspTagLibraryLookup != null ? gspTagLibraryLookup.lookupNamespaceDispatcher(namespace) : null;
}
- private JspTagLib lookupJspTagLib(String property) {
- String uri = (String) jspTags.get(property);
+ protected JspTagLib lookupJspTagLib(String jspTagLibName) {
+ String uri = (String) jspTags.get(jspTagLibName);
if (uri != null) {
TagLibraryResolver tagResolver = getTagLibraryResolver();
return tagResolver.resolveTagLibrary(uri);
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2016 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
+ *
+ * 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 org.grails.gsp
+
+import groovy.transform.CompileStatic
+import org.grails.core.lifecycle.ShutdownOperations
+import org.grails.gsp.compiler.GroovyPageParser
+import org.grails.gsp.jsp.JspTagLib
+import org.grails.taglib.encoder.OutputContext
+
+import java.util.concurrent.ConcurrentHashMap
+
+
+/**
+ * Development time helper class to add model definitions to existing GSP pages
+ *
+ * This adds a feature for migrating existing GSPs to type checked and staticly compiled GSPs.
+ * The model types are recorded during use and the model definition is written directly to the
+ * original GSP files at shutdown.
+ *
+ * Activate with '-Dgrails.views.gsp.modelrecording=true' system property
+ *
+ * @author Lari Hotari
+ * @since 3.3
+ */
+@CompileStatic
+abstract class ModelRecordingGroovyPage extends GroovyPage {
+ public static final String CONFIG_SYSTEM_PROPERTY_NAME
+ public static final boolean ENABLED
+ static {
+ CONFIG_SYSTEM_PROPERTY_NAME = "grails.views.gsp.modelrecording"
+ ENABLED = Boolean.getBoolean(CONFIG_SYSTEM_PROPERTY_NAME)
+ }
+ private static final ModelRecordingCache modelRecordingCache = new ModelRecordingCache()
+ private ModelEntry modelEntry
+
+ @Override
+ void initRun(Writer target, OutputContext outputContext, GroovyPageMetaInfo metaInfo) {
+ super.initRun(target, outputContext, metaInfo)
+ def key = getGroovyPageFileName()
+ modelEntry = modelRecordingCache.models.get(key)
+ if (modelEntry == null) {
+ modelEntry = new ModelEntry()
+ modelRecordingCache.models.put(key, modelEntry)
+ }
+ }
+
+ @Override
+ protected Object lookupTagDispatcher(String namespace) {
+ Object value = super.lookupTagDispatcher(namespace)
+ if (value != null) {
+ modelEntry.taglibs.add(namespace)
+ }
+ return value
+ }
+
+ @Override
+ protected JspTagLib lookupJspTagLib(String jspTagLibName) {
+ Object value = super.lookupJspTagLib(jspTagLibName)
+ if (value != null) {
+ modelEntry.taglibs.add(jspTagLibName)
+ }
+ return value
+ }
+
+ @Override
+ protected Object resolveProperty(String property) {
+ Object value = super.resolveProperty(property)
+ if (value != null) {
+ if (!modelEntry.model.containsKey(property) && !modelEntry.taglibs.contains(property)) {
+ Class valueClass = value.getClass()
+ if (valueClass.name.contains('$') || valueClass.isSynthetic()) {
+ valueClass = valueClass.superclass
+ }
+ if (value instanceof CharSequence) {
+ valueClass = CharSequence
+ }
+ modelEntry.model.put(property, valueClass.getName())
+ }
+ }
+ return value
+ }
+}
+
+@CompileStatic
+class ModelRecordingCache {
+ private Map<String, ModelEntry> models = new ConcurrentHashMap<>()
+ private boolean initialized
+
+ synchronized Map<String, ModelEntry> getModels() {
+ if (!initialized) {
+ initialize()
+ initialized = true
+ }
+ this.@models
+ }
+
+ private void initialize() {
+ System.err.println("Initialized model recording.")
+ ShutdownOperations.addOperation {
+ System.err.println("Writing model recordings to disk...")
+ try {
+ close()
+ } catch (e) {
+ e.printStackTrace(System.err)
+ } finally {
+ System.err.println("Done.")
+ }
+ }
+ }
+
+ void close() {
+ this.@models.each { String fileName, ModelEntry modelEntry ->
+ def gspDeclaration = modelEntry.gspDeclaration
+ if (gspDeclaration) {
+ File file = new File(fileName)
+ if (file.exists()) {
+ System.err.println("Writing model recordings to ${file.name}...")
+ file.text = gspDeclaration + file.text
+ } else {
+ System.err.println("GSP file '${fileName}' not found. Declaration: ${gspDeclaration}")
+ }
+ }
+ }
+ }
+}
+
+@CompileStatic
+class ModelEntry {
+ // defaults are defined by org.grails.web.taglib.WebRequestTemplateVariableBinding
+ static Map<String, String> DEFAULT_TYPES = [webRequest : 'org.grails.web.servlet.mvc.GrailsWebRequest',
+ request : 'javax.servlet.http.HttpServletRequest',
+ response : 'javax.servlet.http.HttpServletResponse',
+ flash : 'grails.web.mvc.FlashScope',
+ application : 'javax.servlet.ServletContext',
+ applicationContext: 'org.springframework.context.ApplicationContext',
+ grailsApplication : 'grails.core.GrailsApplication',
+ session : 'grails.web.servlet.mvc.GrailsHttpSession',
+ params : 'grails.web.servlet.mvc.GrailsParameterMap',
+ actionName : 'CharSequence',
+ controllerName : 'CharSequence']
+
+ Map<String, String> model = Collections.synchronizedMap([:])
+ Set<String> taglibs = Collections.synchronizedSet([] as Set)
+ Set<String> defaultTagLibs = new HashSet(GroovyPageParser.DEFAULT_TAGLIB_NAMESPACES)
+ int initialSize
+
+ ModelEntry() {
+ taglibs.addAll(defaultTagLibs)
+ initialSize = taglibs.size()
+ }
+
+ boolean hasTagLibs() {
+ taglibs.size() > initialSize
+ }
+
+ Iterable<String> getCustomTagLibs() {
+ taglibs.findAll { !defaultTagLibs.contains(it) }
+ }
+
+ String getGspDeclaration() {
+ if (model || hasTagLibs()) {
+ def gspDeclaration = new StringBuilder()
+ gspDeclaration << "@{"
+ if (model) {
+ gspDeclaration << " model='''\n"
+ model.each { String fieldName, String fieldType ->
+ String cleanedFieldType = fieldType - ~/^java\.(util|lang)\./
+ String defaultType = DEFAULT_TYPES.get(fieldName)
+ if(defaultType) {
+ try {
+ // use default field type for if field type is instance of the class
+ // for example instance of 'org.apache.catalina.core.ApplicationHttpRequest', use 'javax.servlet.http.HttpServletRequest'
+ Class<?> fieldTypeClass = Class.forName(fieldType)
+ Class<?> defaultTypeClass = Class.forName(defaultType)
+ if (defaultTypeClass.isAssignableFrom(fieldTypeClass)) {
+ cleanedFieldType = defaultType
+ }
+ } catch (e) {
+ // ignore
+ }
+ }
+ gspDeclaration << "${cleanedFieldType} ${fieldName}\n"
+ }
+ gspDeclaration << "''' "
+ }
+ if (hasTagLibs()) {
+ gspDeclaration << " taglibs='${customTagLibs.join(', ')}' "
+ }
+ gspDeclaration << "}\n"
+ return gspDeclaration.toString()
+ }
+ return null
+ }
+}
@@ -27,6 +27,7 @@
import org.grails.buffer.StreamCharBuffer;
import org.grails.gsp.CompileStaticGroovyPage;
import org.grails.gsp.GroovyPage;
+import org.grails.gsp.ModelRecordingGroovyPage;
import org.grails.gsp.compiler.tags.GrailsTagRegistry;
import org.grails.gsp.compiler.tags.GroovySyntaxTag;
import org.grails.io.support.SpringIOUtils;
@@ -81,7 +82,7 @@
public static final String MODEL_DIRECTIVE = "model";
public static final String COMPILE_STATIC_DIRECTIVE = "compileStatic";
public static final String TAGLIBS_DIRECTIVE = "taglibs";
- private static final List<String> DEFAULT_TAGLIB_NAMESPACES = Collections.unmodifiableList(Arrays.asList(new String[]{"g", "tmpl", "f", "asset", "plugin"}));
+ public static final List<String> DEFAULT_TAGLIB_NAMESPACES = Collections.unmodifiableList(Arrays.asList(new String[]{"g", "tmpl", "f", "asset", "plugin"}));
private GroovyPageScanner scan;
private GSPWriter out;
@@ -811,8 +812,7 @@ private void page() {
out.print("class ");
out.print(className);
out.print(" extends ");
- Class<?> gspSuperClass = isCompileStaticMode() ? CompileStaticGroovyPage.class : GroovyPage.class;
- out.print(gspSuperClass.getName());
+ out.print(resolveGspSuperClassName());
out.println(" {");
if(modelDirectiveValue != null) {
out.println("// start model fields");
@@ -956,6 +956,15 @@ private void page() {
}
}
+ private String resolveGspSuperClassName() {
+ Class<?> gspSuperClass = isCompileStaticMode() ? CompileStaticGroovyPage.class : (isModelRecordingModeEnabled() ? ModelRecordingGroovyPage.class : GroovyPage.class);
+ return gspSuperClass.getName();
+ }
+
+ private boolean isModelRecordingModeEnabled() {
+ return ModelRecordingGroovyPage.ENABLED;
+ }
+
/**
* Determines if the line numbers array should be added to the generated Groovy class.
* @return true if they should

0 comments on commit cc60cdf

Please sign in to comment.