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...
lhotari committed Nov 18, 2016
1 parent b07b3a8 commit cc60cdf3464c8f07d7f397c5f37d8df3f408e37c
@@ -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.