Source

JSONExclusionMarshaller / JsonExclusionMarshallerGrailsPlugin.groovy

Full commit
import grails.converters.JSON
import groovy.lang.GroovyObject
import groovy.lang.GroovyClassLoader
import groovy.util.ConfigSlurper
import groovy.util.ConfigObject

import java.beans.PropertyDescriptor
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.lang.reflect.Modifier

import org.codehaus.groovy.grails.web.converters.exceptions.ConverterException
import org.codehaus.groovy.grails.web.converters.marshaller.ObjectMarshaller
import org.codehaus.groovy.grails.web.json.JSONWriter
import org.springframework.beans.BeanUtils


class JsonExclusionMarshallerGrailsPlugin {
    def version = "0.3"
    def grailsVersion = "2.0 > *"
    def pluginExcludes = [
        "grails-app/domain/**"
    ]

    def title = "JSON Exclusion Marshaller Plugin"
    def author = "Jason Stonebraker"
    def authorEmail = "jasonstonebraker@gmail.com"
    def description = '''Adds a convenience method to the Grails JSON converter for excluding properties from JSON output. Example use: excludeForPublicAPI(MyDomainClass, ['id', 'class']).'''

    def documentation = "https://bitbucket.org/stonebraker/jsonexclusionmarshaller/wiki/Home"

    def license = "APACHE"
    def issueManagement = [ system: "Bitbucket", url: "https://bitbucket.org/stonebraker/jsonexclusionmarshaller/issues" ]
    def scm = [ url: "https://bitbucket.org/stonebraker/jsonexclusionmarshaller/src" ]

    def doWithDynamicMethods = { ctx ->
        // TODO Implement registering dynamic methods to classes (optional)
        // println "Do with dynamic methods called..."

        JSON.metaClass.static.methodMissing = { String name, args ->
            def jsonConverter = delegate
            boolean isAnExclusionaryMethod = name.startsWith('excludeFor')

            if (!isAnExclusionaryMethod) {
                throw new MissingMethodException(name, JSON, args)
            }

            def impl = { Object[] vargs ->
                String configName = name // The name of the missing method called
                def type = vargs[0]
                def propertiesToExclude = vargs[1]

                jsonConverter.createNamedConfig(configName) {
                    it.registerObjectMarshaller(type) {
                        def o = it // Object being marshalled
                        def map = [:]

                        try {
                            PropertyDescriptor[] properties = BeanUtils.getPropertyDescriptors(o.getClass())
                            for (PropertyDescriptor property : properties) {
                                String propertyName = property.getName()
                                Method readMethod = property.getReadMethod()

                                if (readMethod != null
                                    && !(propertyName.equals("metaClass"))) {
                                    Object value = readMethod.invoke(o, (Object[]) null)
                                    map[propertyName] = value
                                }
                            }
                            
                            Field[] fields = o.getClass().getDeclaredFields()
                            for (Field field : fields) {
                                int modifiers = field.getModifiers()
                                if (Modifier.isPublic(modifiers)
                                    && !(Modifier.isStatic(modifiers)
                                        || Modifier.isTransient(modifiers))) {
                                    map[field.getName()] = field.get(o)
                                }
                            }

                        } catch (ConverterException ce) {
                            throw ce
                        } catch (Exception e) {
                            throw new ConverterException(
                                "Error converting Bean with class "
                                + o.getClass().getName(), e)
                        }

                        propertiesToExclude += this.getConfiguredGlobalExclusions(application)
                        propertiesToExclude.each { property -> map.remove(property) }
                        
                        return map
                    }
                }
            }

            JSON.metaClass."$name" = impl //future calls will use this

            return impl(args)
        }
    }

    private List getConfiguredGlobalExclusions(application) {
        GroovyClassLoader classLoader = new GroovyClassLoader(getClass().getClassLoader())
        ConfigObject config
        try {
           config = new ConfigSlurper().parse(classLoader.loadClass('JsonExclusionMarshallerDefaultConfig'))
        } catch (Exception e) {/*??handle or what? use default here?*/}
        
        def configGlobalExclusions = config.merge(application.config).jsonExclusionMarshaller.globalExclusions

        return (configGlobalExclusions) ? configGlobalExclusions.split(/,\s*/).collect { it } : []
    }
}