Skip to content

Binding to a Map with EnumSet values fails with "Cannot create EnumSet for unknown element type" #15539

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
olideakin opened this issue Dec 20, 2018 · 5 comments
Assignees
Labels
type: bug A general bug
Milestone

Comments

@olideakin
Copy link

olideakin commented Dec 20, 2018

When binding properties to type EnumSet, there is a failure with "Cannot create EnumSet for unknown element type" as the cause. Stack trace is as follows:

Caused by: java.lang.IllegalArgumentException: Cannot create EnumSet for unknown element type
	at org.springframework.util.Assert.notNull(Assert.java:193)
	at org.springframework.core.CollectionFactory.createCollection(CollectionFactory.java:195)
	at org.springframework.core.CollectionFactory.createCollection(CollectionFactory.java:151)
	at org.springframework.boot.context.properties.bind.CollectionBinder.lambda$bindAggregate$0(CollectionBinder.java:49)
	at org.springframework.boot.context.properties.bind.AggregateBinder$AggregateSupplier.get(AggregateBinder.java:109)
	at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:85)
	at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:71)
	at org.springframework.boot.context.properties.bind.CollectionBinder.bindAggregate(CollectionBinder.java:50)
	at org.springframework.boot.context.properties.bind.AggregateBinder.bind(AggregateBinder.java:58)
	at org.springframework.boot.context.properties.bind.Binder.lambda$bindAggregate$2(Binder.java:305)
	at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:441)
	at org.springframework.boot.context.properties.bind.Binder$Context.access$100(Binder.java:381)
	at org.springframework.boot.context.properties.bind.Binder.bindAggregate(Binder.java:304)
	at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:262)
	at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:221)
	... 57 common frames omitted

Debugging into the code, it appears this may be caused by a bug.

Stepping into CollectionBinder.bindAggregate(CollectionBinder.java), I can see that aggregateType is set to EnumSet and elementType is set to MyObject, which is expected. However the next lines are:

IndexedCollectionSupplier result = new IndexedCollectionSupplier(
				() -> CollectionFactory.createCollection(collectionType, 0));

which does not pass in the elementType.
The next line (bindAggregate) is then invoked which eventually results in a call to the IndexedCollectionSupplier created above, which ends up in CollectionFactory.createCollection():

        } else if (EnumSet.class == collectionType) {
            Assert.notNull(elementType, "Cannot create EnumSet for unknown element type");

which of course does not have an elementType set because it was not passed into the earlier call, which then results in the assertion stack trace.

Perhaps the CollectionFactory.createCollection(collectionType, 0)) should actually be CollectionFactory.createCollection(collectionType, elementType, 0));?

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Dec 20, 2018
@wilkinsona
Copy link
Member

Thanks for the report, but I can't reproduce the behaviour that you have described. Consider the following application:

package com.example.demo;

import java.util.EnumSet;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@SpringBootApplication
@EnableConfigurationProperties(ExampleProperties.class)
public class Gh15539Application {

	public static void main(String[] args) {
		ExampleProperties properties = SpringApplication.run(Gh15539Application.class,
				"--example.directions=north,south").getBean(ExampleProperties.class);
		System.out.println(properties.getDirections());
	}

}

@ConfigurationProperties("example")
class ExampleProperties {

	private EnumSet<Direction> directions;

	public EnumSet<Direction> getDirections() {
		return directions;
	}

	public void setDirections(EnumSet<Direction> directions) {
		this.directions = directions;
	}

}

enum Direction {

	NORTH, SOUTH, EAST, WEST

}

It produces the following output when run:


  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.1.RELEASE)

2018-12-20 16:58:58.146  INFO 61268 --- [           main] com.example.demo.Gh15539Application      : Starting Gh15539Application on Andys-MacBook-Pro.local with PID 61268 (/Users/awilkinson/dev/workspaces/spring-projects/spring-boot/master/gh-15539/target/classes started by awilkinson in /Users/awilkinson/dev/workspaces/spring-projects/spring-boot/master/gh-15539)
2018-12-20 16:58:58.149  INFO 61268 --- [           main] com.example.demo.Gh15539Application      : No active profile set, falling back to default profiles: default
2018-12-20 16:58:58.514  INFO 61268 --- [           main] com.example.demo.Gh15539Application      : Started Gh15539Application in 0.578 seconds (JVM running for 0.868)
[NORTH, SOUTH]

If you'd like us to spend some more time investigating, can you please provide a minimal sample (something that we can unzip or git clone) that reproduces the behaviour you have described?

@wilkinsona wilkinsona added the status: waiting-for-feedback We need additional information before we can continue label Dec 20, 2018
@olideakin
Copy link
Author

Thanks for the quick response - here's a recreator for how I'm getting the error. I'm rebinding properties from a YAML file myself using code similar to the following. This for me gives the "Cannot create EnumSet for unknown element type" IllegalArgumentException:

import java.io.FileInputStream;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.InputStreamResource;

public class Test {

    public static void main(String[] args) throws Exception {
        final StandardEnvironment environment = new StandardEnvironment();
        final YamlPropertySourceLoader propertySourceLoader = new YamlPropertySourceLoader();
        final MutablePropertySources propertySources = environment.getPropertySources();
        final List<PropertySource<?>> load = propertySourceLoader.load("test.yaml", new InputStreamResource(new FileInputStream("test.yaml")));
        load.forEach(propertySources::addFirst);
        final ExampleProperties props = Binder.get(environment).bind("example", Bindable.of(ExampleProperties.class)).orElse(new ExampleProperties());
        System.out.println(props.getDirectionsMap());
    }

    @ConfigurationProperties("example")
    static class ExampleProperties {
        private Map<String, EnumSet<Direction>> directionsMap = new LinkedHashMap<>();

        public Map<String, EnumSet<Direction>> getDirectionsMap() {
            return directionsMap;
        }

        public void setDirectionsMap(Map<String, EnumSet<Direction>> directionsMap) {
            this.directionsMap = directionsMap;
        }

    }

    enum Direction {
        NORTH, SOUTH, EAST, WEST
    }
}

with test.yaml:

example:
  directions-map:
    DIRECTIONS: NORTH, SOUTH

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Dec 20, 2018
@mbhave
Copy link
Contributor

mbhave commented Dec 21, 2018

Thanks for the sample @olideakin. It looks like a bug and this should actually be CollectionFactory.createCollection(collectionType, elementType, 0));, as you suggested.

@mbhave mbhave added type: bug A general bug and removed status: feedback-provided Feedback has been provided status: waiting-for-triage An issue we've not yet triaged labels Dec 21, 2018
@mbhave mbhave added this to the 2.1.x milestone Dec 21, 2018
@mbhave mbhave changed the title Binding properties of EnumSet<MyObject> fails with "Cannot create EnumSet for unknown element type" Binding to collection of type EnumSet fails with "Cannot create EnumSet for unknown element type" Dec 21, 2018
@wilkinsona wilkinsona changed the title Binding to collection of type EnumSet fails with "Cannot create EnumSet for unknown element type" Binding to a Map with EnumSet values fails with "Cannot create EnumSet for unknown element type" Dec 21, 2018
nishantraut added a commit to nishantraut/spring-boot that referenced this issue Dec 21, 2018
@wilkinsona
Copy link
Member

Here's an addition to MapBinderTests that reproduces the problem:

@Test
public void bindToMapWithEnumSetValuesShouldPopulateMap() {
	ResolvableType type = ResolvableType.forClassWithGenerics(Map.class,
			ResolvableType.forClass(String.class),
			ResolvableType.forClassWithGenerics(EnumSet.class, Direction.class));
	MockConfigurationPropertySource source = new MockConfigurationPropertySource();
	source.put("a.alpha", "north");
	source.put("a.bravo", "east");
	source.put("a.charlie", "west");
	this.sources.add(source);
	this.binder.bind("a", Bindable.<Map<String, EnumSet<Direction>>>of(type)).get();
}

static enum Direction {

	NORTH, SOUTH, EAST, WEST;

}

The following change moves things a bit further along:

diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java
index 4213abd7da..99d0582a26 100644
--- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java
+++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java
@@ -46,7 +46,8 @@ class CollectionBinder extends IndexedElementsBinder<Collection<Object>> {
                                target.getType().asCollection().getGenerics());
                ResolvableType elementType = target.getType().asCollection().getGeneric();
                IndexedCollectionSupplier result = new IndexedCollectionSupplier(
-                               () -> CollectionFactory.createCollection(collectionType, 0));
+                               () -> CollectionFactory.createCollection(collectionType,
+                                               elementType.resolve(), 0));
                bindIndexed(name, target, elementBinder, aggregateType, elementType, result);
                if (result.wasSupplied()) {
                        return result.get();

However, we then see a failure as Framework tries to create a RegularEnumSet using its default constructor (which doesn't exist):

org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'a.alpha' to java.util.EnumSet<org.springframework.boot.context.properties.bind.MapBinderTests$Direction>
	at org.springframework.boot.context.properties.bind.Binder.handleBindError(Binder.java:249)
	at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:225)
	at org.springframework.boot.context.properties.bind.Binder.lambda$1(Binder.java:299)
	at org.springframework.boot.context.properties.bind.Binder$Context.withSource(Binder.java:419)
	at org.springframework.boot.context.properties.bind.Binder$Context.access$1(Binder.java:416)
	at org.springframework.boot.context.properties.bind.Binder.lambda$0(Binder.java:301)
	at org.springframework.boot.context.properties.bind.AggregateElementBinder.bind(AggregateElementBinder.java:39)
	at org.springframework.boot.context.properties.bind.MapBinder$EntryBinder.lambda$0(MapBinder.java:165)
	at java.util.HashMap.computeIfAbsent(HashMap.java:1127)
	at org.springframework.boot.context.properties.bind.MapBinder$EntryBinder.bindEntries(MapBinder.java:164)
	at org.springframework.boot.context.properties.bind.MapBinder.bindAggregate(MapBinder.java:70)
	at org.springframework.boot.context.properties.bind.AggregateBinder.bind(AggregateBinder.java:58)
	at org.springframework.boot.context.properties.bind.Binder.lambda$2(Binder.java:304)
	at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:448)
	at org.springframework.boot.context.properties.bind.Binder$Context.access$2(Binder.java:445)
	at org.springframework.boot.context.properties.bind.Binder.bindAggregate(Binder.java:303)
	at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:261)
	at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:220)
	at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:208)
	at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:164)
	at org.springframework.boot.context.properties.bind.MapBinderTests.bindToMapWithEnumSetValuesShouldPopulateMap(MapBinderTests.java:648)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:89)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:41)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:541)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:763)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:463)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:209)
Caused by: org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.util.RegularEnumSet<java.lang.Enum<?>>] to type [java.util.RegularEnumSet<org.springframework.boot.context.properties.bind.MapBinderTests$Direction>] for value '[NORTH]'; nested exception is java.lang.IllegalArgumentException: Could not instantiate Collection type: java.util.RegularEnumSet
	at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:46)
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:191)
	at org.springframework.boot.context.properties.bind.BindConverter$CompositeConversionService.convert(BindConverter.java:185)
	at org.springframework.boot.context.properties.bind.BindConverter.convert(BindConverter.java:99)
	at org.springframework.boot.context.properties.bind.IndexedElementsBinder.convert(IndexedElementsBinder.java:153)
	at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindValue(IndexedElementsBinder.java:101)
	at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:85)
	at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:71)
	at org.springframework.boot.context.properties.bind.CollectionBinder.bindAggregate(CollectionBinder.java:51)
	at org.springframework.boot.context.properties.bind.AggregateBinder.bind(AggregateBinder.java:58)
	at org.springframework.boot.context.properties.bind.Binder.lambda$2(Binder.java:304)
	at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:448)
	at org.springframework.boot.context.properties.bind.Binder$Context.access$2(Binder.java:445)
	at org.springframework.boot.context.properties.bind.Binder.bindAggregate(Binder.java:303)
	at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:261)
	at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:220)
	... 43 more
Caused by: java.lang.IllegalArgumentException: Could not instantiate Collection type: java.util.RegularEnumSet
	at org.springframework.core.CollectionFactory.createCollection(CollectionFactory.java:212)
	at org.springframework.core.convert.support.CollectionToCollectionConverter.convert(CollectionToCollectionConverter.java:81)
	at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:40)
	... 58 more
Caused by: java.lang.NoSuchMethodException: java.util.RegularEnumSet.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at org.springframework.util.ReflectionUtils.accessibleConstructor(ReflectionUtils.java:530)
	at org.springframework.core.CollectionFactory.createCollection(CollectionFactory.java:208)
	... 60 more

I have thus far been unable to find a way to pass sufficient type information into the conversion service so that the collection-to-collection conversion is short-circuited. It looks like we may need a change in Framework here as well.

@mbhave
Copy link
Contributor

mbhave commented Dec 22, 2018

Thanks @wilkinsona. It doesn't look like even if were able to pass type information to the CollectionToCollectionConverter, it would be short-circuited. I've created a Spring Framework issue. The issue is a bit more generic because it seems like this would fail whenever the targetType is RegularEnumSet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: bug A general bug
Projects
None yet
Development

No branches or pull requests

4 participants