[MAGNOLIA-6378] Allow yaml based configuration to make use of groovy Created: 16/Sep/15  Updated: 23/Feb/16  Resolved: 26/Jan/16

Status: Closed
Project: Magnolia
Component/s: configuration
Affects Version/s: 5.4.2
Fix Version/s: 5.4.5

Type: Improvement Priority: Major
Reporter: Richard Gange Assignee: Federico Grilli
Resolution: Fixed Votes: 0
Labels: pm
Remaining Estimate: 0d
Time Spent: 0.5d
Original Estimate: Not Specified

Attachments: Java Source File Map2BeanTransformer.java     File magnolia-configuration-groovymodel-patch-5.4.3.jar    
Issue Links:
Relates
relates to MGNLRES-210 Groovy as resources: Allow groovy in ... Closed
relation
is related to MGNLGROOVY-68 Changes to a Groovy class in config a... Closed
Template:
Acceptance criteria:
Empty
Task DoD:
[ ]* Doc/release notes changes? Comment present?
[ ]* Downstream builds green?
[ ]* Solution information and context easily available?
[ ]* Tests
[ ]* FixVersion filled and not yet released
[ ]  Architecture Decision Record (ADR)
Date of First Response:
Epic Link: Light Development 1.0
Sprint: Basel 28
Story Points: 5

 Description   

I am not able to use groovy model classes in yaml based templates definitions.

2015-09-15 12:31:42,701 WARN  agnolia.config.source.yaml.YamlConfigurationSource: Problem while registering TEMPLATE from LayeredResource{path='/workshop-module/templates/pages/hello.yaml', layeredResources=[FileSystemResource{origin=filesystem,path=/workshop-module/templates/pages/hello.yaml,file}]}: Conversion: Error converting from 'String' to 'Class' info.magnolia.groovy.models.HelloModel
org.apache.commons.beanutils.ConversionException: Error converting from 'String' to 'Class' info.magnolia.groovy.models.HelloModel
	at org.apache.commons.beanutils.converters.AbstractConverter.handleError(AbstractConverter.java:282)
	at org.apache.commons.beanutils.converters.AbstractConverter.convert(AbstractConverter.java:177)
	at org.apache.commons.beanutils.converters.ConverterFacade.convert(ConverterFacade.java:61)
	at org.apache.commons.beanutils.ConvertUtilsBean.convert(ConvertUtilsBean.java:491)
	at org.apache.commons.beanutils.BeanUtilsBean.setProperty(BeanUtilsBean.java:1000)
	at info.magnolia.config.map2bean.Map2BeanTransformer.readOutObject(Map2BeanTransformer.java:184)
	at info.magnolia.config.map2bean.Map2BeanTransformer.readComplexValue(Map2BeanTransformer.java:151)
	at info.magnolia.config.map2bean.Map2BeanTransformer.readValue(Map2BeanTransformer.java:114)
	at info.magnolia.config.map2bean.Map2BeanTransformer.toBean(Map2BeanTransformer.java:101)
	at info.magnolia.config.source.yaml.YamlConfigurationSource.loadAndRegister(YamlConfigurationSource.java:94)
	at info.magnolia.config.source.yaml.AbstractFileResourceConfigurationSource$LoadAndRegisterFunction.doWith(AbstractFileResourceConfigurationSource.java:132)
	at info.magnolia.config.source.yaml.AbstractFileResourceConfigurationSource$LoadAndRegisterFunction.doWith(AbstractFileResourceConfigurationSource.java:113)
	at info.magnolia.resourceloader.util.VoidFunction.apply(VoidFunction.java:49)
	at info.magnolia.resourceloader.util.VoidFunction.apply(VoidFunction.java:46)
	at info.magnolia.resourceloader.util.PredicatedResourceVisitor.visitFile(PredicatedResourceVisitor.java:117)
	at info.magnolia.resourceloader.layered.RelayerResourceVisitor.visitFile(RelayerResourceVisitor.java:61)
	at info.magnolia.resourceloader.file.VisitorFunction.apply(VisitorFunction.java:58)
	at info.magnolia.resourceloader.file.VisitorFunction.apply(VisitorFunction.java:46)
	at info.magnolia.resourceloader.file.FileWatcherCallback.doIt(FileWatcherCallback.java:78)
	at info.magnolia.resourceloader.file.FileWatcherCallback.modified(FileWatcherCallback.java:66)
	at info.magnolia.dirwatch.DirectoryWatcher.processEvent(DirectoryWatcher.java:247)
	at info.magnolia.dirwatch.DirectoryWatcher.run(DirectoryWatcher.java:199)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
	at java.util.concurrent.FutureTask.run(FutureTask.java:262)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
	at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.ClassNotFoundException: info.magnolia.groovy.models.HelloModel
	at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1858)
	at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1709)
	at org.apache.commons.beanutils.converters.ClassConverter.convertToType(ClassConverter.java:104)
	at org.apache.commons.beanutils.converters.AbstractConverter.convert(AbstractConverter.java:169)
	... 25 more

CAUSE AND PROPOSED SOLUTION

Basically in Map2Bean resolving model classes is delegated to BeanUtils which uses its default class converter org.apache.commons.beanutils.converters.ClassConverter. Code eventually ends up at ClassConverter.convertToType(..) where the context class loader for the current thread is not aware of Groovy classes and thus fails. One way to fix this could be adding a check at Map2BeanTransformer.readSimpleValue(Object, TypeDescriptor)

 private <T> T readSimpleValue(Object source, TypeDescriptor defaultTargetTypeDescriptor) throws ClassNotFoundException {
        // we need to convert classes here else org.apache.commons.beanutils.converters.ClassConverter (which isn't aware of Groovy classes) would be used and an exception thrown, e.g. in case of a Groovy class model. Basically we need to do here something similar to what is done at Node2BeanTransformerImpl.convertPropertyValue(..)  
        if (defaultTargetTypeDescriptor.getType().isAssignableFrom(Class.class)) {
            return (T) Classes.getClassFactory().forName((String) source);
        }
        return (T) source;
    }

One problem I see with this approach is that we throw CNFE which eventually must be thrown by the public API method Map2BeanTransformer.toBean(..) meaning not being binary compatible (unless we catch the exception and just log it or rethrow it wrapped in some exception already thrown by toBean(..))



 Comments   
Comment by Richard Gange [ 16/Sep/15 ]

Looking at the code it appears that MgnlGroovyClassLoader is never checked, therefore the class isn't found.

Comment by Philipp Bärfuss [ 10/Dec/15 ]

First it should work. We support Groovy models in JCR configuration or lest say Groovy classes in general. The code transforming YAML to configuration beans need to respect that. I don't know how this is exactly done in Node2Bean but the same approach should be used.

Comment by Richard Gange [ 10/Dec/15 ]

My "patched" version of Map2Bean transformer.

What I'm doing is looking for properties which come in with the name modelClass in the function readOutObject()

private <T> T readOutObject(Map<String, Object> sourceMap, TypeDescriptor targetType) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, ConfigurationParsingException {
        T targetObj = (T) componentProvider.newInstance(targetType.getType());

        Map<String, String> targetDescription = beanUtils.describe(targetObj);

        for (Map.Entry<String, Object> sourceEntry : sourceMap.entrySet()) {
            String sourcePropertyName = sourceEntry.getKey();
            Object sourcePropertyValue = sourceEntry.getValue();

            if ("class".equals(sourcePropertyName)) {
                continue; // skip class property because its only meta data
            }

            if (!targetDescription.containsKey(sourcePropertyName)) {
                // We should not just ignore this - see MAGNOLIA-6196
                log.warn("Property " + sourcePropertyName + " not found in class " + targetObj.getClass());
                continue;
            }

            PropertyTypeDescriptor propertyTypeDescriptor = mapping.getPropertyTypeDescriptor(targetType.getType(), sourcePropertyName);

            Object value;
            if (propertyTypeDescriptor.isCollection()) {
                value = readOutList(prepareListValue(sourcePropertyValue), propertyTypeDescriptor.getCollectionEntryType());
            } else if (propertyTypeDescriptor.isMap()) {
                value = readOutMap((Map<String, Object>) sourcePropertyValue, propertyTypeDescriptor.getCollectionEntryType());
            } else {
                value = readValue(sourcePropertyValue, propertyTypeDescriptor.getType());
            }

            if ("modelClass".equals(sourcePropertyName)) {
                try {
                    value = Classes.getClassFactory().forName((String) value);
                } catch (ClassNotFoundException e) {
                    log.error("Can't convert property. Class for type [{}] not found.", value);
                    throw new ConfigurationParsingException(e.getMessage(), e);
                }
            }
            
            beanUtils.setProperty(targetObj, sourcePropertyName, value);
        }

        return targetObj;
    }
Comment by Richard Gange [ 10/Dec/15 ]

If you'd like to use groovy models with YAML templates then you can replace magnolia-configuration-5.4.3.jar with magnolia-configuration-groovymodel-patch-5.4.3.jar for the time being. Until we implement a real fix. I'm not quite sure my approach was the best but I was under a bit of a time crunch getting it ready for the LDW. The funny thing is here, Map2Bean and Node2Bean are soooo different.

Comment by Philipp Bärfuss [ 14/Dec/15 ]

The Content2BeanTranformer. convertPropertyValue() method handles this for JCR by using our Classes.getClassFactory().forName(name). The Groovy module registers its own GroovyClassFactory which delegates to the our MgnlGroovyClassLoader.

Since any class can be a groovy class the solution should not be tied to a test on modelClass only.

Anyway if this is fixed the problem of stale classes upon script changes remains: MGNLGROOVY-68

Generated at Mon Feb 12 04:13:57 CET 2024 using Jira 9.4.2#940002-sha1:46d1a51de284217efdcb32434eab47a99af2938b.