spring test rest template : Could not extract response: no suitable HttpMessageConverter found for response type - spring

I Get this exception
org.springframework.web.client.RestClientException: Could not extract
response: no suitable HttpMessageConverter found for response type [AnalyticsResponse] and content type [application/json;charset=UTF-8]
in my junit test cases only(Rest endpoints work fine) if my DTO contains a map with a user defined class as a key
#Data
public class AnalyticsResponse {
private List<Committer> commitersList; //OK
private Map<Committer , Long> comittersCommitsMap; // Problem
private Map<Date, List<CommitItem>> commitItemsTimeLineMap; //OK
}
If comittersCommitsMap field is removed, every thing goes fine
my test case code snippet:
ResponseEntity<AnalyticsResponse> analyticsResponse = testRestTemplate.getForEntity(ANALYSIS_CONTROLLER_BASE_URL+"analytics?repo-full-name=" + searchResponse.getBody().get(0).getFull_name() ,
AnalyticsResponse.class);
---update : Committer class
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Committer {
private String name;
private String email;
}

By default, the ObjectMapper cannot determine serialization and deserialization for a Map in which a key is not a String, you have to provide your custom implementation.
Implement KeyDeserializer and JsonSerializer for Committer and Date(as key for Map)
Configure ObjectMapper - register module with KeyDeserializer and KeySerializer(JsonSerializer)
#SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
// KeyDeserializer for Committer (simple without 'null' check)
#RequiredArgsConstructor
public static class CommitterKeyDeserializer extends KeyDeserializer {
private final ObjectMapper mapper;
#Override
public Object deserializeKey(final String key,
final DeserializationContext ctxt)
throws IOException, JsonProcessingException {
return mapper.readValue(key, Committer.class);
}
}
// KeyDeserializer for Date (simple without 'null' check)
#RequiredArgsConstructor
public static class DateKeyDeserializer extends KeyDeserializer {
private final ObjectMapper mapper;
#Override
public Object deserializeKey(final String key,
final DeserializationContext ctxt)
throws IOException, JsonProcessingException {
return mapper.readValue(key, Date.class);
}
}
// JsonSerializer for Committer (simple without 'null' check)
#RequiredArgsConstructor
public static class CommitterJsonSerializer extends JsonSerializer<Committer> {
private final ObjectMapper mapper;
#Override
public void serialize(Committer committer,
JsonGenerator jgen,
SerializerProvider provider)
throws IOException, JsonProcessingException {
jgen.writeFieldName(mapper.writeValueAsString(committer));
}
}
// JsonSerializer for Date (simple without 'null' check)
public static class DateJsonSerializer extends JsonSerializer<Date> {
#Override
public void serialize(Date date,
JsonGenerator jgen,
SerializerProvider provider)
throws IOException, JsonProcessingException {
jgen.writeFieldName(String.valueOf(date.getTime()));
}
}
// ObjectMapper configuration
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
// register module with custom serializers and deserializers
mapper.registerModule(new SimpleModule()
.addKeyDeserializer(
Committer.class,
new CommitterKeyDeserializer(mapper))
.addKeyDeserializer(
Date.class,
new DateKeyDeserializer(mapper))
.addKeySerializer(
Committer.class,
new CommitterJsonSerializer(mapper))
.addKeySerializer(
Date.class,
new DateJsonSerializer()));
return mapper;
}
// RestTemplate configuration
#Bean
public RestTemplate restTemplate(List<HttpMessageConverter<?>> converters) {
RestTemplate restTemplate = new RestTemplate();
// add spring's predefined converters
restTemplate.setMessageConverters(converters);
return restTemplate;
}
}
Note that in this simple implementation the key Committer in comittersCommitsMap represented as a String in JSON response (RestTemplate with this implementation works as well):
{
"commitersList": [
{
"name": "name",
"email": "email"
}
],
"comittersCommitsMap": {
"{\"name\":\"name\",\"email\":\"email\"}": 1
},
"commitItemsTimeLineMap": {
"1570929503854": [
{
"data": "data"
}
]
}
}

Related

How to use a custom deserializer with Spring #RequestParam

I have an interface that's not aware of its implementations (module-wise):
public interface SomeInterface {
}
and an enum implementing it:
public enum SomeInterfaceImpl {
}
I have a #RestController:
#RequestMapping(method = RequestMethod.GET)
public List<SomeClass> find(#RequestParam(value = "key", required = false) SomeInterface someInt,
HttpServletResponse response){
}
Although Jackson is aware that it should deserialize SomeInterface as SomeInterfaceImpl as follows:
public class DefaultSomeInterfaceDeserializer extends JsonDeserializer<SomeInterface> {
#Override
public SomeInterface deserialize(JsonParser parser, DeserializationContext context) throws IOException {
return parser.readValuesAs(SomeInterfaceImpl.class).next();
}
}
and:
#Configuration
public class MyJacksonConfiguration implements BeanPostProcessor {
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if(bean instanceof Jackson2ObjectMapperBuilder){
Jackson2ObjectMapperBuilder builder = (Jackson2ObjectMapperBuilder) bean;
builder.deserializerByType(SomeInterface.class, new DefaultSomeInterfaceDeserializer());
}
return bean;
}
}
and successfully serializes and deserializes SomeInterface as SomeInterfaceImpl in the #RequestBody, it doesn't seem to have any effect on mapping SomeInterface to SomeInterfaceImpl with #RequestParam. How do I overcome this?
Having a Converter in the application context as M.Denium suggested does the job:
#Component
public class SomeInterfaceConverter implements Converter<String, SomeInterface> {
#Override
public SomeInterface convert(String value) {
return new SomeInterfaceImpl(value);
}
}

InvalidFormatException for Date - fixing without using JsonFormat or modifying original class

Introduction
We are using a custom starter hosted on a nexus repository, that contains spring-cloud-feign clients that make requests to microservices.
One of the microservices returns the dates as "dd-MM-yyyy HH:mm:ssZ" and this works in most of our applications. However, we have one application that is throwing the following error:
Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2019-10-16 14:23:17": not a valid representation (error: Failed to parse Date value '2019-10-16 14:23:17': Unparseable date: "2019-10-16 14:23:1
7")
Current work-around
My Current work-around, as I don't want to pollute the starter, is to extend the class and create a local feign-client and local pojo with the proper JsonFormat:
public class DocumentMetaDataFix extends DocumentMetaData {
#JsonFormat(
shape = Shape.STRING,
pattern = "yyyy-MM-dd HH:mm:ss"
)
private Date creationDate;
#JsonFormat(
shape = Shape.STRING,
pattern = "yyyy-MM-dd HH:mm:ss"
)
Failed Fixes
I have tried the following in my configuration class, in order to try affecting the de-serialization from another path. However, the DocumentMetaDataSerializer is never called. The ObjectMapper bean IS called.
#Configuration
#EnableSpringDataWebSupport
#RequiredArgsConstructor
public class MyConfig extends WebMvcConfigurerAdapter {
#Bean
public Jackson2ObjectMapperBuilderCustomizer addCustomBigDecimalDeserialization() {
return new Jackson2ObjectMapperBuilderCustomizer() {
#Override
public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
jacksonObjectMapperBuilder.deserializerByType(DocumentMetaData.class, new DocumentMetaDataDeserializer());
}
};
}
#Primary
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
mapper.setDateFormat(new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"));
//mapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, true);
return mapper;
}
#Bean
public Module dynamoDemoEntityDeserializer() {
SimpleModule module = new SimpleModule();
module.addDeserializer(DocumentMetaData.class, new DocumentMetaDataDeserializer());
return module;
}
public static class DocumentMetaDataDeserializer extends JsonDeserializer<DocumentMetaData> {
#Override
public DocumentMetaData deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// return DynamoDemoEntity instance;
JsonNode node = jp.getCodec().readTree(jp);
return null;
}
public DocumentMetaData deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer t) throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
return null;
}
}
Full Stacktrace
Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2019-10-16 14:23:17": not a valid representation (error: Failed to parse Date value '2019-10-16 14:23:17': Unparseable date: "2019-10-16 14:23:1
7")
at [Source: (ByteArrayInputStream); line: 1, column: 580] (through reference chain: eu.europa.ec.nova.documentstore.DocumentMetaData["creationDate"])
at com.fasterxml.jackson.databind.exc.InvalidFormatException.from(InvalidFormatException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.weirdStringException(DeserializationContext.java:1548)
at com.fasterxml.jackson.databind.DeserializationContext.handleWeirdStringValue(DeserializationContext.java:910)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseDate(StdDeserializer.java:524)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseDate(StdDeserializer.java:467)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateBasedDeserializer._parseDate(DateDeserializers.java:195)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateDeserializer.deserialize(DateDeserializers.java:285)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateDeserializer.deserialize(DateDeserializers.java:268)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:127)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3084)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:237)
... 70 common frames omitted
So, any ideas?
I have searched through the project for references to Jackson in case there is anything else in my project causing this.
I am will try to go inside the ObjectMapper and try to debug the current parameters/fields of the configuration at ObjectMapper.java:3084 from the stacktace:
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3084)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:237)
... 67 common frames omitted
Update
I added a breakpoint in the objectmapper constructor, and am seeing that it is being initialized from more than one location. This led me to suspect that spring-boot is not using my ObjectMapper. Instead it is using an internal spring one that is called from MappingJackson2HttpMessageConverter .
<init>:480, ObjectMapper
build:606, Jackson2ObjectMapperBuilder
<init>:59, MappingJackson2HttpMessageConverter
<init>:74, AllEncompassingFormHttpMessageConverter
I will therefore try to over-ride this internal spring one, based on results I found from: How to customise the Jackson JSON mapper implicitly used by Spring Boot?
However this also failed.
References
Is it possible to configure Jackson custom deserializers at class level for different data types?
https://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-mvc.html#howto-customize-the-jackson-objectmapper
https://www.baeldung.com/jackson-deserialization
very useful: https://mostafa-asg.github.io/post/customize-json-xml-spring-mvc-output/
How to customise Jackson in Spring Boot 1.4
Update - final list of tries
It still fails with an error.
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.serializationInclusion(JsonInclude.Include.NON_NULL);
builder.propertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
builder.serializationInclusion(Include.NON_EMPTY);
builder.indentOutput(true).dateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));
//converters.add(cmsaMessageConverter());
converters.add(new StringHttpMessageConverter());
converters.add(new FormHttpMessageConverter());
converters.add(new MappingJackson2HttpMessageConverter());
}
#Bean
public Jackson2ObjectMapperBuilderCustomizer addCustomBigDecimalDeserialization() {
return new Jackson2ObjectMapperBuilderCustomizer() {
#Override
public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
jacksonObjectMapperBuilder.deserializerByType(DocumentMetaData.class, new DocumentMetaDataDeserializer());
}
};
}
#Primary
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
mapper.setDateFormat(new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"));
//mapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, true);
return mapper;
}
#Bean
public Module dynamoDemoEntityDeserializer() {
SimpleModule module = new SimpleModule();
module.addDeserializer(DocumentMetaData.class, new DocumentMetaDataDeserializer());
return module;
}
public static class DocumentMetaDataDeserializer extends JsonDeserializer<DocumentMetaData> {
#Override
public DocumentMetaData deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// return DynamoDemoEntity instance;
JsonNode node = jp.getCodec().readTree(jp);
return null;
}
public DocumentMetaData deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer t) throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
return null;
}
}
It still fails with an error.
Try using LocalDateTime,
this is what I'm doing and working for me
#JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime date;

Couldn't find PersistentEntity for type class when using #EnableMongoAuditing

I am getting "Couldn't find PersistentEntity for type class" error when I am using #EnableMongoAuditing features along with MongoRepository.
This happens when I save a document when collection isn't already present in database.
I tried whatever is mentioned in:
https://github.com/spring-projects/spring-boot/issues/12023
https://jira.spring.io/browse/DATAMONGO-1999
Spring boot mongodb auditing error
but nothing is working.
Mentioned things are:
Extend MongoConfig by AbstractMongoConfiguration and override all methods.
Here is my code which reproduced the same error:
MongoConfig class
#Configuration
public class MongoConfig extends AbstractMongoConfiguration {
#Value("${spring.data.mongodb.host}")
private String mongoHost;
#Value("${spring.data.mongodb.port}")
private String mongoPort;
#Value("${spring.data.mongodb.database}")
private String mongoDB;
#Override
public MongoDbFactory mongoDbFactory() {
return new SimpleMongoDbFactory(new MongoClient(mongoHost + ":" + mongoPort), mongoDB);
}
#Override
public MongoClient mongoClient() {
return new MongoClient(mongoHost, Integer.parseInt(mongoPort));
}
#Override
public MongoTemplate mongoTemplate() {
return new MongoTemplate(mongoDbFactory());
}
#Override
public MappingMongoConverter mappingMongoConverter() {
return new MappingMongoConverter(new DefaultDbRefResolver(mongoDbFactory()), new MongoMappingContext());
}
#Override
protected String getDatabaseName() {
return mongoDB;
}
}
Person Collection class
#Document
public class Person {
#Id
private String id;
private String name;
#CreatedDate
private LocalDateTime createdAt;
#LastModifiedDate
private LocalDateTime lastModified;
// Getter Setters Constructors omitted for brevity
}
Main Application class
#EnableMongoAuditing
#EnableMongoRepositories ({"com.example.*", "org.apache.*"})
#SpringBootApplication
#ComponentScan({"com.example.*", "org.apache.*"})
public class DemoApplication implements CommandLineRunner {
#Autowired
PersonRepository personRepository;
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#Override
public void run(String... args) throws Exception {
Person p1 = new Person("1", "prakhar");
personRepository.save(p1);
}
}
Expected Result is Person entity should be saved in database.
Actual Result is "Couldn't find PersistentEntity for type class Person" error
Looks like you ran into https://github.com/spring-projects/spring-boot/issues/12023
Extending AbstractMongoConfiguration will switch off Spring Boot's auto-configuration of various Mongo components and also customises the base packages that are used to scan for mappings. I would recommend that you don't use it in Spring Boot.
Update
I managed to get the example running with the configuration as simple as
#Configuration
public class MongoConfig {
#Value("${spring.data.mongodb.host}")
private String mongoHost;
#Value("${spring.data.mongodb.port}")
private String mongoPort;
#Value("${spring.data.mongodb.database}")
private String mongoDB;
#Bean
public MongoDbFactory mongoDbFactory() {
return new SimpleMongoDbFactory(new MongoClient(mongoHost + ":" + mongoPort), mongoDB);
}
#Bean
public MongoClient mongoClient() {
return new MongoClient(mongoHost, Integer.parseInt(mongoPort));
}
}
and the app class
#EnableMongoAuditing
#SpringBootApplication
public class DemoApplication implements CommandLineRunner {
#Autowired
PersonRepository personRepository;
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#Override
public void run(String... args) throws Exception {
Thread.sleep(2000);
Person p1 = new Person("1", "prakhar");
personRepository.save(p1);
}
}
Notice that I followed my own advice and did't inherit from AbstractMongoConfiguration
Explanation
The problem lies in the initialization of
#Bean
public MappingMongoConverter mappingMongoConverter() {
return new MappingMongoConverter(new DefaultDbRefResolver(mongoDbFactory()), new MongoMappingContext());
}
You simply call MongoMappingContext constructor, without calling setInitialEntitySet. Compare that with MongoDataConfiguration auto-configuration class.
#Bean
#ConditionalOnMissingBean
public MongoMappingContext mongoMappingContext(MongoCustomConversions conversions)
throws ClassNotFoundException {
MongoMappingContext context = new MongoMappingContext();
context.setInitialEntitySet(new EntityScanner(this.applicationContext)
.scan(Document.class, Persistent.class));
Class<?> strategyClass = this.properties.getFieldNamingStrategy();
if (strategyClass != null) {
context.setFieldNamingStrategy(
(FieldNamingStrategy) BeanUtils.instantiateClass(strategyClass));
}
context.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
return context;
}
Even worse, you don't register MongoMappingContext as a managed bean.
Due to this fact, auto-configuration class is still created. This leads to a race condition, I tried to run the original code and could easily reproduce the error, but with a breakpoint in AbstractMappingContext.addPersistentEntity the test always passed.
For me I resolved this issue by adding following method in MongoConfig if your class extends from AbstractMongoConfiguration
#Override
protected String getMappingBasePackage() {
return "com.companyName.modulename"
}
If MongoConfig extends from MongoConfigurationSupport then add below method
#Override
protected Collection<String> getMappingBasePackages() {
return Arrays.asList("com.companyName.module1","com.companyName.module2");
}
Note that in later case I can specify multiple package names as base packages.

OffsetDateTime returns the datetime in wrong format

I use a Spring 4.3.4, a Jackson 2.8.6 and a Tomcat 8.5.11. I've some REST API:
#RestController
#RequestMapping(value = "/netadvice")
public class NetworkAdviceController {
...
#RequestMapping(
value = "/offsetDateTime",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public OffsetDateTime todayOffsetDateTime() {
return OffsetDateTime.now();
}
}
After call /netadvice/offsetDateTime I would like to receive the datetime in the following format: 2017-02-14T08:46:06.102Z but I receive something like this: 1487062454.957000000.
What did I wrong and how can I fix this issue?
The following code is my custom datetime serializer and deserializer:
#Configuration
public class JsonConfig {
public static final DateTimeFormatter FORMATTER = ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
#Bean
#Primary
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(OffsetDateTime.class, new OffsetDateTimeSerializer());
javaTimeModule.addDeserializer(OffsetDateTime.class, new OffsetdateTimeDeserializer());
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
objectMapper.registerModule(javaTimeModule);
return objectMapper;
}
public class OffsetDateTimeSerializer extends JsonSerializer<OffsetDateTime> {
#Override
public void serialize(OffsetDateTime value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
gen.writeString(value.format(FORMATTER));
}
}
public class OffsetdateTimeDeserializer extends JsonDeserializer<OffsetDateTime> {
#Override
public OffsetDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return OffsetDateTime.parse(p.getValueAsString(), FORMATTER);
}
}
}

Spring Security: How do I enable custom expression result type support?

In my Spring Boot application I'm using the #PreAuthorize annotation in my controller methods to make them authorized. The expressions use simple boolean-returning methods, like this:
#ResponseStatus(OK)
#PreAuthorize("#auth.authentication.mayReadMe(principal)")
public UserDto readMe() {
...
The mayReadMe(...) method simply returns a boolean value, however it uses ternary logic under the hood and just converts a special enum to boolean:
boolean mayReadMe(#Nonnull UserDetails principal);
Now let's say I want to rework the authorization components and let the method return the enum:
#Nonnull
foo.bar.FooBarEnum mayReadMe(#Nonnull final UserDetails principal);
However, I'm getting the following exception:
java.lang.IllegalArgumentException: Failed to evaluate expression '#primaryAuth.authentication.mayReadMe(principal)'
at org.springframework.security.access.expression.ExpressionUtils.evaluateAsBoolean(ExpressionUtils.java:15)
at org.springframework.security.access.expression.method.ExpressionBasedPreInvocationAdvice.before(ExpressionBasedPreInvocationAdvice.java:44)
at org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter.vote(PreInvocationAuthorizationAdviceVoter.java:57)
at org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter.vote(PreInvocationAuthorizationAdviceVoter.java:25)
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:62)
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:232)
at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:64)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655)
...
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1001E:(pos 0): Type conversion problem, cannot convert from #javax.annotation.Nonnull foo.bar.FooBarEnum to java.lang.Boolean
at org.springframework.expression.spel.support.StandardTypeConverter.convertValue(StandardTypeConverter.java:78)
at org.springframework.expression.common.ExpressionUtils.convertTypedValue(ExpressionUtils.java:53)
at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:301)
at org.springframework.security.access.expression.ExpressionUtils.evaluateAsBoolean(ExpressionUtils.java:11)
... 113 common frames omitted
Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [#javax.annotation.Nonnull foo.bar.FooBarEnum] to type [java.lang.Boolean]
at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:313)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:195)
at org.springframework.expression.spel.support.StandardTypeConverter.convertValue(StandardTypeConverter.java:74)
... 116 common frames omitted
The exception message is really clear, but I can't inject my custom converter in any way. What I've tried so far:
Registering custom converters via WebMvcConfigurerAdapter.addFormatters(FormatterRegistry) (both Converter and GenericConverter)
Bean-ining a custom ExpressionBasedPreInvocationAdvice (but it shouldn't work as far as I understand)
... and a few other ways I can't recall after spending a few hours unfortunately.
How do I inject a custom type converter so the #PreAuthorization expressions could be aware of the foo.bar.FooBarEnum as the returning type?
Edit 1
Why do I need a custom type to be returned, and not a boolean. I'm also writing a simple REST API self-describing subsystem, just a simple GET /api endpoint to return a list of endpoints and so on. This list consists of a certain objects describing API end point, HTTP method, incoming and outgoing DTOs, and the last thing I'm trying to add to the definition object is an endpoint authorization policy expression. Note that it's not a good idea to return the #PreAuthorize string expression (I mean a raw string), but it might be good to return a custom object describing the authorization rules. What I want the most is returning an object like:
public final class AuthorizationExpression
implements BooleanSupplier {
...
public IExpression toExpression() {
...
}
where BooleanSupplier is expected to be used in the converter I'm trying to inject in order to satisfy the authorization needs -- just return true or false, and where IExpression is expected to be toString-ed in the GET /api handler using the Spring expression evaluator. Hence the mayReadMe signature might be as follows:
AuthorizationExpression mayReadMe(...)
so I could use AuthorizationExpression up to a certain use case. The FooBarEnum is just a simplification for the original question prior to the edit.
A suggestion, let your enum implement the conversion method:
public enum FooBarEnum {
// previous code
public boolean booleanValue() {
// TODO
}
}
And change your annotation:
#PreAuthorize("#auth.authentication.mayReadMe(principal).booleanValue()")
Figured it out. I only need to tune the DefaultMethodSecurityExpressionHandler instance. Let's assume the net two classes as library ones:
public abstract class CustomTypesGlobalMethodSecurityConfiguration
extends GlobalMethodSecurityConfiguration {
protected abstract ApplicationContext applicationContext();
protected abstract ConversionService conversionService();
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
final ApplicationContext applicationContext = applicationContext();
final TypeConverter typeConverter = new StandardTypeConverter(conversionService());
final DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler() {
#Override
public StandardEvaluationContext createEvaluationContextInternal(final Authentication authentication, final MethodInvocation methodInvocation) {
final StandardEvaluationContext decoratedStandardEvaluationContext = super.createEvaluationContextInternal(authentication, methodInvocation);
return new ForwardingStandardEvaluationContext() {
#Override
protected StandardEvaluationContext standardEvaluationContext() {
return decoratedStandardEvaluationContext;
}
#Override
public TypeConverter getTypeConverter() {
return typeConverter;
}
};
}
};
handler.setApplicationContext(applicationContext);
return handler;
}
}
where ForwardingStandardEvaluationContext is a simple forwarding decorator to decorate an instance of StandardEvaluationContext because the latter is ConversionService-aware:
public abstract class ForwardingStandardEvaluationContext
extends StandardEvaluationContext {
protected abstract StandardEvaluationContext standardEvaluationContext();
#Override public void setRootObject(final Object rootObject, final TypeDescriptor typeDescriptor) { standardEvaluationContext().setRootObject(rootObject, typeDescriptor); }
#Override public void setRootObject(final Object rootObject) { standardEvaluationContext().setRootObject(rootObject); }
#Override public TypedValue getRootObject() { return standardEvaluationContext().getRootObject(); }
#Override public void addConstructorResolver(final ConstructorResolver resolver) { standardEvaluationContext().addConstructorResolver(resolver); }
#Override public boolean removeConstructorResolver(final ConstructorResolver resolver) { return standardEvaluationContext().removeConstructorResolver(resolver); }
#Override public void setConstructorResolvers(final List<ConstructorResolver> constructorResolvers) { standardEvaluationContext().setConstructorResolvers(constructorResolvers); }
#Override public List<ConstructorResolver> getConstructorResolvers() { return standardEvaluationContext().getConstructorResolvers(); }
#Override public void addMethodResolver(final MethodResolver resolver) { standardEvaluationContext().addMethodResolver(resolver); }
#Override public boolean removeMethodResolver(final MethodResolver methodResolver) { return standardEvaluationContext().removeMethodResolver(methodResolver); }
#Override public void setMethodResolvers(final List<MethodResolver> methodResolvers) { standardEvaluationContext().setMethodResolvers(methodResolvers); }
#Override public List<MethodResolver> getMethodResolvers() { return standardEvaluationContext().getMethodResolvers(); }
#Override public void setBeanResolver(final BeanResolver beanResolver) { standardEvaluationContext().setBeanResolver(beanResolver); }
#Override public BeanResolver getBeanResolver() { return standardEvaluationContext().getBeanResolver(); }
#Override public void addPropertyAccessor(final PropertyAccessor accessor) { standardEvaluationContext().addPropertyAccessor(accessor); }
#Override public boolean removePropertyAccessor(final PropertyAccessor accessor) { return standardEvaluationContext().removePropertyAccessor(accessor); }
#Override public void setPropertyAccessors(final List<PropertyAccessor> propertyAccessors) { standardEvaluationContext().setPropertyAccessors(propertyAccessors); }
#Override public List<PropertyAccessor> getPropertyAccessors() { return standardEvaluationContext().getPropertyAccessors(); }
#Override public void setTypeLocator(final TypeLocator typeLocator) { standardEvaluationContext().setTypeLocator(typeLocator); }
#Override public TypeLocator getTypeLocator() { return standardEvaluationContext().getTypeLocator(); }
#Override public void setTypeConverter(final TypeConverter typeConverter) { standardEvaluationContext().setTypeConverter(typeConverter); }
#Override public TypeConverter getTypeConverter() { return standardEvaluationContext().getTypeConverter(); }
#Override public void setTypeComparator(final TypeComparator typeComparator) { standardEvaluationContext().setTypeComparator(typeComparator); }
#Override public TypeComparator getTypeComparator() { return standardEvaluationContext().getTypeComparator(); }
#Override public void setOperatorOverloader(final OperatorOverloader operatorOverloader) { standardEvaluationContext().setOperatorOverloader(operatorOverloader); }
#Override public OperatorOverloader getOperatorOverloader() { return standardEvaluationContext().getOperatorOverloader(); }
#Override public void setVariable(final String name, final Object value) { standardEvaluationContext().setVariable(name, value); }
#Override public void setVariables(final Map<String, Object> variables) { standardEvaluationContext().setVariables(variables); }
#Override public void registerFunction(final String name, final Method method) { standardEvaluationContext().registerFunction(name, method); }
#Override public Object lookupVariable(final String name) { return standardEvaluationContext().lookupVariable(name); }
#Override public void registerMethodFilter(final Class<?> type, final MethodFilter filter) throws IllegalStateException { standardEvaluationContext().registerMethodFilter(type, filter); }
}
And then a couple application classes:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = false)
class SecurityConfiguration
extends CustomTypesGlobalMethodSecurityConfiguration {
private final ApplicationContext applicationContext;
private final ConversionService conversionService;
public SecurityConfiguration(
#Autowired final ApplicationContext applicationContext,
#Autowired final ConversionService conversionService
) {
this.applicationContext = applicationContext;
this.conversionService = conversionService;
}
#Override
protected ApplicationContext applicationContext() {
return applicationContext;
}
#Override
protected ConversionService conversionService() {
return conversionService;
}
}
And finally the conversion service configuration:
#Configuration
class ConversionConfiguration {
#Bean
public ConversionService conversionService() {
final DefaultConversionService conversionService = new DefaultConversionService();
conversionService.addConverter(FooBar.class, Boolean.class, FooBar::mayProceed);
return conversionService;
}
}
The code above makes #PreAuthorize to understand FooBar-returning expressions.

Resources