cawka
cawka

Reputation: 437

Defining global constraints in a Grails plugin

In Grails you can define global constraints in the project's Config.groovy file like this

grails.gorm.default.constraints = {
    myShared(nullable: false, blank: false)
}

and use them like this inside the domain

static constraints = {
    name(shared: "myShared")
}

Since our domain classes are reused in several Grails projects they are split up into plugins. Plugins' Config.groovy files are excluded so defining global constraints there would not work. Therefore I created a Constraints.groovy file which gets merged into the application's config inside the plugin descriptor of the plugin containing the domain classes. This works but I still get the following exception running the main project (grails run-app):

Caused by GrailsConfigurationException: Property [test.plugin.TestDomain.name] references shared constraint [myShared:null], which doesn't exist!

After some debugging in the Grails core I found out that the domain classes are already initialized with the shared constraints before the plugin descriptor is run.

public DefaultGrailsDomainClass(Class<?> clazz, Map<String, Object> defaultConstraints)

The map in the constructor contains the shared constraints. If I put the global constraints in the main project's Config.groovy file it contains the defined constraints and everything works fine. But if I merge them inside the plugin descriptor this map is empty and the exception gets thrown.

My question is if it is possible to somehow define global constraints inside a Grails plugin? Am I probably missing something? Copying the global constraints into every Grails project should not be the solution. Also a solution without the usage of another plugin to define constraints is preferred.

BTW we are using Grails 2.2.4.

Upvotes: 1

Views: 712

Answers (2)

cawka
cawka

Reputation: 437

I debugged a little further with Sérgio's answer in mind and came up with a solution that will probably work for some projects and for some it won't. Unfortunately our Grails project at work belongs to the latter group of projects... But first things first. Here is what I did.

I setup a completely empty Grails project and Grails plugin project. The plugin is included inline to reflect the situation we have in our Grails project at work.

I created a file Constraints.groovy in the conf directory of the plugin like described in the question.

grails.gorm.default.constraints = {
    myShared(nullable: false, blank: false)
}

To have these shared constraints available for testing inside the plugin I added this file to the config locations in Config.groovy.

grails.config.locations = [Constraints]

Now comes the part to make those constraints available for the main project. This is achieved by adding some lines to the plugin descriptor.

def loadBefore = ['domainClass']

As Sérgio suggested I changed the load order. Though I am not sure if this is actually necessary. More details on why I am unsure about that follow.

def doWithSpring = {
    ConstraintEvalUtils.clearDefaultConstraints()

    mergeConfig(application)
}

protected mergeConfig(application) {
    application.config.merge(loadConfig(application))
}

protected loadConfig(application) {
    new ConfigSlurper(Environment.current.name).parse(application.classLoader.loadClass("Constraints"))
}

The two methods are responsible for loading and merging the contents of Constraints.groovy into the application config. But what actually did the trick was invoking ConstraintEvalUtils.clearDefaultConstraints() before merging.

/**
 * Looks up the default configured constraints from the given configuration
 */
public static Map<String, Object> getDefaultConstraints(ConfigObject config) {
    def cid = System.identityHashCode(config)
    if (defaultConstraintsMap == null || configId != cid) {
        configId = cid
        def constraints = config?.grails?.gorm?.default?.constraints
        if (constraints instanceof Closure) {
            defaultConstraintsMap = new ClosureToMapPopulator().populate((Closure<?>) constraints);
        }
        else {
            defaultConstraintsMap = Collections.emptyMap()
        }
    }
    return defaultConstraintsMap
}

This method (also in ConstraintEvalUtils) gets called to load shared constraints. And as you can see the result gets cached in defaultConstraintsMap. So if the shared constraints were empty at the first invocation of this method it always returns an empty map. So I debugged this method to find out when this method actually gets called. It always returned an empty map. Even when I set loadBefore to core in the plugin descriptor the method got already called (more than once) before the plugin descriptor. As I already mentioned ConstraintEvalUtils.clearDefaultConstraints() worked as it cleans the cached constraints and the default constraints can be freshly loaded from the config that I merged my constraints into.

And that is pretty much it. Actually not many lines that need to be added. It works in my test project and probably for others as well but not in our Grails project at work. I will update my answer if I get it working there too.

Upvotes: 1

user800014
user800014

Reputation:

Since Grails initialize the constraints in the doWithSpring closure, I think that you cannot do it using a config file.

But if you look at DomainClassGrailsPlugin you have access to the config object.

def doWithSpring = {
  def config = application.config
  def defaultConstraintsMap = getDefaultConstraints(config)
  ...
}

So I think you can do something like (not tested)

def loadBefore = ['domainClass']

def doWithSpring = {
  def config = application.config
  config.grails.gorm.default.constraints = {
    myShared(nullable: false, blank: false)
  }
}

Upvotes: 1

Related Questions