Deserialize property file that contains dot with Jackson - java

In our application we are trying to read a flat property file using Jackson to match the properties to our POJO
Everything works fine, but when the property name contains some dot, the wholo POJO is set to null
Here is a sample of the property file
p.test=Just a test
Here is my POJO
public class BasicPOJO {
#JsonProperty("p.test")
private String test;
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
}
}
And here is how I map it
InputStream in = ApplicationProperties.class.getClassLoader()
.getResourceAsStream("application.properties");
JavaPropsMapper mapper = new JavaPropsMapper();
try {
BasicPOJO myProperties = mapper.readValue(in,
BasicPOJO .class);
LOGGER.debug("Loaded properties {}", myProperties); //myProperties.test is null here
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
Any help will be appreciated

Dot in property names is used to represent nested objects.
This is described here
https://github.com/FasterXML/jackson-dataformats-text/blob/master/properties/README.md#basics-of-conversion
Since default java.util.Properties can read "flat" key/value entries in, what is the big deal here?
Most properties files actually use an implied structure by using a naming convention; most commonly by using period ('.') as logical path separator.
You can disable it by using JavaPropsSchema.withoutPathSeparator() as described here https://github.com/FasterXML/jackson-dataformats-text/blob/master/properties/README.md#javapropsschemapathseparator
JavaPropsSchema schema = JavaPropsSchema.emptySchema()
.withoutPathSeparator();
BasicPOJO myProperties = mapper.readerFor(BasicPOJO.class)
.with(schema)
.readValue(source);

Related

How can I avoid a ClasstCastException when when deserializing a nested Map containing a custom class using SnakeYaml?

I am attempting to use SnakeYaml to deserialize an object into the correct Java object in Spring boot.
My Yaml looks like this:
workflows:
flow1:
firstPage:
nextPages:
- pageName: secondPage
secondPage:
nextPages:
- pageName: thirdPage
flow2:
firstPage:
nextPages:
- pageName: fancyPage
fancyPage:
nextPages:
The object this yaml represents is:
Map<String flowName, Map<String pageName, Class PageConfiguration>>
This data structure is a field (workflows) in class I have defined called ApplicationConfiguration.
PageConfiguration is a custom class that contains a single field of a custom class NextPage which is another class that contains a single field String pageName.
The object was originally just Map<String, PageConfiguration> and that deserialized fine, but I wanted to extend the functionality to allow for multiple flows hence the new Map<String, Map<String, PageConfiguration>> data structure.
The new nested data structure is throwing an ClassCastException error, cannot cast LinkedHashMap to PageConfiguration when I try to access the values contained within.
Here is my deserialization code: (note configPath is the correct path to the YAML file)
public ApplicationConfiguration getObject() {
ClassPathResource classPathResource = new ClassPathResource(configPath);
LoaderOptions loaderOptions = new LoaderOptions();
loaderOptions.setAllowDuplicateKeys(false);
loaderOptions.setMaxAliasesForCollections(Integer.MAX_VALUE);
loaderOptions.setAllowRecursiveKeys(true);
Yaml yaml = new Yaml(new Constructor(ApplicationConfiguration.class), new Representer(),
new DumperOptions(), loaderOptions);
ApplicationConfiguration appConfig = null;
try {
appConfig = yaml.load(classPathResource.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}
return appConfig;
}
And how I am attempting to access it when I get the ClassCastException:
public PageWorkflowConfiguration getPageWorkflow(String flowName, String pageName) {
return this.workflows.get(flowName).get(pageName);
}
It seems like the additional layer of nesting caused SnakeYaml to fail to deserialize correctly causing the ClassCastException error, cannot cast LinkedHashMap to PageConfiguration?

Junit test class for dependency with value from application.properties

I am writing Junit test case for the following class :
#Component
public class ExpandParam {
/* expand parameter with value "expand" */
#Value("${api.expand.value}")
private String expandParam;
public MultiValueMap<String, String> getExpandQueryParam(String[] expand) {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
// Creating comma separated format string
StringBuilder builder = new StringBuilder();
for (String value : expand) {
if(!expand[expand.length-1].equals(value)) {
builder.append(value+", ");
}
else {
builder.append(value);
}
}
String expandText = builder.toString();
queryParams.add(expandParam, expandText);
return queryParams;
}
}
The test class is following :
public class ExpandParamTest {
#InjectMocks
#Spy
ExpandParam expandQueryParam;
// #Value("${api.expand.value}")
// private String expandParam;
private String[] expand = {"fees"};
#Before
public void setup() {
ReflectionTestUtils.setField(expandQueryParam, "expandParam", "expand");
}
#Test
public void testExpandParam() {
MultiValueMap<String, String> queryParams = expandQueryParam.getExpandQueryParam(expand);
try {
System.out.println(new ObjectMapper().writeValueAsString(queryParams));
} catch (JsonProcessingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
In application. properties files I have set the values :
#expand param
api.expand.value: expand
I am new to this, can any one tell me where I am making the mistake:
Getting the following error:
java.lang.IllegalArgumentException: Either targetObject or targetClass for the field must be specified
at org.springframework.util.Assert.isTrue(Assert.java:121)
at org.springframework.test.util.ReflectionTestUtils.setField(ReflectionTestUtils.java:178)
at org.springframework.test.util.ReflectionTestUtils.setField(ReflectionTestUtils.java:107)
at org.springframework.test.util.ReflectionTestUtils.setField(ReflectionTestUtils.java:91)
at com.aig.rs.products.orchestrator.api.utils.ExpandParamTest.setup(ExpandParamTest.java:29)
#Value is a spring annotation, it depends on the Spring Context to function. If you want #Value to read the value from your application properties then you need to convert your unit test into a #SpringBootTest. Take a look at this tutorial to understand a bit more about Spring Test.
You're also using ReflectionTestUtils.setField(expandQueryParam, "expandParam", "expand"); which will just set a value to this field, not read it from properties. This exception you're seeing is because expandQueryParam is null, these annotations #Spy and #InjectMocks are Mockito annotations and for them to initialize your object you need to enable mockito annotations, you can do this by adding #ExtendWith(MockitoExtension.class) on top of your class or using MockitoAnnotations.initMocks(this) in setUp method.
I don't think you need mockito to test this class, in my opinion going for a Spring Test would be a better option this way you can also test the reading of the property key.

SnakeYaml "Unable to find property error"

Here is part of my config.yml:
#Authenctication
AuthenticationConfig:
AuthencticationType: LDAP
LDAPConfig:
LDAPUrl: ldap://localhost:389
ConnectionType: simple
LDAPSecurityConfig:
RootDN: cn=manager,dc=maxcrc,dc=com
RootPassword: secret
UserSearchDN: ou=People,dc=maxcrc,dc=com
GroupdSearchDB: ou=Groups,dc=maxcrc,dc=com
I have a class used for parsing:
public class YamlConfiguraiton {
private AuthenticationConfiguration AuthenticationConfig;
public void setAuthenticationConfig(AuthenticationConfiguration AuthenticationConfig) {
this.AuthenticationConfig = AuthenticationConfig;
}
public AuthenticationConfiguration getAuthenticationConfig() {
return this.AuthenticationConfig;
}
}
However, when I run
try(InputStream in = new FileInputStream(new File(ymalPath))) {
yamlConfig = yaml.loadAs(in, YamlConfiguraiton.class);
} catch (IOException e) {
e.printStackTrace();
}
the following error happens:
Exception in thread "main" Cannot create property=AuthenticationConfig for JavaBean=com.ibm.entity.matching.common.bootstrap.YamlConfiguraiton#e7860081
in 'reader', line 2, column 1:
AuthenticationConfig:
^
Unable to find property 'AuthenticationConfig' on class: com.ibm.entity.matching.common.bootstrap.YamlConfiguraiton
in 'reader', line 3, column 4:
AuthencticationType: LDAP
^
at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.constructJavaBean2ndStep(Constructor.java:270)
at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.construct(Constructor.java:149)
at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:309)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructObjectNoCheck(BaseConstructor.java:204)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:193)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructDocument(BaseConstructor.java:159)
at org.yaml.snakeyaml.constructor.BaseConstructor.getSingleData(BaseConstructor.java:146)
at org.yaml.snakeyaml.Yaml.loadFromReader(Yaml.java:524)
at org.yaml.snakeyaml.Yaml.loadAs(Yaml.java:518)
at com.ibm.entity.matching.bootstrap.EntityMatching.boot(EntityMatching.java:55)
at com.ibm.entity.matching.bootstrap.EntityMatching.main(EntityMatching.java:35)
Caused by: org.yaml.snakeyaml.error.YAMLException: Unable to find property 'AuthenticationConfig' on class: com.ibm.entity.matching.common.bootstrap.YamlConfiguraiton
at org.yaml.snakeyaml.introspector.PropertyUtils.getProperty(PropertyUtils.java:159)
at org.yaml.snakeyaml.introspector.PropertyUtils.getProperty(PropertyUtils.java:148)
at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.getProperty(Constructor.java:287)
at org.yaml.snakeyaml.constructor.Constructor$ConstructMapping.constructJavaBean2ndStep(Constructor.java:208)
... 10 more
Why it complains about cannot finding property AuthenticationConfig while AuthenticationConfig is just the name of the instance variable?
UPDATE
After I changed the instance variables from "private" to "public", they were recognized by SnakeYaml, but this is not what we expect for sure. The classes are not recognized as JavaBean.
UPDATE
I found the root cause. It is the naming convention. If you want SnakeYaml to parse your yaml file, camelCase has to be complied with. The name of setter and getter method is also important. Say there is a private instance variable called ldapConfig, then its getter and setter's name has to be getLdapConfig and setLdapConfig, even getLDAPConfig and setLDAPConfig won't work.
The main reason for the error is that you need to define all the attributes present in Yaml file in POJO class (i.e. YamlConfiguraiton).
You can use the below code to skip the undefined properties.
Representer representer = new Representer();
representer.getPropertyUtils().setSkipMissingProperties(true);
Yaml yaml = new Yaml(new Constructor(YamlConfiguraiton.class), representer);
Firstly, rename the attribute names to camelCase in Yaml file.
Refer the below code:-
Code:-
public class YamlReadCustom {
private static String yamlPath = "/authentication.yaml";
public static void main(String[] args) {
Representer representer = new Representer();
representer.getPropertyUtils().setSkipMissingProperties(true);
Yaml yaml = new Yaml(new Constructor(Authentication.class), representer);
try(InputStream in = YamlReadCustom.class.getResourceAsStream (yamlPath)) {
Authentication authentication = yaml.loadAs(in, Authentication.class);
System.out.println(authentication.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
}
Authentication:-
public class Authentication {
private String authenticationConfig;
private String authencticationType;
private LdapConfig ldapConfig;
//getters and setters
}
LdapConfig:-
public class LdapConfig {
private String ldapUrl;
private String connectionType;
private Map<String, Object> ldapSecurityConfig;
//getters and setters
}
authentication.yaml
authenticationConfig:
authencticationType: LDAP
ldapConfig:
ldapUrl: ldap://localhost:389
connectionType: simple
ldapSecurityConfig:
rootDn: cn=manager,dc=maxcrc,dc=com
rootPassword: secret
userSearchDn: ou=People,dc=maxcrc,dc=com
groupdSearchDb: ou=Groups,dc=maxcrc,dc=com

Spring Auto Create Properties File

I have a simple class to represent property file using #PropertySource like so
#Component
#PropertySource(value = "file:${user.home}/.cooperp/app-conf.properties")
public class ApplicationProperties {
#Value("${mysql-location}")
private String mysqlLocation;
public String getMysqlLocation() {
return mysqlLocation;
}
public void setMysqlLocation(String mysqlLocation) {
this.mysqlLocation = mysqlLocation;
}
}
I know if file is not found one can add ignoreResourceNotFound = true which will make spring to ignore its absence and startup the application.
I would like spring to create the file it not found, not ignore or throw exception.
Usually we keep the properties file withing the project directory. But if your project requires the properties outside then you can check if the file exists in SpringBootApplication class. If it doesen't, then you can create the properties file there. Code example :
#SpringBootApplication
public class SpringDocBotApplication {
public static void main(String[] args) {
File file = new File("EXPECTED PATH");
try{
if(!file.exists()){
// create propeties file add properties
}
} catch (IOException e) {
//HANDLE EXCEPTION
}
SpringApplication.run(SpringDocBotApplication.class, args);
}
}

Spring Boot with #JsonIgnore [duplicate]

This question already has an answer here:
Return some object in my Json
(1 answer)
Closed 6 years ago.
If I need to ignore the serialization/deserialization from one field I put the #JsonIgnore on that field:
#JsonIgnore
private String name;
But if I only need in serialization or only in deserealization I put in getter or setter method.
but it`s possible to use in some method? If my user call:
http://myApp/public/user/1
I need to ignore the properties pass for example, but if my user call:
http://myApp/private/user/1
I don't need to ignore the properties pass
Is it possible?
You can achieve this by using Json Filter with a FilterProvider
you use #JsonFilter annotation to assign a filter name to your POJO.
before serialization, you attach an instance of SimpleBeanPropertyFilter to the filter name. the class has two factory methods for filters that work based on propertry names.
Here is an example of the annotation declaration:
#JsonFilter("filterByName")
public class So {
public String alwaysWrite = "alwaysWriteValue";
public String somtimeIgnore = "somtimeIgnoreValue";
}
here is a method that assignes a filter that will ignore somtimeIgnore property
public void dontWriteSomtimes(So so) {
ObjectMapper mapper = new ObjectMapper();
FilterProvider filters = new SimpleFilterProvider()
.addFilter("filterByName", SimpleBeanPropertyFilter.serializeAllExcept("somtimeIgnore"));
ObjectWriter writer = mapper.writer(filters);
try {
System.out.println(writer.writeValueAsString(so));
} catch (Exception e) {
e.printStackTrace();
}
}
output:
{"alwaysWrite":"alwaysWriteValue"}
here is a method that will write everything: just assign a non-existent or empty property:
public void writeEveryting(So so) {
ObjectMapper mapper = new ObjectMapper();
FilterProvider filters = new SimpleFilterProvider()
.addFilter("filterByName", SimpleBeanPropertyFilter.serializeAllExcept(""));
ObjectWriter writer = mapper.writer(filters);
try {
System.out.println(writer.writeValueAsString(so));
} catch (Exception e) {
e.printStackTrace();
}
}
output:
{"alwaysWrite":"alwaysWriteValue","somtimeIgnore":"somtimeIgnoreValue"}

Categories