Spring bean initialize maps using external property file - java

I have a class which has a Map as its member variable. Something like this -
public Clas Engine{
private Map<String,List<String>> filesByKey;
public void setFilesByKey(Map<String,List<String>> map) {
this.filesByKey = map;
}
public Map<String,List<String>> getFilesByKey() {
return filesByKey;
}
}
User can specify any number of keys in the map and its not predefined concept. They can basically group any number of files into one key and provider the map Value.
I was using PropertyOverrideConfigurer and in the properties file, I was trying to do something like this -
engine.filesByKey[key1][0]=file1
engine.filesByKey[key1][1]=file2
engine.filesByKey[key2][0]=anotherfile1
engine.filesByKey[key2][1]=anotherfile2
Now this is not working because the the List value corresponding to key1 or key2 is null to being with. So Spring Bean creation fails with the message that it can not set value to a property which is NULL.
What is the best way to handle this situation?

You should be able to use the LazyMap & LazyList from commons collections to achieve this.

Try to initialize your filesByKey variable with DefaultedMap from commons collections. It can return default value instead of null if map doesn't contain required key.

Related

How to bind a property with space to Map key in Spring application.properties file?

Given an application.properties file:
myProperties.map.key1=value1
myProperties.map.key2=value2
myProperties.map.key with space=value3
And the properties class:
#ConfigurationProperties
public class MyProperties {
private Map<String, String> map;
// getters and setters
}
I have tried to escape the spaces like key\ with\ space or even key\u0020with\u0020space but both still ended up as keywithspace in the map. Please point out the correct way to add key with space to map using Spring application.properties, thank you.
Happened to run into the same issue, I found the trick to be using [] brackets combined with escaping the whitespace using either \ or \u0020:
myProperties.map.key1=value1
myProperties.map.key2=value2
myProperties.map.[key\ with\ space]=value3
This way the whitespace is preserved in the map key.
I was unable to find documentation for this, purely stumbled upon it by trial and error.
I could do it with different approach.
In application.properties
key\u0020with\u0020space=test
And wherever I want to read, autowired Environment object.
#Autowired
private Environment env
and read the property
String test = env.getProperty("key with space");
In your case if you know the key with spaces you can read the property and then place it in the map. But if you don't know it exactly not sure how it can be handled.
For me, Spring even removed a period and non-Latin 1 characters (I use YAML, because of this) from the key. Thats why I ended up with a list with the values separated by semicolon:
myMapAsList:
- key 1;value 1
- key 2 with äöü.;value 2
You need a wrapper class with the fields key and value:
public class YourWrapper {
private String key;
private String value;
// getter & setter & constructor
}
... plus a converter:
#Component
#ConfigurationPropertiesBinding
public class YourWrapperConverter implements Converter<String, YourWrapper>{
#Override
public YourWrapper convert(String source) {
String[] data = source.split(";");
return new YourWrapper(data[0], data[1]);
}
}
In your properties class: private List<YourWrapper> myMapAsList;. Afterwards you may loop through the list and fill your map.
For map keys with non-alphanumeric characters (other than -) in them, surround the key name with [].
In your example:
'[myProperties.map.key with space]'=value3
See https://github.com/spring-projects/spring-boot/wiki/Relaxed-Binding-2.0#maps for further details.

Spring boot ConfigurationProperties - Inject a TreeMap with a comparator

I have the following config properties in a spring boot app:
#Configuration
#ConfigurationProperties(prefix = "test")
public class ConfigProperties {
private Map<String, String> values = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
public void setValues(Map<String, String> values) {
this.values = values;
}
I want to end up with a Map that ignore the case of the keys.
When the injection occurs, the setValues method receives a treemap without the comparator.
I fixed this by changing the setter to this:
public void setValues(Map<String, String> values) {
this.values.clear();
this.values.putAll(values );
}
Is there a proper way to do this? Am I missing something?
I think you are doing everything correctly to copy all entries of one arbitrary map into a sorted TreeMap with a custom comparator. Basically you only have two options: clear and putAll or create a new instance and putAll
But, I don't think this should be something that the configuration is responsible for. Your intended order likely has some business value, depending its use. So ordering should happen in the code that uses the values of the configuration. But then again, see both options above.

Creating a map in config files

We have one application where we use configuration files and they have fields as arrays and normal variables:
metadata {
array=["val1", "val2"]
singleValue=2.0
}
Now, I know how to extract these above values like
config.getStringList("metadata.array").asScala.toArray
and config.getString("metadata.singleValue)
But, is there any way I can define maps here so that I can find value wrt desired key from that map.
This config is an object of
public interface Config extends com.typesafe.config.ConfigMergeable
You can use config.getConfig("metadata") to obtain a (sub)config object.
Converting the (sub)config to a map is something you'll have to do yourself. I would use config.entrySet() to obtain the entries as key-values, and load it into a map that way.
I haven't tried compiling/testing this code, but something like this should work:
Map<String,Object> metadata = new HashMap<>();
for (Map.Entry<String,ConfigValue> entry : config.entrySet()) {
metadata.put(entry.getKey(), entry.getValue().unwrapped());
}

Assert that map contains not null vales by given keys

I have a code that consumes map of properties with string keys which represents some kind of context. And I want this code to fail if the map does not contain some of the required properties.
The corresponding code might look like this:
SomeResultType businessMethod(Map<String, String> context) {
Assert.isTrue(context.containsKey("A"), "A property must not be empty");
Assert.isTrue(context.containsKey("B"), "B property must not be empty");
Assert.isTrue(context.containsKey("C"), "C property must not be empty");
// ...
}
I wrote a simple replacement by myself with signature like this
public static <K, V> void mapContainsKeys(Map<K, V> map, K... keys)
But I think that something like this must be already implemented somewhere. So I'm searching for library replacement of this code.
It would be great if Spring guys implemented something like this in org.springframework.util.Assert.
if you want to just check that a map contains a list of keys, use this:
map.keySet().containsAll(keys)
if you want more details, to know which ones were missing:
Set<String> missingKeys = new HashSet<>(keys);
missingKeys.removeAll(map.keySet());
and then:
if (!missingKeys.isEmpty()) {
throw new IllegalArgumentException(missingKeys + " keys missing");
}
The fact that you had to write a helper method means that you are probably manipulating maps all over your code.
Looks like a code smell to me, you should probably map your properties to an object that you can validate once and pass everywhere.
Since you are using Spring, and if you are using Spring Boot (most people do nowadays), you can use #ConfigurationProperties to map your configuration properties to an object.
This way you can also add validation like #javax.validation.constraints.NotNull and make sure your properties are valid.

Spring Configuration creating complex data structure with yaml

Using yaml in my Spring-boot application (with snakeyaml dependency 1.16) I am attempting to create a #ConfigurationProperties based off of my application.yml file. I want to create a data structure like the json below which is a Map with String Keys and Array values.
mapName: {
"key1": ["elem0","elem1"],
"key2": ["hello","world"]
}
Attempting to create a Spring configuration class as follows
#Component
#ConfigurationProperties(prefix = "channel-broker")
#EnableConfigurationProperties
public class BrokerConfiguration {
private Map<String, Set<String>> broker = new HashMap<>();
public Map<String, Set<String>> getBroker() {
return broker;
}
}
I have tried the following for my yaml
channel-broker:
broker: {message-delivery: ['all'], facebook: ['client1']}
Attempt two
channel-broker:
message-delivery: ['all']
facebook: ['client1']
Attempt three
channel-broker:
message-delivery:
- ['all']
facebook:
- ['client1']
I have also tried initializing the HashMap in the #ConfigurationProperties class as such ... new HashMap<String, Set<String>> this didn't work either
All attempts result in this error which makes me believe its an error when converting to the object not that there is anything wrong with the yaml syntax.
Caused by: org.springframework.beans.InvalidPropertyException: Invalid
property 'brokerTest[message-delivery][0]' of bean class
[my.classpackage.clasname]:
Property referenced in indexed property path
'brokerTest[message-delivery][0]' is neither an array nor a List nor a
Map; returned value was [all]
Is it possible to create such an object? How would I accomplish this
-UPDATE-
If I change the Set to an ArrayList (or List interface) this works but that isn't what I'm looking for. changed to this
private Map<String, ArrayList<String>> brokerTest = new HashMap<>();
but need this doesn't work with Set interface either:
private Map<String, HashSet<String>> brokerTest = new HashMap<>();
This issue was being caused by the format of the yaml file. The following structure allowed me to build my graph like data structure out of yaml
channel-broker:
broker:
message-delivery:
all
facebook:
client1,client2
The Set doesn't want anything extra surrounding the key. Note if your Set will contain multiple values you can add a comma to separate them. Just like Json the last element will not have a comma after.
What you are looking for is this :
channel-broker: {broker: {message-delivery:['all', ...], facebook:['client1', ...]}}
see Complete idiot's introduction to yaml
If you use [] then it's an array so arraylist works, for hashset/hashmap you need to use {} brackets.
channel-broker: {
broker: {
message-delivery:{'all', '123'},
facebook:{'client1', 'cleant2'}
}
}
will work for hashset.
(hashmap example)

Categories