Configuring Groovy MarkupTemplateEngine in Spring Boot leads to Cast Exception - spring

I am trying to configure a custom Template class for the MarkupTemplateEngine in Spring Boot, which allows writing templates in Groovy. My configuration is pretty simple:
#Configuration
class TemplateConfiguration {
#Bean
public GroovyMarkupConfig groovyMarkupConfigurer() {
new GroovyMarkupConfigurer().tap {
resourceLoaderPath = 'classpath:/templates/'
baseTemplateClass = MainTemplate
}
}
}
When changing the resourceLoaderPath to a non-existent path I get an 404 error, which reveals, that that configuration is loaded properly. However, when using setBaseTemplateClass(Class<? extends BaseTemplate>) as in the snippet above, I get the following error:
org.codehaus.groovy.runtime.typehandling.GroovyCastException:
Cannot cast object
'org.springframework.web.servlet.view.groovy.GroovyMarkupConfigurer#111bb71a'
with class 'org.springframework.web.servlet.view.groovy.GroovyMarkupConfigurer'
to class 'TemplateConfiguration'
I have no clue, why this error comes up. My custom Template class looks like this:
abstract class MainTemplate extends BaseTemplate {
MainTemplate(MarkupTemplateEngine templateEngine, Map model, Map<String, String> modelTypes,
TemplateConfiguration configuration) {
super(templateEngine, model, modelTypes, configuration)
}
void doctype() {
yieldUnescaped '<!DOCTYPE html>'
}
}

Letting the Configuration extend GroovyMarkupConfigurer and overwriting the properties solved the issue:
#Configuration
class TemplateConfiguration extends GroovyMarkupConfigurer {
final String resourceLoaderPath = 'classpath:/templates/'
final Class<? extends BaseTemplate> baseTemplateClass = MainTemplate
}

Related

Testing #ConfigurationProperties with ApplicationContextRunner

I have several #Configuration classes using the same set of properties, which are defined under a specific #Component class also tagged with #ConfigurationProperties.
When I try to test one of my config using the ApplicationContextRunner, and change a property, the update is not reflected in the Properties class.
class CustomConfigurationTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withBean(CustomProperties.class)
.withUserConfiguration(CustomConfiguration.class);
#Test
void testDefaultBeanCreation() {
contextRunner.run(context -> {
assertThat(context.containsBean("myBean")).isTrue();
assertThat(context.getBean("myBean")).isInstanceOf(DefaultBean.class);
});
}
#Test
void testCustomBeanCreation() {
contextRunner
.withPropertyValues("custom.enabled=true")
.run(context -> {
assertThat(context.containsBean("myBean")).isTrue();
//below line fails
assertThat(context.getBean("myBean")).isInstanceOf(CustomBean.class);
});
}
}
The only way I managed to have my test to pass was to remove the #Component on my properties and add the #EnableConfigurationProperties to ALL my config classes, which I find really cumbersome.
Should Spring be able to find my propety class and correctly update it ? Or am I missing something here ?

Custom YML configuration file String conversion Enum

In Spring Boot project, the configuration in the YML file can be automatically converted to an #ConfigurationProperties annotated bean, I found from the official documents and source code in ApplicationConversionService#addApplicationConverters() method to add A default LenientStringToEnumConverterFactory to handle all the String conversion to Enum, it through the Enum.valueOf() implementation,But I want to use other rules to turn strings into examples of enum,Just like the fromAlias() method below,
#ConfigurationProperties(prefix = "head")
#Component
#Data
public class HeadProperties {
private PayType payType;
private Integer cast;
#Getter
#RequiredArgsConstructor
enum PayType {
GOLD("GOLD", "金币"), DIAMOND("DIAMOND", "钻石"), VIP_FREE("VIP_FREE", "会员免费");
private final String val;
private final String alias;
static PayType fromAlias(String alias) {
return Arrays.stream(values())
.filter(type -> alias.equals(type.getAlias()))
.findAny()
.orElse(null);
}
}
}
The following is the YML file configuration
head:
payType: "金币"
cast: 10
I don't know where the entry is, so I get an error as soon as the program runs
code:
#SpringBootApplication
#Slf4j
public class DemoApplication{
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#Bean
ApplicationRunner runner(HeadProperties headConfig) {
return arg -> log.info("head config:{}", headConfig);
}
}
the following is error message:
APPLICATION FAILED TO START
***************************
Description:
Failed to bind properties under 'head.pay-type' to com.example.demo.HeadProperties$PayType:
Property: head.pay-type
Value: 金币
Origin: class path resource [application.yml] - 2:12
Reason: failed to convert java.lang.String to com.example.demo.HeadProperties$PayType (caused by java.lang.IllegalArgumentException: No enum constant com.example.demo.HeadProperties.PayType.金币)
Action:
Update your application's configuration. The following values are valid:
DIAMOND
GOLD
VIP_FREE
I tried injecting various converters like the one below into the container,but it still didn't work.
#Component
public class PayTypeConverter implements Converter<String, HeadProperties.PayType> {
#Override
public HeadProperties.PayType convert(String source) {
return HeadProperties.PayType.fromAlias(source);
}
}
#Component
public class PayTypeConverter implements Converter<String, Enum<HeadProperties.PayType>> {
#Override
public Enum<HeadProperties.PayType> convert(String source) {
return HeadProperties.PayType.fromAlias(source);
}
}
How can this requirement be fulfilled?
The converters that are used for #ConfigurationProperties binding need a special qualifier that tells Spring that they are to be used for that purpose.
An annotation exists for this- #ConfigurationPropertiesBinding. The Javadoc is as follows:
Qualifier for beans that are needed to configure the binding of #ConfigurationProperties (e.g. Converters).
So all that's needed is to add that annotation to your converter, then Spring will use it during the binding process:
#Component
#ConfigurationPropertiesBinding
public class PayTypeConverter implements Converter<String, HeadProperties.PayType> {
#Override
public HeadProperties.PayType convert(String source) {
return HeadProperties.PayType.fromAlias(source);
}
}
That then produces the expected output:
head config:HeadProperties(payType=GOLD, cast=10)
And a minor note, when writing custom converters, be aware that returning null will not trigger an error (assuming there are no other measures configured to prevent that). That means that unlike the out-of-the-box enum converter, your custom one does not produce an error if no matching enum can be found. You can remedy this by instead throwing an exception instead of returning null.

Cannot resolve bean in SpEL for Spring Data MongoDB collection name

I am trying to customize the collection name where an entity class is saved into and indexed, using Spring Data MongoDB and Spring Batch. The class is declared as follows:
#Document
#CompoundIndex(name = "unique_source", def = "{'fid': 1, 'sid': 1}", unique = true, background = true)
public class VariantSource {
...
}
And the item writer:
public class VariantSourceMongoWriter extends MongoItemWriter<VariantSource> {
public VariantSourceEntityMongoWriter(MongoOperations mongoOperations, String collectionName) {
setTemplate(mongoOperations);
setCollection(collectionName);
}
}
Saving works fine: the objects are written into the collection provided as argument. The problem is that the indexes are created in the default collection, named after the class name (variantSource).
After reading this and this, I created the following:
public class MongoCollections {
public String getCollectionFilesName() {
return "my_custom_collection_name"; // TODO Dynamic value
}
}
#Configuration
public class MongoCollectionsConfiguration {
#Bean
public MongoCollections mongoCollections() {
return new MongoCollections();
}
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = {MongoCollectionsConfiguration.class})
public class VariantSourceMongoWriterTest {
#Autowired
private MongoCollections mongoCollections;
}
I have checked the instance is correctly autowired into the unit tests, but I can't make it work with SpEL.
After changing the #Document annotation to look like this:
#Document(collection = "#{#mongoCollections.getCollectionFilesName()}")
the following exception is thrown:
org.springframework.expression.spel.SpelEvaluationException: EL1057E:(pos 1): No bean resolver registered in the context to resolve access to bean 'mongoCollections'
And if I use this:
#Document(collection = "#{mongoCollections.getCollectionFilesName()}")
the exception is this one:
org.springframework.expression.spel.SpelEvaluationException: EL1007E:(pos 0): Property or field 'mongoCollections' cannot be found on null
Finally, the following creates a collection with the name as specified, symbols included:
#Document(collection = "#mongoCollections.getCollectionFilesName()")
As pointed by this answer, to fix the injection:
#Document(collection = "#{mongoCollections.getCollectionFilesName()}")
SpelEvaluationException: EL1007E:(pos 0): Property or field 'mongoCollections' cannot be found on null
(or a direct method bean: #Document(collection = "#{getCollectionFilesName}")), try setting the ApplicationContext into the MongoMappingContext (which is used to instantiate the MongoConverter, and later the MongoTemplate):
#Bean
public MongoMappingContext MongoMappingContext() {
MongoMappingContext mappingContext = new MongoMappingContext();
mappingContext.setApplicationContext(applicationContext);
return mappingContext;
}
Make sure that your bean mongoCollections is registered in the application context,
and also correct the SpEL expression as below.
#Document(collection = "#{#mongoCollections.getCollectionFilesName()}")
I was able to get my #Document tag to access a bean by simply changing my MongoTemplate configuration file.
Previously, I had it set up like this:
#Configuration
public class MongoTemplateConfiguration {
...
#Bean
public MongoTemplate mongoTemplate() {
...
return new MongoTemplate(...);
}
}
Changing it to follow this (3.2 Java Configuration) format was all I needed in order to remove the "bean resolver" error:
#Configuration
public class MongoTemplateConfiguration extends AbstractMongoClientConfiguration {
...
#Override
#Bean
public com.mongodb.client.MongoClient mongoClient() {
MongoClientSettings settings = ...;
return MongoClients.create(settings);
}
}

Spring Data Rest: custom Converter<Entity, Resource> is never invoked

I'm trying to implement a custom Converter for an Entity to a Resource object with Spring Data Rest, but the Converter is never invoked.
I'm following this documentation:
If your project needs to have output in a different format, however,
it’s possible to completely replace the default outgoing JSON
representation with your own. If you register your own
ConversionService in the ApplicationContext and register your own
Converter, then you can return a Resource
implementation of your choosing.
That's what I've tried to do.
I have a #Configuration class that extends RepositoryRestMvcConfiguration, with this method:
#Configuration
#EnableWebMvc
#EnableHypermediaSupport(type = HypermediaType.HAL)
public class RepositoryBaseConfiguration extends RepositoryRestMvcConfiguration {
#Override
public DefaultFormattingConversionService defaultConversionService() {
return super.defaultConversionService();
}
}
And I have a Class that extends RepositoryRestConfigurerAdapter, with this implementation:
#Configuration
public class RepositoryBaseConfigurerAdapter extends RepositoryRestConfigurerAdapter {
#Override
public void configureConversionService(ConfigurableConversionService conversionService) {
if(!conversionService.canConvert(Entity.class, Resource.class))
conversionService.addConverter(new EntityConverter());
super.configureConversionService(conversionService);
}
}
Both methods of those two classes are correctly invoked and managed, so it's natural to think that the Converter has been registered in the Application Context...
This is my custom converter EntityConverter:
#Component
public class EntityConverter implements Converter<Entity, Resource> {
#Override
public Resource convert(Entity source) {
System.out.println("method convert of class EntityConverter");
return null;
}
}
The method "convert" is never invoked by Spring Data Rest.
What's wrong/missing ?
Thanks in advance.

How to inject java.nio.file.Path dependency using #ConfigurationProperties

I'm using Spring Boot and have the following Component class:
#Component
#ConfigurationProperties(prefix="file")
public class FileManager {
private Path localDirectory;
public void setLocalDirectory(File localDirectory) {
this.localDirectory = localDirectory.toPath();
}
...
}
And the following yaml properties file:
file:
localDirectory: /var/data/test
I would like to remove the reference of java.io.File (of setLocalDirectory) by replacing with java.nio.file.Path. However, I receive a binding error when I do this. Is there way to bind the property to a Path (e.g. by using annotations)?
To add to jst's answer, the Spring Boot annotation #ConfigurationPropertiesBinding can be used for Spring Boot to recognize the converter for property binding, as mentioned in the documentation under Properties Conversion:
#Component
#ConfigurationPropertiesBinding
public class StringToPathConverter implements Converter<String, Path> {
#Override
public Path convert(String pathAsString) {
return Paths.get(pathAsString);
}
}
I don't know if there is a way with annotations, but you could add a Converter to your app. Marking it as a #Component with #ComponentScan enabled works, but you may have to play around with getting it properly registered with the ConversionService otherwise.
#Component
public class PathConverter implements Converter<String,Path>{
#Override
public Path convert(String path) {
return Paths.get(path);
}
When Spring sees you want a Path but it has a String (from your application.properties), it will lookup in its registry and find it knows how to do it.
I took up james idea and defined the converter within the spring boot configuration:
#SpringBootConfiguration
public class Configuration {
public class PathConverter implements Converter<String, Path> {
#Override
public Path convert(String path) {
return Paths.get(path);
}
}
#Bean
#ConfigurationPropertiesBinding
public PathConverter getStringToPathConverter() {
return new PathConverter();
}
}

Resources