Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Constraint API + concurrency fixes #18

Open
wants to merge 2 commits into from

2 participants

@samcday

Hi Geoff,

This patch is a little bigger than the other one I just submitted. This one focuses on the way we enhance the metaClass for the purposes of the "magic" stuff available in validate closure (params/hibernateTemplate/etc).

The issue is, the way it's currently implemented is inherently not thread-safe. This is because a single instance of DefaultGrailsConstraintClass (and the actual constraint class being wrapped) is shared across any number of applied constraints. Consider this example:

class AwesomeConstraint {
    def validate = { val ->
        return params == "foo"
    }
}

class FirstCommandObject {
    String blah

    static constraints = {
        blah(awesome: "foo")
    }
}

class SecondCommandObject {
    String blah

    static constraints = {
        blah(awesome: "bar")
    }
}

We would assume in this example that any time SecondCommandObject is used, it would be guaranteed to fail, however this is currently not the case. Instead, FirstCommandObject will sometimes sporadically fail, and SecondCommandObject will succeed. This is because the way setupConstraint was dishing out params and such was not thread safe.

So I've opted to implement the expected constraints validate api by stashing the CustomConstraintClass currently being executed in the request attributes of the current ServletRequest. The new api proxies requests for params/owningClass/propertyName/etc through the stashed CustomConstraintClass.

While I was doing this, I also refactored the api to be an interface, and the implementation will be applied to constraints using the Grails-y MetaClassEnhancer.

I also introduced another implementation of the ConstraintApi for test-mode, which will just proxy params and friends over to the test method that is currently executing. This means that accessing params/constraintOwningClass/etc will just access the same var that is injected into test classes via ConstraintsUnitTestMixin.

Please let me know if you have any questions!

-Sam

samcday added some commits
@samcday samcday introduced ConstraintApi, which is used by MetaClassEnhancer to provi…
…de the metaclass magic to validate closure. The RequestConstraintApi uses current web request to get a handle on the CustomConstraintClass being executed. This new functionality obseletes the manual metaclass stuff in ConstraintGrailsPlugin.setupConstaint, so it and its supporting stuff in CustomConstraintFactory + DefaultGrailsConstraintClass have been removed.
17dced6
@samcday samcday MockConstraintApi implemented, which simply delegates to a unit test …
…that has been injected with ConstraintUnitTestMixin. Aforementioned class updated to use this Api for enhancing constraint under test (CUT, hehe).
167d0ab
@geofflane
Owner

Thanks for this, I want to look into this a bit more to see if there's another way to fix this that doesn't involve Request state, etc.

@samcday

@geofflane Did you ever get a chance to review this? We've been using it in production for a while now and haven't had any issues. I agree it would be nice to avoid needing state tracking in ServletRequests, but I couldn't come up with a better way at the time. Would be interested to see if you come up with a better way. Let me know! :)

@geofflane
Owner

Sorry I haven't gotten to it yet. I started digging into this more tonight and I see what you're saying in that we only have one "params" value per Constraint instead of per "ConstrainedProperty". That currently really is a fatal flaw... I need to think on this a bit more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Apr 5, 2012
  1. @samcday

    introduced ConstraintApi, which is used by MetaClassEnhancer to provi…

    samcday authored
    …de the metaclass magic to validate closure. The RequestConstraintApi uses current web request to get a handle on the CustomConstraintClass being executed. This new functionality obseletes the manual metaclass stuff in ConstraintGrailsPlugin.setupConstaint, so it and its supporting stuff in CustomConstraintFactory + DefaultGrailsConstraintClass have been removed.
  2. @samcday

    MockConstraintApi implemented, which simply delegates to a unit test …

    samcday authored
    …that has been injected with ConstraintUnitTestMixin. Aforementioned class updated to use this Api for enhancing constraint under test (CUT, hehe).
This page is out of date. Refresh to see the latest.
View
39 ConstraintsGrailsPlugin.groovy
@@ -23,6 +23,8 @@ import net.zorched.constraints.UsPhoneConstraint
import net.zorched.constraints.SsnConstraint
import net.zorched.constraints.ComparisonConstraint
import net.zorched.grails.plugins.validation.CustomConstraintFactory
+import net.zorched.grails.plugins.validation.RequestConstraintApi
+import org.codehaus.groovy.grails.commons.metaclass.MetaClassEnhancer
class ConstraintsGrailsPlugin {
// the plugin version
@@ -82,12 +84,18 @@ class ConstraintsGrailsPlugin {
configureConstraintBeans.delegate = delegate
configureConstraintBeans(constraintClass)
}
+
+ "requestConstraintApiInstance" (RequestConstraintApi)
}
def doWithDynamicMethods = { applicationContext ->
- // TODO Implement registering dynamic methods to classes (optional)
+ MetaClassEnhancer enhancer = new MetaClassEnhancer()
+ RequestConstraintApi api = applicationContext.requestConstraintApiInstance
+ enhancer.addApi(api)
+
application.constraintClasses.each {constraintClass ->
- setupConstraintProperties(constraintClass)
+ MetaClass mc = constraintClass.clazz.metaClass
+ enhancer.enhance(mc)
registerConstraint.delegate = delegate
registerConstraint(constraintClass, false)
@@ -102,7 +110,12 @@ class ConstraintsGrailsPlugin {
if (application.isArtefactOfType(ConstraintArtefactHandler.TYPE, event.source)) {
def artefactClass = application.addArtefact(ConstraintArtefactHandler.TYPE, event.source)
- setupConstraintProperties(artefactClass)
+
+ MetaClass mc = artefactClass.clazz.metaClass
+
+ MetaClassEnhancer enhancer = new MetaClassEnhancer()
+ enhancer.addApi(event.ctx.requestConstraintApiInstance)
+ enhancer.enhance(mc)
}
}
@@ -126,26 +139,6 @@ class ConstraintsGrailsPlugin {
}
/**
- * Setup properties on the custom Constraints to make extra information available to them
- */
- def setupConstraintProperties = { constraintClass ->
- Object params = null
- Object hibernateTemplate = null
- Object constraintOwningClass = null
- String constraintPropertyName = null
- constraintClass.clazz.metaClass {
- setParams = {val -> params = val}
- getParams = {-> return params}
- setHibernateTemplate = {val -> hibernateTemplate = val}
- getHibernateTemplate = {-> return hibernateTemplate}
- setConstraintOwningClass = {val -> constraintOwningClass = val}
- getConstraintOwningClass = {-> return constraintOwningClass}
- setConstraintPropertyName = {val -> constraintPropertyName = val}
- getConstraintPropertyName = {-> return constraintPropertyName}
- }
- }
-
- /**
* Register the Custom constraint with ConstrainedProperty. Manages creating them with a CustomConstraintFactory
*/
def registerConstraint = { constraintClass, usingHibernate ->
View
8 src/groovy/net/zorched/grails/plugins/validation/ConstraintApi.groovy
@@ -0,0 +1,8 @@
+package net.zorched.grails.plugins.validation
+
+interface ConstraintApi {
+ Object getParams(Object instance)
+ Object getHibernateTemplate(Object instance)
+ Class getConstraintOwningClass(Object instance)
+ String getConstraintPropertyName(Object instance)
+}
View
18 src/groovy/net/zorched/grails/plugins/validation/ConstraintUnitTestMixin.groovy
@@ -2,10 +2,12 @@ package net.zorched.grails.plugins.validation
import grails.test.mixin.support.GrailsUnitTestMixin
import org.junit.After
+import org.codehaus.groovy.grails.commons.metaclass.MetaClassEnhancer
class ConstraintUnitTestMixin extends GrailsUnitTestMixin {
def params = [:]
- def veto = null
+ def constraintOwningClass = null
+ def constraintPropertyName = null
def <T> T testFor(Class<T> constraintClass) {
return this.mockConstraint(constraintClass)
@@ -13,18 +15,18 @@ class ConstraintUnitTestMixin extends GrailsUnitTestMixin {
def <T> T mockConstraint(Class<T> constraintClass) {
def instance = constraintClass.newInstance()
-
- constraintClass.metaClass.getParams = {-> params }
- constraintClass.metaClass.setParams = {newParams -> params = newParams }
- constraintClass.metaClass.getVeto = {-> veto }
- constraintClass.metaClass.setVeto = {newVeto -> veto = newVeto }
+
+ MetaClassEnhancer enhancer = new MetaClassEnhancer()
+ enhancer.addApi(new MockConstraintApi(this))
+ enhancer.enhance(constraintClass.metaClass)
return instance
}
@After
void resetFields() {
- params = [:]
- veto = null
+ this.params = [:]
+ this.constraintOwningClass = null
+ this.constraintPropertyName = null
}
}
View
33 src/groovy/net/zorched/grails/plugins/validation/MockConstraintApi.groovy
@@ -0,0 +1,33 @@
+package net.zorched.grails.plugins.validation
+
+/**
+ * The unit test implementation of ConstraintApi. Which just uses instance variables to implement the Api.
+ * TODO: is this safe? How many people out there are using multithreaded test runners?
+ */
+class MockConstraintApi implements ConstraintApi {
+ def owner
+
+ MockConstraintApi(owner) {
+ this.owner = owner
+ }
+
+ @Override
+ Object getParams(Object instance) {
+ return this.owner.params
+ }
+
+ @Override
+ Object getHibernateTemplate(Object instance) {
+ return null
+ }
+
+ @Override
+ Class getConstraintOwningClass(Object instance) {
+ return this.owner.constraintOwningClass ?: null
+ }
+
+ @Override
+ String getConstraintPropertyName(Object instance) {
+ return this.owner.constraintPropertyName ?: null
+ }
+}
View
46 src/groovy/net/zorched/grails/plugins/validation/RequestConstraintApi.groovy
@@ -0,0 +1,46 @@
+package net.zorched.grails.plugins.validation
+
+import org.codehaus.groovy.grails.commons.GrailsApplication
+import org.codehaus.groovy.grails.plugins.support.aware.GrailsApplicationAware
+import org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest
+import org.springframework.web.context.request.ServletRequestAttributes
+import org.springframework.web.context.request.RequestContextHolder
+import org.springframework.orm.hibernate3.HibernateTemplate
+import org.hibernate.SessionFactory
+
+/**
+ * The runtime implementation for ConstraintApi, which fetches information from the constraint stored in request attrs.
+ */
+class RequestConstraintApi implements ConstraintApi, GrailsApplicationAware {
+ public static String CONSTRAINT_REQUEST_ATTRIBUTE = "grails-constraints_constraint"
+
+ GrailsApplication grailsApplication
+
+ @Override
+ Object getParams(Object instance) {
+ return constraint.parameter
+ }
+
+ @Override
+ Object getHibernateTemplate(Object instance) {
+ if(grailsApplication.mainContext.containsBean("sessionFactory")) {
+ return new HibernateTemplate(grailsApplication.mainContext.sessionFactory, true)
+ }
+
+ return null
+ }
+
+ @Override
+ Class getConstraintOwningClass(Object instance) {
+ return constraint.constraintOwningClass
+ }
+
+ @Override
+ String getConstraintPropertyName(Object instance) {
+ return constraint.propertyName
+ }
+
+ CustomConstraintFactory.CustomConstraintClass getConstraint() {
+ return RequestContextHolder.currentRequestAttributes().getAttribute(CONSTRAINT_REQUEST_ATTRIBUTE, 0)
+ }
+}
View
13 src/java/net/zorched/grails/plugins/validation/CustomConstraintFactory.java
@@ -22,6 +22,7 @@
import org.springframework.context.ApplicationContext;
import org.springframework.util.ReflectionUtils;
import org.springframework.validation.Errors;
+import org.springframework.web.context.request.RequestContextHolder;
import java.lang.reflect.Method;
@@ -75,13 +76,7 @@ protected void processValidate(Object target, Object propertyValue, Errors error
if (validationParamCount > 2)
params.add(errors);
- // Setup parameters needed by constraints
- constraint.setParameter(constraintParameter);
- constraint.setConstraintOwningClass(constraintOwningClass);
- constraint.setConstraintPropertyName(constraintPropertyName);
- if (constraint.isPersistent()) {
- constraint.setHibernateTemplate(applicationContext);
- }
+ RequestContextHolder.currentRequestAttributes().setAttribute(RequestConstraintApi.CONSTRAINT_REQUEST_ATTRIBUTE, this, 0);
// Inject dependencies
applicationContext.getAutowireCapableBeanFactory().autowireBeanProperties(constraint.getReferenceInstance(), AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false);
@@ -149,6 +144,10 @@ protected boolean skipBlankValues() {
protected boolean skipNullValues() {
return constraint.skipNullValues();
}
+
+ public Class getConstraintOwningClass() {
+ return this.constraintOwningClass;
+ }
public void setParameter(Object constraintParameter) {
constraint.validateParams(constraintParameter, constraintPropertyName, constraintOwningClass);
View
36 src/java/net/zorched/grails/plugins/validation/DefaultGrailsConstraintClass.java
@@ -79,42 +79,6 @@ public int getValidationPropertyCount() {
return c.getMaximumNumberOfParameters();
}
- /**
- * The value that was passed to the constraint parameter. This will be available as
- * 'params' in a constraint.
- * @param constraintParameter The value to make available in params
- */
- public void setParameter(Object constraintParameter) {
- getMetaClass().invokeMethod(getReferenceInstance(), "setParams", constraintParameter);
- }
-
- /**
- * Make the hibernateTemplate available to persisent constraints
- * @param applicationContext The application context used to lookup the hibernate SessionFactory
- */
- public void setHibernateTemplate(ApplicationContext applicationContext) {
- if (applicationContext.containsBean("sessionFactory")) {
- HibernateTemplate template = new HibernateTemplate((SessionFactory) applicationContext.getBean("sessionFactory"),true);
- getMetaClass().invokeMethod(getReferenceInstance(), "setHibernateTemplate", template);
- }
- }
-
- /**
- * Make the owning class available to persistent constraints
- * @param owningClass The class the constraint is applied to.
- */
- public void setConstraintOwningClass(Object owningClass) {
- getMetaClass().invokeMethod(getReferenceInstance(), "setConstraintOwningClass", owningClass);
- }
-
- /**
- * Make the property the constraint was applied to available to persistent constraints
- * @param constrainedPropertyName The property the constraint is applied to.
- */
- public void setConstraintPropertyName(String constrainedPropertyName) {
- getMetaClass().invokeMethod(getReferenceInstance(), "setConstraintPropertyName", constrainedPropertyName);
- }
-
public String getName() {
return getConstraintName();
}
Something went wrong with that request. Please try again.