Skip to content

Commit fd1a04b

Browse files
author
graeme
committed
fix for GRAILS-781, improved test coverage of auto-reloading for controllers, services and tag libraries
git-svn-id: https://svn.codehaus.org/grails/trunk@3177 1cfb16fd-6d17-0410-8ff1-b7e8e1e2867d
1 parent ff4f11e commit fd1a04b

File tree

10 files changed

+523
-74
lines changed

10 files changed

+523
-74
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/* Copyright 2004-2005 Graeme Rocher
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
package org.codehaus.groovy.grails.commons;
16+
17+
import groovy.lang.MetaClassRegistry;
18+
import groovy.lang.MetaClass;
19+
import groovy.lang.Closure;
20+
import groovy.lang.GroovyObject;
21+
import org.codehaus.groovy.runtime.InvokerHelper;
22+
import org.codehaus.groovy.grails.commons.metaclass.AdapterMetaClass;
23+
import org.codehaus.groovy.grails.commons.metaclass.ExpandoMetaClass;
24+
import org.codehaus.groovy.grails.commons.metaclass.ClosureInvokingMethod;
25+
import org.codehaus.groovy.grails.commons.metaclass.ThreadManagedMetaBeanProperty;
26+
import org.springframework.beans.BeanUtils;
27+
import org.apache.commons.logging.Log;
28+
import org.apache.commons.logging.LogFactory;
29+
30+
import java.util.List;
31+
import java.util.Iterator;
32+
import java.lang.reflect.Constructor;
33+
34+
/**
35+
* A class that provides utility methods for working with the Groovy MetaClass API
36+
*
37+
* @author Graeme Rocher
38+
* @since 0.5
39+
* <p/>
40+
*
41+
* Created: Feb 21, 2007
42+
* Time: 6:01:07 PM
43+
*/
44+
public class GrailsMetaClassUtils {
45+
private static final Log LOG = LogFactory.getLog(GrailsMetaClassUtils.class);
46+
47+
/**
48+
* Retrieves the MetaClassRegistry instance
49+
*
50+
* @return The registry
51+
*/
52+
public static MetaClassRegistry getRegistry() {
53+
return InvokerHelper.getInstance().getMetaRegistry();
54+
}
55+
56+
/**
57+
* Copies the ExpandoMetaClass dynamic methods and properties from one Class to another
58+
*
59+
* @param fromClass The source class
60+
* @param toClass The destination class
61+
* @param removeSource Whether to remove the source class after completion. True if yes
62+
*/
63+
public static void copyExpandoMetaClass(Class fromClass, Class toClass, boolean removeSource) {
64+
MetaClassRegistry registry = getRegistry();
65+
66+
MetaClass oldMetaClass = registry.getMetaClass(fromClass);
67+
68+
69+
AdapterMetaClass adapter = null;
70+
ExpandoMetaClass emc;
71+
72+
if(oldMetaClass instanceof AdapterMetaClass) {
73+
adapter = ((AdapterMetaClass)oldMetaClass);
74+
emc = (ExpandoMetaClass)adapter.getAdaptee();
75+
if(removeSource)
76+
registry.removeMetaClass(fromClass);
77+
}
78+
else {
79+
emc = (ExpandoMetaClass)oldMetaClass;
80+
}
81+
82+
List metaMethods = emc.getExpandoMethods();
83+
ExpandoMetaClass replacement = new ExpandoMetaClass(toClass);
84+
replacement.setAllowChangesAfterInit(true);
85+
for (Iterator i = metaMethods.iterator(); i.hasNext();) {
86+
Object obj = i.next();
87+
if(obj instanceof ClosureInvokingMethod) {
88+
ClosureInvokingMethod cim = (ClosureInvokingMethod) obj;
89+
Closure callable = cim.getClosure();
90+
if(!cim.isStatic()) {
91+
replacement.setProperty(cim.getName(), callable);
92+
}
93+
else {
94+
((GroovyObject)replacement.getProperty(ExpandoMetaClass.STATIC_QUALIFIER)).setProperty(cim.getName(),callable);
95+
}
96+
}
97+
}
98+
List metaProperties = emc.getExpandoProperties();
99+
for (Iterator i = metaProperties.iterator(); i.hasNext();) {
100+
Object o = i.next();
101+
if(o instanceof ThreadManagedMetaBeanProperty) {
102+
ThreadManagedMetaBeanProperty mbp = (ThreadManagedMetaBeanProperty)o;
103+
replacement.setProperty( mbp.getName(), mbp.getInitialValue() );
104+
}
105+
}
106+
replacement.initialize();
107+
if(adapter == null) {
108+
if(LOG.isDebugEnabled()) {
109+
LOG.debug("Adding MetaClass for class ["+toClass+"] MetaClass ["+replacement+"]");
110+
}
111+
registry.setMetaClass(toClass, replacement);
112+
}
113+
else {
114+
if(LOG.isDebugEnabled()) {
115+
LOG.debug("Adding MetaClass for class ["+toClass+"] MetaClass ["+replacement+"] with adapter ["+adapter+"]");
116+
}
117+
try {
118+
Constructor c = adapter.getClass().getConstructor(new Class[]{MetaClass.class});
119+
MetaClass newAdapter = (MetaClass) BeanUtils.instantiateClass(c,new Object[]{replacement});
120+
registry.setMetaClass(toClass,newAdapter);
121+
122+
} catch (NoSuchMethodException e) {
123+
// safe to ignore, plugin must take responsibility here
124+
}
125+
126+
}
127+
128+
}
129+
}

src/commons/org/codehaus/groovy/grails/plugins/DefaultGrailsPlugin.java

Lines changed: 3 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.codehaus.groovy.grails.commons.GrailsApplication;
2525
import org.codehaus.groovy.grails.commons.GrailsClassUtils;
2626
import org.codehaus.groovy.grails.commons.GrailsResourceUtils;
27+
import org.codehaus.groovy.grails.commons.GrailsMetaClassUtils;
2728
import org.codehaus.groovy.grails.commons.metaclass.AdapterMetaClass;
2829
import org.codehaus.groovy.grails.commons.metaclass.ClosureInvokingMethod;
2930
import org.codehaus.groovy.grails.commons.metaclass.ExpandoMetaClass;
@@ -454,7 +455,7 @@ private void checkForNewResources(final GrailsPlugin plugin) throws IOException
454455
}
455456
}
456457

457-
private void fireModifiedEvent(final Resource resource, final GrailsPlugin plugin) {
458+
protected void fireModifiedEvent(final Resource resource, final GrailsPlugin plugin) {
458459

459460
Class loadedClass = null;
460461
String className = GrailsResourceUtils.getClassName(resource);
@@ -481,67 +482,7 @@ private void fireModifiedEvent(final Resource resource, final GrailsPlugin plugi
481482
}
482483

483484
private void replaceExpandoMetaClass(Class loadedClass, Class oldClass) {
484-
MetaClass oldMetaClass = registry.getMetaClass(oldClass);
485-
486-
487-
AdapterMetaClass adapter = null;
488-
ExpandoMetaClass emc;
489-
490-
if(oldMetaClass instanceof AdapterMetaClass) {
491-
adapter = ((AdapterMetaClass)oldMetaClass);
492-
emc = (ExpandoMetaClass)adapter.getAdaptee();
493-
registry.removeMetaClass(oldClass);
494-
}
495-
else {
496-
emc = (ExpandoMetaClass)oldMetaClass;
497-
}
498-
499-
List metaMethods = emc.getExpandoMethods();
500-
ExpandoMetaClass replacement = new ExpandoMetaClass(loadedClass);
501-
replacement.setAllowChangesAfterInit(true);
502-
for (Iterator i = metaMethods.iterator(); i.hasNext();) {
503-
Object obj = i.next();
504-
if(obj instanceof ClosureInvokingMethod) {
505-
ClosureInvokingMethod cim = (ClosureInvokingMethod) obj;
506-
Closure callable = cim.getClosure();
507-
if(!cim.isStatic()) {
508-
replacement.setProperty(cim.getName(), callable);
509-
}
510-
else {
511-
((GroovyObject)replacement.getProperty(ExpandoMetaClass.STATIC_QUALIFIER)).setProperty(cim.getName(),callable);
512-
}
513-
}
514-
}
515-
List metaProperties = emc.getExpandoProperties();
516-
for (Iterator i = metaProperties.iterator(); i.hasNext();) {
517-
Object o = i.next();
518-
if(o instanceof ThreadManagedMetaBeanProperty) {
519-
ThreadManagedMetaBeanProperty mbp = (ThreadManagedMetaBeanProperty)o;
520-
replacement.setProperty( mbp.getName(), mbp.getInitialValue() );
521-
}
522-
}
523-
replacement.initialize();
524-
if(adapter == null) {
525-
if(LOG.isDebugEnabled()) {
526-
LOG.debug("Replacing reloaded class ["+loadedClass+"] MetaClass ["+replacement+"]");
527-
}
528-
registry.setMetaClass(loadedClass, replacement);
529-
}
530-
else {
531-
if(LOG.isDebugEnabled()) {
532-
LOG.debug("Replacing reloaded class ["+loadedClass+"] MetaClass ["+replacement+"] with adapter ["+adapter+"]");
533-
}
534-
try {
535-
Constructor c = adapter.getClass().getConstructor(new Class[]{MetaClass.class});
536-
MetaClass newAdapter = (MetaClass)BeanUtils.instantiateClass(c,new Object[]{replacement});
537-
registry.setMetaClass(loadedClass,newAdapter);
538-
539-
} catch (NoSuchMethodException e) {
540-
// safe to ignore, plugin must take responsibility here
541-
}
542-
543-
}
544-
485+
GrailsMetaClassUtils.copyExpandoMetaClass(oldClass, loadedClass, true);
545486
}
546487

547488
private Class attemptClassReload(final Resource resource) {

src/groovy/org/codehaus/groovy/grails/services/plugins/ServicesGrailsPlugin.groovy

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ class ServicesGrailsPlugin {
3333
def loadAfter = ['hibernate']
3434
def influences = ['controllers']
3535

36-
def watchedResources = "file:./grails-app/services/*Service.groovy"
36+
def watchedResources = ["file:./grails-app/services/*Service.groovy",
37+
"file:./plugins/*/grails-app/services/*Service.groovy"]
3738

3839

3940
def doWithSpring = {
@@ -70,11 +71,35 @@ class ServicesGrailsPlugin {
7071
def onChange = { event ->
7172
if(event.source) {
7273
def serviceClass = application.addServiceClass(event.source)
73-
if(serviceClass.transactional) {
74-
log.warn "Transactional services classes cannot be reloaded. Skipping ${serviceClass.fullName}."
74+
def serviceName = "${serviceClass.propertyName}"
75+
76+
if(serviceClass.transactional && event.ctx.containsBean("transactionManager")) {
77+
def beans = beans {
78+
"${serviceClass.fullName}ServiceClass"(MethodInvokingFactoryBean) {
79+
targetObject = ref("grailsApplication", true)
80+
targetMethod = "getGrailsServiceClass"
81+
arguments = serviceClass.fullName
82+
}
83+
def props = new Properties()
84+
props."*"="PROPAGATION_REQUIRED"
85+
"${serviceName}"(TransactionProxyFactoryBean) {
86+
target = { bean ->
87+
bean.factoryBean = "${serviceClass.fullName}ServiceClass"
88+
bean.factoryMethod = "newInstance"
89+
bean.autowire = "byName"
90+
}
91+
proxyTargetClass = true
92+
transactionAttributes = props
93+
transactionManager = ref("transactionManager")
94+
}
95+
}
96+
if(event.ctx) {
97+
event.ctx.registerBeanDefinition("${serviceClass.fullName}ServiceClass", beans.getBeanDefinition("${serviceClass.fullName}ServiceClass"))
98+
event.ctx.registerBeanDefinition(serviceName, beans.getBeanDefinition(serviceName))
99+
}
75100
}
76101
else {
77-
def serviceName = "${serviceClass.propertyName}"
102+
78103
def beans = beans {
79104
"$serviceName"(serviceClass.getClazz()) { bean ->
80105
bean.autowire = true
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package org.codehaus.groovy.grails.commons;
2+
3+
import org.codehaus.groovy.grails.commons.metaclass.*
4+
5+
import org.springframework.beans.BeanUtils
6+
7+
/**
8+
* Tests for the GrailsMetaClassUtils class
9+
10+
* @author Graeme Rocher
11+
**/
12+
public class GrailsMetaClassUtilsTests extends GroovyTestCase {
13+
14+
15+
void testGetMetaRegistry() {
16+
assertNotNull(GrailsMetaClassUtils.getRegistry())
17+
}
18+
19+
void testCopyExpandoMetaClass() {
20+
def metaClass = new ExpandoMetaClass(Dummy.class, true)
21+
22+
// add property
23+
metaClass.getFoo = {-> "bar" }
24+
// add instance method
25+
metaClass.foo = { String txt -> "bar:$txt" }
26+
// add static method
27+
metaClass.'static'.bar = {-> "foo" }
28+
// add constructor
29+
metaClass.'ctor' = { String txt ->
30+
def obj = BeanUtils.instantiateClass(Dummy.class)
31+
obj.name = txt
32+
obj
33+
}
34+
35+
metaClass.initialize()
36+
37+
def d = new Dummy("foo")
38+
assertEquals "foo", d.name
39+
assertEquals "bar", d.foo
40+
assertEquals "bar", d.getFoo()
41+
assertEquals "bar:1", d.foo("1")
42+
assertEquals "foo", Dummy.bar()
43+
44+
GrailsMetaClassUtils.copyExpandoMetaClass(Dummy.class, Dummy2.class, false)
45+
46+
d = new Dummy2("foo")
47+
assertEquals "foo", d.name
48+
assertEquals "bar", d.foo
49+
assertEquals "bar", d.getFoo()
50+
assertEquals "bar:1", d.foo("1")
51+
assertEquals "foo", Dummy.bar()
52+
}
53+
54+
}
55+
class Dummy {
56+
String name
57+
}
58+
class Dummy2 {
59+
String name
60+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package org.codehaus.groovy.grails.reload;
2+
3+
import org.codehaus.groovy.grails.web.servlet.mvc.*
4+
import org.codehaus.groovy.grails.commons.*
5+
import org.apache.commons.logging.*
6+
7+
/**
8+
* Tests for auto-reloading of controllers
9+
*
10+
* @author Graeme Rocher
11+
**/
12+
13+
class ControllerReloadTests extends AbstractGrailsControllerTests {
14+
15+
def reloadedController = '''
16+
class TestController {
17+
def testMe = {
18+
render "bar"
19+
}
20+
}
21+
'''
22+
void testReloadController() {
23+
runTest {
24+
def testController = ga.getController("TestController").newInstance()
25+
testController.testMe.call()
26+
27+
assertEquals "foo", testController.response.delegate.contentAsString
28+
29+
def newGcl = new GroovyClassLoader()
30+
def event = [source:newGcl.parseClass(reloadedController),
31+
ctx:appCtx]
32+
33+
def plugin = mockManager.getGrailsPlugin("controllers")
34+
35+
def eventHandler = plugin.instance.onChange
36+
eventHandler.delegate = [log: {false} as Log, application:ga]
37+
eventHandler.call(event)
38+
GrailsMetaClassUtils.copyExpandoMetaClass(testController.getClass(), event.source, true)
39+
40+
def newController = ga.getController("TestController").newInstance()
41+
42+
newController.testMe.call()
43+
44+
assertEquals "foobar", newController.response.delegate.contentAsString
45+
}
46+
}
47+
48+
void onSetUp() {
49+
gcl.parseClass(
50+
'''
51+
class TestController {
52+
def testMe = {
53+
render "foo"
54+
}
55+
}
56+
'''
57+
)
58+
}
59+
60+
}

0 commit comments

Comments
 (0)