I have a Spring Boot application with the following application.yml
Detail_1:
name: X,Y,Z
place: xplace,yplace,zplace
Detail_2:
name: X,Y,Z
place: xplaceanother,yplaceanother,zplaceanother
How can I obtain this map in java:
X {
detail1 :xplace
detail2 :xplaceanother
}
Y {
detail1:yplace,
detail2:yplaceanother
}
Z{
detail1:zplace,
detail2:zplaceanother
}
I have tried the following code :
#Value${detail1.name}
private String names;
#value${detail2.place}
List<Object> Names = Arrays.asList(getNames().split(","));
List<Object> places = Arrays.asList(getPlaces().split(","));
Then I tried to create a map of names and places corresponding to detail 1
similarly I fetched names and places for detail 2
But In this case i end up with 2 maps , one for detail 1 and one for detail 2.
I need to create a single map.
You need to use #ConfigurationProperties annotation
The following URLs provide good examples in both .properties and .yml format:
https://www.mkyong.com/spring-boot/spring-boot-configurationproperties-example/
https://www.baeldung.com/configuration-properties-in-spring-boot
Please update your config like below in application.yml
map:
detail1:
name:X,Y,Z
place:xplace,yplace,zplace
detail2:
name:X,Y,Z
place:xplaceanother,yplaceanother,zplaceanother
and then configure the property as below,
DetailConfig.java
#Component
#ConfigurationProperties(prefix="map")
public class DetailConfig {
private Map<String, Object> detail1;
private Map<String, Object> detail2;
public Map<String, Object> getDetail1() {
return detail1;
}
public void setDetail1(Map<String, Object> detail1) {
this.detail1 = detail1;
}
public Map<String, Object> getDetail2() {
return detail2;
}
public void setDetail2(Map<String, Object> detail2) {
this.detail2 = detail2;
}
}
You can use the following pojo for property;
public class Detail {
private List<String> name;
private List<String> place;
public Map<String, String> getNamePlaceMap() {
return IntStream.range(0, name.size()).boxed()
.collect(Collectors.toMap(i -> name.get(i), i -> place.get(i)));
}
// getters/setters
}
and use the following configuration to get properties into context;
#Configuration
public class Config {
#Bean
#ConfigurationProperties(prefix = "detail-1")
public Detail detailOne() {
return new Detail();
}
#Bean
#ConfigurationProperties(prefix = "detail-2")
public Detail detailTwo() {
return new Detail();
}
}
and autowire them and pass them to the logic where that map is created;
#Service
public class TestService {
#Autowired
private Detail detailOne;
#Autowired
private Detail detailTwo;
public void test() {
System.out.println(createSpecialMap(detailOne, detailTwo));
}
private static Map<String, Map<String, String>> createSpecialMap(Detail detailOne, Detail detailTwo) {
Map<String, Map<String, String>> resultMap = new HashMap<>();
detailOne.getNamePlaceMap().forEach((key, value) -> {
Map<String, String> subMap = resultMap.getOrDefault(key, new HashMap<>());
subMap.put("detail1", value);
resultMap.put(key, subMap);
});
detailTwo.getNamePlaceMap().forEach((key, value) -> {
Map<String, String> subMap = resultMap.getOrDefault(key, new HashMap<>());
subMap.put("detail2", value);
resultMap.put(key, subMap);
});
return resultMap;
}
}
results in;
{
X={detail1=xplace, detail2=xplaceanother},
Y={detail1=yplace, detail2=yplaceanother},
Z={detail1=zplace, detail2=zplaceanother}
}
Or better in readability, using a Letter class;
public class Letter {
private String name;
private String detail1;
private String detail2;
public Letter(String name, String detail1, String detail2) {
this.name = name;
this.detail1 = detail1;
this.detail2 = detail2;
}
// getters/setters
}
doing the following;
private static List<Letter> createList(Detail detailOne, Detail detailTwo) {
List<Letter> resultList = new ArrayList<>();
Map<String, String> detailOneMap = detailOne.getNamePlaceMap();
Map<String, String> detailTwoMap = detailTwo.getNamePlaceMap();
Set<String> keySet = new HashSet<>();
keySet.addAll(detailOneMap.keySet());
keySet.addAll(detailTwoMap.keySet());
return keySet.stream()
.map(key -> new Letter(key, detailOneMap.get(key), detailTwoMap.get(key)))
.collect(Collectors.toList());
}
will result in;
[
Letter{name='X', detail1='xplace', detail2='xplaceanother'},
Letter{name='Y', detail1='yplace', detail2='yplaceanother'},
Letter{name='Z', detail1='zplace', detail2='zplaceanother'}
]
which is a better result than a raw map of map...
Related
I have a Class like this:
public class MyClass
{
private int id;
private Map<String, String> myMap;
public Map<String, String> getMyMap()
{
return myMap;
}
public void setMyMap(Map<String, String> myMap)
{
this.myMap = myMap;
}
}
I added new setter method(overloading) because i didn't want to do set HashMap directly, and that's what you see now :
public class MyClass
{
private int id;
private Map<String, String> myMap;
public Map<String, String> getMyMap()
{
return myMap;
}
public void setMyMap(Map<String, String> myMap)
{
this.myMap = myMap;
}
public void setMyMap(String key , String value)
{
setMyMap(new HashMap<>(){{put(key, value);}});
}
}
But because i used new HashMap<>(){{put(key, value);}} keyword every time i use this method , it create new Map and last items deleted .
So i have 2 question:
1-correct solution for set items by 2nd setter method
2-how i could use this setter method for multiple put's for this situations:
MyClass.setMyMap(new HashMap<>()
{{
put("title", title);
put("id", id);
}});
Thank you guys for your time .
It depends on what your class does. But in general, I would not expose a setter for a map field.
It makes sense to add a constructor with a map argument, then do something like this:
public class MyClass
{
private final int id;
private final Map<String, String> myMap;
public MyClass(int id, Map<String, String> myMap) {
this.id = id;
this.myMap = myMap;
}
public Map<String, String> getMyMap()
{
return myMap;
}
public void addPairs(Map<String, String> pairs)
{
myMap.putAll(pairs);
}
public void addPair(String key, String value)
{
myMap.put(key, value);
}
}
Of course, you can expose an additional constructor:
public MyClass(int id) {
this.id = id;
this.myMap = new HashMap<>();
}
Try some thing like this:
public void setMyMap(String key , String value) {
if(myMap == null)
myMap = new HashMap<String, String>();
myMap.put(key, value);
}
You've already declared class field myMap and you want to use it in setMyMap method.
Do null check. If the field is null then create a new map. Then use put method to store data in the map.
I have a Java interface PlatformConfigurable. I also have two classes PlatformProducerConfig and PlatformConsumerConfig.
Later on, I need to add a common config to both that sets a property to an empty string:
private PlatformConfigurable disableHostNameVerificationConfig(PlatformConfigurable platformConfig) {
if (platformConfig instanceof PlatformProducerConfig) {
PlatformProducerConfig oldConfig = (PlatformProducerConfig) platformConfig;
Map<String, String> additionalConfig = oldConfig.additionalProperties();
Map<String, String> newConfig = new HashMap<>(Optional.ofNullable(additionalConfig).orElseGet(ImmutableMap::of));
newConfig.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, "");
return oldConfig.toBuilder().additionalProperties(newConfig).build();
}
else if (platformConfig instanceof PlatformConsumerConfig) {
PlatformConsumerConfig oldConfig = (PlatformConsumerConfig) platformConfig;
Map<String, String> additionalConfig = platformConfig.additionalProperties();
Map<String, String> newConfig = new HashMap<>(Optional.ofNullable(additionalConfig).orElseGet(ImmutableMap::of));
newConfig.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, "");
return oldConfig.toBuilder().additionalProperties(newConfig).build();
}
return platformConfig;
}
I am casting to producer or consumer config because the PlatformConfigurable interface doesn't have .toBuilder() or .build() methods declared in it, and I don't have access to modify the interface, as I can only implement it.
I would want to get rid of the duplicate code:
Map<String, String> additionalConfig = platformConfig.additionalProperties();
Map<String, String> newConfig = new HashMap<>(Optional.ofNullable(additionalConfig).orElseGet(ImmutableMap::of));
newConfig.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, "");
return oldConfig.toBuilder().additionalProperties(newConfig).build();
I was thinking of using lambdas, but I am not 100% sure how to do it.
You could just refactor existing code like this:
private PlatfromConfigurable disableHostNameVerificationConfig(Platfromonfigurable platfromConfig) {
if (!(platformConfig instanceof PlatformProducerConfig) && !(platformConfig instanceof PlatformConsumerConfig)) {
return platformConfig;
}
Map<String, String> additionalConfig = platformConfig.additionalProperties();
Map<String, String> newConfig = new HashMap<>(Optional.ofNullable(additionalConfig).orElseGet(ImmutableMap::of));
newConfig.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, "");
if (platformConfig instanceof PlatformProducerConfig) {
return ((PlatformProducerConfig)platformConfig).toBuilder().additionalProperties(newConfig).build();
}
return ((PlatformConsumerConfig)platformConfig).toBuilder().additionalProperties(newConfig).build();
}
Update
Another approach could be to extract functionality related to the builder to separate interfaces and use them in this way:
// 1. extend existing `PlatformConfigurable`
public interface BuilderedPlatformConfigurable extends PlatformConfigurable {
ConfigPlatformBuilder toBuilder();
}
// 2. provide builder interface with common implementation
public interface ConfigPlatformBuilder {
Map<String, String> additionalProperties = new HashMap<>();
BuilderedPlatformConfigurable build();
default ConfigPlatformBuilder additionalProperties(Map<String, String> properties) {
this.additionalProperties.clear();
this.additionalProperties.putAll(properties);
return this;
}
}
// 3. update PlatformConsumerConfig class (similarly, PlatformProducerConfig)
public class PlatformConsumerConfig implements BuilderedPlatformConfigurable {
private Map<String, String> additionalProperties = new HashMap<>();
#Override
public Map<String, String> additionalProperties() {
return additionalProperties;
}
public ConfigPlatformBuilder toBuilder() {
return new Builder();
}
public static class Builder implements ConfigPlatformBuilder {
public PlatformConsumerConfig build() {
PlatformConsumerConfig config = new PlatformConsumerConfig();
config.additionalPropertie.putAll(this.additionalProperties);
return config;
}
}
}
// 4. provide overloaded method
private PlatformConfigurable disableHostNameVerificationConfig(PlatformConfigurable platformConfig) {
return platformConfig;
}
private PlatformConfigurable disableHostNameVerificationConfig(BuilderedPlatformConfigurable platformConfig) {
Map<String, String> additionalConfig = platformConfig.additionalProperties();
Map<String, String> newConfig = new HashMap<>(Optional.ofNullable(additionalConfig).orElseGet(Map::of));
newConfig.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, "");
return platformConfig.toBuilder().additionalProperties(newConfig).build();
}
Taking Alex Rudenko's answer a bit further, using generics:
private <P extends PlatformConfigurable> P disableHostNameVerificationConfig(P platformConfig, BiFunction<P, Map<String, String>, P> appender) {
Map<String, String> additionalConfig = platformConfig.additionalProperties();
Map<String, String> newConfig = new HashMap<>(Optional.ofNullable(additionalConfig).orElseGet(ImmutableMap::of));
newConfig.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, "");
return appender.apply(platformConfig, newConfig);
}
This assumes that it is safe to do this for any subtype of PlatformConfigurable (and PlatformConfigurable itself).
Then invoke like:
disableHostNameVerificationConfig(
platformProducerConfig,
(p, config) -> p.toBuilder().setAdditionalConfig(config).build());
disableHostNameVerificationConfig(
platformConsumerConfig,
(p, config) -> p.toBuilder().setAdditionalConfig(config).build());
If you like, create helper methods to hide the BiFunctions:
private PlatformProducerConfig disableHostNameVerificationConfig(PlatformProducerConfig config) {
return disableHostNameVerificationConfig(
platformConfigurable,
(p, config) -> p.toBuilder().setAdditionalConfig(config).build());
}
private PlatformConsumerConfig disableHostNameVerificationConfig(PlatformConsumerConfig config) {
return disableHostNameVerificationConfig(
platformConfigurable,
(p, config) -> p.toBuilder().setAdditionalConfig(config).build());
}
Actually, I think a better way to do it would be without generics or lambdas: write a method which creates an updated map:
private static Map<String, String> newConfig(PlatformConfigurable platformConfig) {
Map<String, String> additionalConfig = platformConfig.additionalProperties();
Map<String, String> newConfig = additionalConfig != null ? new HashMap<>(additionalConfig) : new HashMap<>();
newConfig.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, "");
return newConfig;
}
and then just have two overloads:
private PlatformProducerConfig disableHostNameVerificationConfig(PlatformProducerConfig config) {
return config.toBuilder().setAdditionalConfig(newConfig(config)).build();
}
private PlatformConsumerConfig disableHostNameVerificationConfig(PlatformConsumerConfig config) {
return config.toBuilder().setAdditionalConfig(newConfig(config)).build();
}
Adding one thing in Alex Rudenko's answer, and making different function to add different implementations of interfaces.
private PlatformConfigurable disableHostNameVerificationConfig(PlatformConfigurable platformConfig) {
if ((platformConfig == null)) {
return platformConfig;
}
Map<String, String> additionalConfig = platformConfig.additionalProperties();
Map<String, String> newConfig = new HashMap<>(Optional.ofNullable(additionalConfig).orElseGet(ImmutableMap::of));
newConfig.put(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, "");
return PlatformConfigurableObject(platformConfig, newConfig);
}
So you can handle all instances in a different method, and whenever PlatfromXCofing classes are added later you only have to change this method. Single Responsibility Principle.
private PlatformConfigurable PlatformConfigurableObject(PlatformConfigurable platformConfig, Map<String, String> newConfig){
if (platformConfig instanceof PlatformProducerConfig) {
return ((PlatformProducerConfig)platformConfig).toBuilder().additionalProperties(newConfig).build();
} else if (platformConfig instanceof PlatformConsumerConfig){
return ((PlatformConsumerConfig)platformConfig).toBuilder().additionalProperties(newConfig).build();
} else{
return platformConfig;
}
}
I'm trying to unit test a class having the following function:
private String getTabSeparateValues(final QueryParams params, final HttpServletRequest request) {
MetricsSerializer serializer = new MetricsSerializer();
return serializer.serializeValues(params, request);
}
It calls the "serializeValues(params,request)" function in the following class:
public class MetricsSerializer {
private final StringJoiner stringJoiner = new StringJoiner("\t");
private static final String MONTH_FORMAT = "MMMMM";
public String serializeMetrics(final QueryParams queryParams, final HttpServletRequest request) {
addValueFromString(queryParams.getId());
addValueFromString(getCurrentMonth());
addValueFromString(request.getRemoteUser().split("#")[0]);
addValueFromString(queryParams.getCurrency());
addValuesFromList(queryParams.getCompanies());
addValueFromString(queryParams.getCognosDatasetType());
addValuesFromList(queryParams.getScenarios());
addFilter(queryParams.getFilters());
addGroupings(queryParams.getGroupings());
addValueFromString(queryParams.getReportTemplate());
return stringJoiner.toString();
}
private void addValueFromString(final String value) {
stringJoiner.add(value);
}
private void addFilter(final List<Map<String, List<String>>> filters) {
List<String> collect = filters.stream()
.flatMap(entry -> entry.keySet()
.stream())
.collect(Collectors.toList());
addValuesFromList(collect);
}
private void addGroupings(final Map<String, List<String>> groupings) {
addValuesFromList(new ArrayList<>(groupings.keySet()));
}
private void addValuesFromList(final List<String> listValues) {
stringJoiner.add(listValues.stream().collect(Collectors.joining(" ")));
}
private String getCurrentMonth() {
DateFormat monthFormat = new SimpleDateFormat(MONTH_FORMAT);
return monthFormat.format(new Date());
}
}
This class returns the values in a tab-separated format. The structure of the QueryParams class is as follows:
#Data
#Builder
public class QueryParams {
private String datasetType;
private Map<String, List<String>> groupings;
private List<Aggregate> aggregates;
private List<String> scenarios;
private List<String> companies;
private List<Map<String, List<String>>> filters;
private List<NamedTimeRange> timeRanges;
private Map<String, String> params;
private String reportTemplate;
private String id;
private String currency;
}
I am not using all of the parameters in the QueryParameters (Skipping timeranges, params, aggregates). In order to test if I'm actually getting tab-separated metrics, I wrote the following test:
public class MetricsHandlerTest {
private QueryParams queryParams;
#Before
public void setUp() throws Exception {
List<String> list = Arrays.asList("one", "two");
List<String> scenarioList = Arrays.asList("A1");
List<Map<String, List<String>>> filter = new ArrayList<>();
Map<String, List<String>> filtersMap = new HashMap<>();
List<String> filterList = Arrays.asList("COM");
filtersMap.put("product", filterList);
filter.add(filtersMap);
queryParams = QueryParams.builder()
.id("123").currency("USD").companies(list).scenarios(scenarioList).filters(filter)
.build();
}
#Test
public void tabSerializerTest() {
String remoteUser = "testuser";
HttpServletRequest httpServletRequest = Mockito.mock(HttpServletRequest.class);
Mockito.when(httpServletRequest.getRemoteUser())
.thenReturn(remoteUser);
MetricsSerializer metricsSerializer = new MetricsSerializer();
String tabs = metricsSerializer.serializeMetrics(queryParams, httpServletRequest);
assertEquals(tabs, "123 \t USD \t one two");
}
}
I'm getting a null pointer exception for groupings. (in "addGroupings" of MetricsSerializer class) I tried logging groupings and it can be null. For scenarios and filters, I have added values. How do I handle this for groupings where it can be null?
Any help regarding how to fix this would be greatly appreciated.
I am not sure if I understand your issue. Is MetricsSerializer under your control? If so then you need to make it defensive to have null check to handle the case where groupings is null.
If it is outside of your control, then you need to build your query with an empty Group(rather than null) like this
queryParams = QueryParams.builder().withGroupings(new HashMap<String,List<String>>()).build();
I want to provide a POST servlet that takes the following JSON content:
{
"name": John
"age": 25,
"some": "more",
"params: "should",
"get": "mapped"
}
Two of those properties should be explicit mapped to defined parameters. All other parameters should go into a Map<String, String>.
Question: how can I let Spring map them directly into the map of the bean?
#RestController
public void MyServlet {
#PostMapping
public void post(#RequestBody PostBean bean) {
}
}
public class PostBean {
private String name;
private String age;
//all other json properties should go here
private Map<String, String> map;
}
public class PostBean {
private Map<String, String> map;
#JsonAnyGetter
public Map<String, String> getMap() {
return map;
}
#JsonAnySetter
public void setMap(String name, String value) {
if (this.map == null) map = new HashMap<>();
this.map.put(name, value);
}
}
I have a class which looks like this:
#JsonFormat(shape=JsonFormat.Shape.OBJECT)
public class MyMap implements Map<String, String>
{
protected Map<String, String> myMap = new HashMap<String, String>();
protected String myProperty = "my property";
public String getMyProperty()
{
return myProperty;
}
public void setMyProperty(String myProperty)
{
this.myProperty = myProperty;
}
//
// java.util.Map mathods implementations
// ...
}
And a main method with this code:
MyMap map = new MyMap();
map.put("str1", "str2");
ObjectMapper mapper = new ObjectMapper();
mapper.getDeserializationConfig().withAnnotationIntrospector(new JacksonAnnotationIntrospector());
mapper.getSerializationConfig().withAnnotationIntrospector(new JacksonAnnotationIntrospector());
System.out.println(mapper.writeValueAsString(map));
When executing this code I'm getting the following output: {"str1":"str2"}
My question is why the internal property "myProperty" is not serialized with the map?
What should be done to serialize internal properties?
Most probably you will end up with implementing your own serializer which will handle your custom Map type. Please refer to this question for more information.
If you choose to replace inheritance with composition, that is to make your class to include a map field not to extend a map, then it is pretty easy to solve this using the #JsonAnyGetter annotation.
Here is an example:
public class JacksonMap {
public static class Bean {
private final String field;
private final Map<String, Object> map;
public Bean(String field, Map<String, Object> map) {
this.field = field;
this.map = map;
}
public String getField() {
return field;
}
#JsonAnyGetter
public Map<String, Object> getMap() {
return map;
}
}
public static void main(String[] args) throws JsonProcessingException {
Bean map = new Bean("value1", Collections.<String, Object>singletonMap("key1", "value2"));
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(map));
}
}
Output:
{"field":"value1","key1":"value2"}