I am trying to find a way to add dynamic fields to a grails domain class. I did find the dynamic domain class plugin based on Burt's article, but this is way too much for our needs.
Supposed we have a domain class of person:
class Person extends DynamicExtendableDomainObject {
String firstName
String lastName
static constraints = {
firstName(nullable: false, blank: false, maxSize: 50)
lastName(nullable: false, blank: false)
}
}
Now customer a wants to also have a birthdate field in this. By using some sort of management tool, he adds this extra field in the database.
Customer b wants to also have a field middle name, so he is adding the field middle name to the person.
Now we implemented a DynamicExtendableDomainObject class, which the Person class inherits from. This adds a custom field to each Domain class inheriting from this to store the dynamic properties as JSON in it (kind of like KiokuDB in Perl stores them).
Now when Person is instantiated, we would like to add those dynamic properties to the Person class, to be able to use the standard Grails getter and setter as well as Templating functions for those.
So on customer a we could use the scaffolding and person would output firstName, lastName, birthDate, on customer b the scaffolding would output firstName, lastName, middleName.
The storing of the properties will be implemented by using the saveinterceptor, to serialize those properties to JSON and store them in the special field.
But we have not yet found a way to add these JSON properties dynamically to the domain class during runtime. Is there a good way to handle this? And if so, how to best implement this?
You can try to add the properties at runtime to the DomainClass of type DynamicExtendableDomainObject by expanding getProperty(), setProperty(), setProperties() in the metaClass and then use beforeUpdate(), beforeInsert() and afterLoad() to hook into Persistence.
For example in Bootstrap (or service):
def yourDynamicFieldDefinitionService
for(GrailsClass c in grailsApplication.getDomainClasses()){
if(DynamicExtendableDomainObject.isAssignableFrom(c.clazz)){
Set extendedFields = yourDynamicFieldDefinitionService.getFieldsFor(c.clazz)
//getProperty()
c.clazz.metaClass.getProperty = { String propertyName ->
def result
if(extendedFields.contains(propertyName)){
result = delegate.getExtendedField(propertyName)
} else {
def metaProperty = c.clazz.metaClass.getMetaProperty(propertyName)
if(metaProperty) result = metaProperty.getProperty(delegate)
}
result
}
//setProperty()
c.clazz.metaClass.setProperty = { propertyName , propertyValue ->
if(extendedFields.contains(propertyName)){
delegate.setExtendedField(propertyName, propertyValue)
delegate.blobVersionNumber += 1
} else {
def metaProperty = c.clazz.metaClass.getMetaProperty(propertyName)
if(metaProperty) metaProperty.setProperty(delegate, propertyValue)
}
}
//setProperties()
def origSetProperties = c.clazz.metaClass.getMetaMethod('setProperties',List)
c.clazz.metaClass.setProperties = { def properties ->
for(String fieldName in extendedFields){
if(properties."${fieldName}"){
delegate."${fieldName}" = properties."${fieldName}"
}
}
origSetProperties.invoke(delegate,properties)
}
}
}
with
abstract DynamicExtendableDomainObject {
String yourBlobField
Long blobVersionNumber //field to signal hibernate that the instance is 'dirty'
Object getExtendedField(String fieldName){
...
}
void setExtendedField(String fieldName, Object value){
...
}
def afterLoad(){
//fill your transient storage to support getExtendedField + setExtendedField
}
def beforeUpdate(){
//serialize your transient storage to yourBlobField
}
def beforeInsert(){
//serialize your transient storage to yourBlobField
}
}
Related
I am using Grails 2.5 and use Grails databinding in request methods.
For a basic example of the situation consider the following:
Domain class
class Product {
String field1
String privateField
}
Controller
class ProductController {
def update(Product productInstance) {
productInstance.save()
}
}
If I pass an existing Product to the controller like
{"id":3, "privateField":"newValue","field1":"whatever"}
the old value of privateField is overwritten. I want to enforce, that privateField is never bound from a request and avoid checking if the field is dirty.
Is there a mechanism in Grails to achieve this?
If I have to do the dirty check, how can I discard the new value and use the old one?
Pretty sure there's a "bindable" constraint.
http://grails.github.io/grails-doc/2.5.x/ref/Constraints/bindable.html
class Product {
String field1
String privateField
static constraints = {
privateField bindable: false
}
}
Should keep that field from binding automatically.
You can enforce which values are bound, but you'll need to change your method signature to get more control of the data binding process.
class ProductController {
def update() {
def productInstance = Product.get(params.id)
bindData(productInstance, params, [exclude: ['privateField']]
productInstance.save()
}
}
I have three classes in grails application
class Category {
String name
}
class Application {
String name
static hasMany =[specialCategoryies:SpecialCategory]
}
class SpecialCategory {
Category category
Integer points
static belongsTo =[application:Application]
}
Here while I am saving the applicationInstance I don't want save duplicate
specialCategories values like ..specialCategories does not have same
category value again ..
application.addToSpecialCategoryies(newSpecialCategory(category:Category.get(1),points:2))
application.addToSpecialCategoryies(newSpecialCategory(category:Category.get(1),points:3))
here i application instance should rise error that category value repeated..
so how define constraints for hasMany properties in domain class......?
suggest how to write constraints to avoid duplicate values of category
You might try using a custom validator in your Application constraints section. For example, one way to check for duplicate property values is to collect the values into an array and compare them to the corresponding unique array (with duplicated elements removed):
class Application {
String name
static hasMany =[specialCategoryies:SpecialCategory]
static constraints = {
specialCategoryies validator: { specialCategories, obj ->
def specialCategoriesIdArray = specialCategories.collect {it?.category?.getId()}
return (specialCategoriesIdArray.size() == specialCategoriesIdArray.unique().size())?true:'application.validator.specialcategoryduplicate.error'
}
}
}
When trying to save a special category with an existing category, it will throw a validation error when saving. You can test with the following:
def cat1 = new Category(name:"Cat 1").save(flush:true)
def cat2 = new Category(name:"Cat 2").save(flush:true)
def app = new Application()
app.name = "Test"
app.addToSpecialCategoryies(new SpecialCategory(category: Category.get(1), points:2))
app.addToSpecialCategoryies(new SpecialCategory(category: Category.get(2), points:2))
app.addToSpecialCategoryies(new SpecialCategory(category: Category.get(1), points:3))
if ( app.save(flush:true) ){
log.info "Saved!"
} else {
log.error "NOT Saved. Error:"
app.errors.each {
log.error it
}
}
I'm having trouble getting saves cascaded down my object hierarchy. Below is the code of my object hierarchy.
class Entity {
static hasMany = [attributes: Attribute]
}
class Attribute extends ValuePossessor {
static belongsTo = Entity
}
abstract class ValuePossessor {
def valueService
Value value
void setValue(val) {
this.value = valueService.Create(val)
this.value.possessor = this
}
}
abstract class Value {
static belongsTo = [possessor: ValuePossessor]
}
class StringValue extends Value {
String value
}
The valueService is simply a service with a big switch statement that creates the correct value type (string, boolean, int, etc.).
Entity e = new Entity()
Attribute attr = new Attribute()
attr.setValue(1)
e.addToAttributes(attr)
e.save()
The above code correctly creates all objects, but fails to save the value object. The entity and attribute are saved, but the value is not. Am I missing some identifier needed to cascade all the way down to the value object?
Figured this out. Apparently there is some magic in the grails dynamic setters. I changed the setValue(val) method to set(val) and it started working. Lesson learned: don't override grails' dynamically added methods because they are built with magic, pixy dust, and unicorn urine.
I have a restriction so there could be no more than
ConfigurationHolder.config.support.reminder.web.person.max object stored.
I didn't find how to add a validator which doesn't relate on particular property. So for now I implemented it in this way. Do you guys have any ideas how to make it better?
package support.reminder.web
import org.codehaus.groovy.grails.commons.ConfigurationHolder;
class Person {
String firstName
String lastName
String email
Date lastDutyDate
static constraints = {
firstName(blank: false)
lastName(blank: false)
email(blank: false, email: true)
lastDutyDate(nullable: true)
id validator: {val ->
if (val)
Person.count() <= ConfigurationHolder.config.support.reminder.web.person.max
else
Person.count() < ConfigurationHolder.config.support.reminder.web.person.max
}
}
String toString() {
"[$firstName $lastName, $email, $lastDutyDate]"
}
}
You can use the Grails Custom Constraints Plugin to manage your validation implementation. Then you can call your own constraint just like the predefined Grails constraints:
package support.reminder.web
import org.codehaus.groovy.grails.commons.ConfigurationHolder as CH
class Person {
String firstName
String lastName
String email
Date lastDutyDate
static constraints = {
firstName(blank: false)
lastName(blank: false)
email(blank: false, email: true)
lastDutyDate(nullable: true)
id(maxRows: CH.config.support.reminder.web.person.max)
}
}
Alternatively, if you don't want to rely on 3rd Party Plugins you can implement the logic of your custom validator within a Service method but call it from the custom validator in the Domain:
package support.reminder.web
import org.codehaus.groovy.grails.commons.ConfigurationHolder as CH
class Person {
def validationService
String firstName
String lastName
String email
Date lastDutyDate
static constraints = {
firstName(blank: false)
lastName(blank: false)
email(blank: false, email: true)
lastDutyDate(nullable: true)
id (validator: {val ->
validationService.validateMaxRows(val, CH.config.support.reminder.web.person.max)
}
}
}
I don't have a better idea, but I do suggest that maybe you need to check for < not <=. I think that when validating your object, it has not yet been stored in the DB, so it will not be included in Person.count(). I reckon that <= would cause it to pass the validation then be saved, then you would be violating the rule.
I recommend you using a service function, like personService.addPerson(). Then check the constraint before you save a new object. It will benefit if you get more complicated constraint, such as when it related many domain objects, for example.
The use of a validator for restricting the number of object is actually not very good if concerning about the meaning of validator: the object is valid, only the number of objects is too large.
In short: the logical code goes to the service.
I am developing a grails application.In that some cases I want to control the domain class fields based on the role.So that in each call to getter setter method of domain class I want to apply some filter based on role(Logged in user's role).I am assuming that grails will create getter setter method at runtime for the domin classes.So while writing grails code is it possible to apply this logic.If it is possible then how to apply?
Example:
Domain Class :
class Book{
String name;
double price;
}
Controller:
def index={
Book book=Book.get(1);
println book.name;
println book.price;
}
In the above code "println book.price;" this line should work only for particular role.For some other role it should throw some exception.
Is it possible achieve?Is there any plugin to do this?
Please give some help on this....Thanks
You can create get/set methods for the properties you want to control access to and put your security logic there. Assuming you've written your own security service or are using a security plugin like the Spring Security (Acegi) plugin you would:
class Book{
String name;
double price;
def authenticateService
void setPrice(double price) {
if(!authenticateService.ifAllGranted('ROLE_PRICE_FIXER')) {
throw new Exception("You are not authorized to set book prices")
}
this.price = price
}
double getPrice() {
if(!authenticateService.ifAllGranted('ROLE_PRICE_FIXER')) {
throw new Exception("You are not authorized to get book prices")
}
return this.price
}
}
I am not aware of any plugin that allows access controls to be put on domain properties.
You could also consider using a custom validator or a spring errors object to catch attempts to set a field before saving it.
EDIT: Here is an example of what I was thinking. You could generalize quite a bit more and the code here hasn't been tested so it probably won't run as is.
class securedDomain {
String securedField
def fieldSetBy = [:]
def previousValue = [:]
static transients = ['fieldSetBy', 'previousValue']
static constraints = {
securedField(validator: { v, o ->
def access = User.findByName(fieldSetBy['securedField']).hasAccess('securedField')
if(!access) securedField = previousValue['securedField']
return access
})
void setProperty(String name, value) {
if(name == "securedField") {
fieldSetBy['securedField'] = session.user
previousValue['securedField'] = securedField
securedField = value
} else {
super(name, value)
}
}