I have a service class which transforms JSON from one pattern to other using DynamoDB.
This class have various methods to manipulate JSON fields, as shown below. I have to write JUnit
tests on this code using Mockito.
The convertToProviderJson method transform one JSON which is coming from 3rd party, to a predefined template and following methods are manipulating details present on the transformed JSON.
I am new to JUnit & Mockito, how shall I proceed?
```
#Service
public class ServiceClass {
public String convertToProviderJson(String consumerString, String providerTemplateJson)
throws JsonProcessingException {
//create ObjectMapper instance
ObjectMapper objectMapper = new ObjectMapper();
//convert json file to map
String currentFieldName = "";
String currentTemplateKey = "";
boolean templateMatchError = false;
Map<?, ?> providerMap;
Map<String, Object> providerOutputMap = new LinkedHashMap<>();
System.out.println("Provider JSON");
if(!UtilityClass.isJSONValid(consumerString)) {
throw new MalformedJsonException("Incorrect Consumer Input JSON.");
}
if(!UtilityClass.isJSONValid(providerTemplateJson)) {
throw new MalformedJsonException("Incorrect Provider Template JSON.");
}
try {
JSONObject consumerJson = new JSONObject(consumerString);
providerMap = objectMapper.readValue(providerTemplateJson, Map.class);
//iterate over Provider Template map.
for (Map.Entry<?, ?> entry : providerMap.entrySet()) {
String key = (String) entry.getKey();
currentTemplateKey = key;
String value = (String) entry.getValue();
Pattern p = Pattern.compile(TransformationConstants.TEMPLATE_FUNCTION_REGEX);
Matcher matcher = p.matcher((CharSequence) entry.getValue());
if (matcher.matches()) {
String[] splitString = value.split(LEFT_ROUND_BRACKET);
String functionName = splitString[0];
String fieldName = splitString[1].split(RIGHT_ROUND_BRACKET)[0];
currentFieldName = fieldName;
Object fieldValue = invokeFunction(consumerJson, functionName, fieldName);
providerOutputMap.put(key, fieldValue);
} else {
templateMatchError = true;
break;
}
}
} catch(JsonEOFException e) {
throw new MalformedJsonException("Incorrect Provider Template JSON.");
} catch (Exception e) {
throw new MalformedJsonException("Field '" + currentFieldName + "' missing in input json.");
}
if(templateMatchError) {
throw new MalformedJsonException("Value for Field '" + currentTemplateKey
+ "' in template JSON is not in correct format.");
}
String outputJson = objectMapper.writeValueAsString(providerOutputMap);
System.out.println("Provider JSON: " + outputJson);
return outputJson;
}
private Object invokeFunction(JSONObject consumerJson, String functionName, String fieldName)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
TransformationService obj = new TransformationService();
Method method;
method = obj.getClass().getMethod(functionName, JSONObject.class, String.class);
return method.invoke(obj, consumerJson, fieldName);
}
public Object getField(JSONObject jsonObject, String fieldName) throws JSONException {
if(jsonObject.has(fieldName)) {
return jsonObject.get(fieldName);
}
throw new MalformedJsonException("Field '" + fieldName + "' missing in input json.");
}
}
I have tried to write Unit test on getField() method after going through some articles. Here's my code, I know its wrong, how shall I proceed?
#Test
public void hasFieldTest() {
JSONObject obj = new JSONObject();
obj.put("id", "1");
obj.put("name", "divyanka");
when(((Object) transformationMock.getField(jsonObjmock, "name")).thenReturn(objectMock);
JSONAssert.assertEquals("{id:1}", obj, 'strict':false);
}
To test (the getField method of) ServiceClass, i would approach like:
import static org.junit.jupiter.api.Assertions.assertThrows;
// ...
class ServiceClassTest {
//Object/Class under test:
private ServiceClass testee = new ServiceClass(); // create new or "obtain somewhere" (like #Autowired in Spring testing...)
//... test all methods, lines:
#Test
public void testGetFieldOK() {
// prepare "good" object:
JSONObject obj = new JSONObject();
obj.put("foo", "bar");
// TEST/invoke (!):
Object result = testee.getField(obj, "foo");
// assert (with your assertion framework/style):
// result is not null AND result == "bar"
// done!
}
#Test
public void testGetFieldException() {
// prepare "bad" object:
JSONObject obj = new JSONObject();
// Test/Expect exception -> https://stackoverflow.com/a/40268447/592355 ->:
MalformedJsonException thrown = assertThrows(
MalformedJsonException.class,
() -> testee.getField(obj, "foo"),
"Expected getField(obj, \"foo\") to throw, but it didn't"
);
//assert on exception (message):
assertTrue(thrown.getMessage().contains("Field 'foo' missing in input json."));
}
//create more tests like that... (-> coverage),
//.. WHEN real parameters, associated objects and class (invocations) are not applicable, mock them!
}
Thx to: https://stackoverflow.com/a/40268447/592355
And to summarize the main topics:
test a (as) real (as possible) object.
(try to) achieve coverage.
prefer "real implementations" to mocks, and use them only when real implementation is not applicable/too costly. (interfaces, external code/systems, ... internal code/systems, which "wiring" is too costly/not applicable and is covered by other tests.)
So in your code: ObjectMapper and TransformationService look like possible mocking candidates, but it is not worth, since they are created locally (in the methods to test).
UtilityClass could also be (power) mocked, but is it worth!?? :-)
In case UtilityClass would be an (external) productive (chargeable) API, you might want to mock it.
Related
The problem I am facing is that I want to ser/des null values when only it comes to non top-level attributes, and I have no idea how to achieve that. So let's say I have a User class:
Class User {
String name;
int id;
Address address;
}
And an Address class:
Class Address{
String street;
String city;
String country;
}
Right now, I can use below Gson instance to ser/des null values:
Gson gson = new GsonBuilder().serializeNulls().create();
Address address = new Address(null, "New York", "US");
User user = new User("Adam", 123, address);
String userJson = gson.toJson(user);
Output is:
{
"name": "Adam",
"id": 123,
"address": {
"street": null,
"city": "New York",
"country": "US"
}
}
However, I do NOT want to ser/des nulls when it comes to top-level attributes of User. For example for below User:
User user = new User("Adam", 123, null);
I want to have an output as below and without address field:
{
"name": "Adam",
"id": 123
}
I am now trying to use a customized serializer to hardcode every top-level attributes and remove them if they are null:
public class SerializerForUser implements JsonSerializer<ConfigSnapshot> {
#Override
public JsonElement serialize(User user, Type type, JsonSerializationContext jsc) {
Gson gson = new GsonBuilder().serializeNulls().create();
JsonObject jsonObject = gson.toJsonTree(user).getAsJsonObject();
if (user.getAddress() == null) {
jsonObject.remove("address");
}
// if... (same with other top-level attributes)
return jsonObject;
}
}
Gson gson = new GsonBuilder().serializeNulls().registerTypeAdapter(User.class, new SerializerForUser()).create();
But somehow it is not working, I will still get below output when for example address is null:
{
"name": "Adam",
"id": 123,
"address: null
}
can anyone give me some hints on what did I wrong here? Or it would be perfect if anyone can tell me if there is more straight forward/general way to achieve this(since I also want to use the same gson instance to ser/des other objects)?
Any comments are appreciated.
Because you are using
Gson gson = new GsonBuilder().serializeNulls().create();
which shows null value.
To skip showing null, let's try
Gson gson = new Gson();
You can test here
public static void main(String[] args) {
Gson yourGson = new GsonBuilder().serializeNulls().create(); // this is how you create your Gson object, which shows null value
Address address = new Address(null, "New York", "US");
User user = new User("Adam", 123, address);
String userJson = yourGson.toJson(user);
System.out.println(userJson);
Gson newGson = new Gson(); // with this one, it doesn't show value
System.out.println(newGson.toJson(user));
}
Update
I have tried to override the method serialize with a few times and it failed until I try #5
public class UserCustomSerializer implements JsonSerializer<User> {
#Override
public JsonElement serialize(User src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject obj = new JsonObject();
if (src.name != null) {
obj.addProperty("name", src.name);
}
obj.addProperty("id", src.id);
if (src.address != null) {
// try #1
//JsonObject addressJsonObj = new JsonObject();
//addressJsonObj.addProperty("street", src.address.street != null ? src.address.street : null);
//addressJsonObj.addProperty("city", src.address.city != null ? src.address.city : null);
//addressJsonObj.addProperty("country", src.address.country != null ? src.address.country : null);
//obj.add("address", addressJsonObj);
// try #2
//Gson gson = new GsonBuilder().serializeNulls().create();
//JsonElement jsonElement = gson.toJsonTree(src.address);
//obj.add("address", jsonElement);
// try #3
//Gson gson2 = new GsonBuilder().serializeNulls().create();
//obj.addProperty("address", gson2.toJson(src.address));
// try #4
//Gson gson = new GsonBuilder().serializeNulls().create();
//JsonObject jsonObject = gson.toJsonTree(src.address).getAsJsonObject();
//obj.add("address", jsonObject);
// try #5
JsonObject addressJsonObj = new JsonObject();
addressJsonObj.addProperty("street", src.address.street != null ? src.address.street : "null");
addressJsonObj.addProperty("city", src.address.city != null ? src.address.city : "null");
addressJsonObj.addProperty("country", src.address.country != null ? src.address.country : "null");
obj.add("address", addressJsonObj);
}
return obj;
}
}
For try #3, I built the incorrect String.
For try #1, #2 and #4, I have the problem with the null value. I searched and found the reason and also the suggestion here
In a JSON "object" (aka dictionary), there are two ways to represent absent values: Either have no key/value pair at all, or have a key with the JSON value null.
So you either use .add with a proper value what will get translated to null when you build the JSON, or you don't have the .add call.
And my #5 approach is to check if the child node is null, I just add the string "null" literally and then I replace it when I build the json string
private String parseToGson(User user){
Gson gson = new GsonBuilder().registerTypeAdapter(User.class, new UserCustomSerializer()).create();
return gson.toJson(user).replace("\"null\"", "null");
}
Here are some test cases I defined
#Test
public void children_attribute_is_null() throws Exception {
String expected = "{\"name\":\"Adam\","
+ "\"id\":123,"
+ "\"address\":{"
+ "\""+ "street\":null,"
+ "\"city\":\"New York\","
+ "\"country\":\"US"
+ "\"}"
+ "}";
Address address = new Address(null, "New York", "US");
User user = new User("Adam", 123, address);
assertEquals(expected, parseToGson(user));
Gson g = new Gson();
User usr = g.fromJson( parseToGson(user), User.class);
assertEquals("Adam", usr.name);
assertEquals(123, usr.id);
assertEquals(null, usr.address.street);
assertEquals("New York", usr.address.city);
assertEquals("US", usr.address.country);
}
#Test
public void parent_attribute_is_null() throws Exception {
String expected = "{\"name\":\"Adam\","
+ "\"id\":123" + "}";
User user = new User("Adam", 123, null);
assertEquals(expected, parseToGson(user));
Gson g = new Gson();
User usr = g.fromJson( parseToGson(user), User.class);
assertEquals("Adam", usr.name);
assertEquals(123, usr.id);
assertEquals(null, usr.address);
}
#Test
public void parent_attribute_and_children_attribute_are_null() throws Exception {
String expected = "{\"id\":123,"
+ "\"address\":{"
+ "\"street\":null,"
+ "\"city\":\"New York\","
+ "\"country\":\"US"
+ "\"}"
+ "}";
Address address = new Address(null, "New York", "US");
User user = new User(null, 123, address);
assertEquals(expected, parseToGson(user));
Gson g = new Gson();
User usr = g.fromJson( parseToGson(user), User.class);
assertEquals(null, usr.name);
assertEquals(123, usr.id);
assertEquals(null, usr.address.street);
assertEquals("New York", usr.address.city);
assertEquals("US", usr.address.country);
}
Update #2
Since the previous version is not a generic one, I would like to update the answer.
For generic, I created MyCustomSerializer as following
public class MyCustomSerializer<T> implements JsonSerializer<T> {
private final Class<T> type;
public MyCustomSerializer(Class<T> type) {
this.type = type;
}
public Class<T> getMyType() {
return this.type;
}
#Override
public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject obj = new JsonObject();
try {
Field[] declaredFields = this.type.getDeclaredFields();
for (Field field : declaredFields) {
Object object = field.get(src);
if (object != null) {
// Here, we check for 4 types of JsonObject.addProperty
if (object instanceof String) {
obj.addProperty(field.getName(), (String) object);
continue;
}
if (object instanceof Number) {
obj.addProperty(field.getName(), (Number) object);
continue;
}
if (object instanceof Boolean) {
obj.addProperty(field.getName(), (Boolean) object);
continue;
}
if (object instanceof Character) {
obj.addProperty(field.getName(), (Character) object);
continue;
}
// This is where we check for other types
// The idea is if it is an object, we need to care its child object as well, so parse it into json string and replace the null value.
Gson gson = new GsonBuilder().serializeNulls().create();
String json = gson.toJson(object);
json = json.replace("null", "\"null\""); // We have to build the string first, then replace it with our special keys. In this case, I use the string "null"
JsonObject convertedObject = new Gson().fromJson(json, JsonObject.class); // Then convert it back to json object
obj.add(field.getName(), convertedObject);
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return obj;
}
}
The main idea is still the same as previous version but I made it to a generic one.
I also added some additional properties to test for the string this code builds with the results
{
"id":123,
"address":{
"street":null,
"city":"New York",
"country":"US",
"info":{
"zipcode":null,
"address2":"stackoverflow",
"workPlaceAddress":{
"street":null,
"companyName":"google"
}
}
}
}
To call this, we need to do
private String parseToGson(User user) {
Gson gson = new GsonBuilder().registerTypeAdapter(User.class, new MyCustomSerializer<>(User.class)).create();
return gson.toJson(user).replace("\"null\"", "null");
}
Update #3
Since you still concern about your solution, I tried to adapt it as well
public class YourSerializer <T> implements JsonSerializer<T>{
private final Class<T> type;
public YourSerializer(Class<T> type) {
this.type = type;
}
public Class<T> getMyType() {
return this.type;
}
#Override
public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) {
Gson gson = new GsonBuilder().serializeNulls().create();
JsonObject jsonObject = gson.toJsonTree(src).getAsJsonObject();
Field[] declaredFields = this.type.getDeclaredFields();
for (Field field : declaredFields) {
try {
if(field.get(src) == null) {
jsonObject.remove(field.getName());
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return jsonObject;
}
}
The reason is you used serializeNulls() incorrectly which makes your output is incorrect. To correct it, you should registerTypeAdapter first to create your custom json, then you call serializeNulls
private String parseToGson(User user) {
Gson gson = new GsonBuilder().registerTypeAdapter(User.class, new YourSerializer<>(User.class)).serializeNulls().create();
return gson.toJson(user);
}
I tested and got the same result with update#2
{
"id":123,
"address":{
"street":null,
"city":"New York",
"country":"US",
"info":{
"zipcode":null,
"address2":"aaa",
"workPlaceAddress":{
"street":null,
"companyName":"google"
}
}
}
}
I have a class A as
Class A{
private static final String ANON_DIR = "/webapps/worldlingo/data/anonymizer/";
private static final String NO_ANON = "noanonymize";
public String first(String text, String srclang, Map dictTokens) {
Set<String> noAnonymize = new HashSet<String>();
second(noAnonymize,ANON_DIR + NO_ANON, "tmpLang","name");
String value;
if(noAnonymize.contains("test")){
value = "test1";
}
else {
value = "test";
}
return value;
}
where ANON_DIR and NO_ANON is static final value. This class has function first and function second .The first function has a calling method in it which calls second function. The second function is void function which takes static fields as parameter.
Second function is just the file read function with the path provided as
public void second (Set<String> hashSet, String path, String lang , String type) {
FileReader fr = null;
BufferedReader br = null;
try {
fr = new FileReader(path);
br = new BufferedReader(fr);
String Line;
while ((Line = br.readLine()) != null) {
hashSet.add(Line);
}
} catch (IOException e) {
log.error("Anonymizer: Unable to load file.", e);
} finally {
try {
if (fr != null) {
fr.close();
}
if (br != null) {
br.close();
}
} catch (IOException e) {
log.error("Anonymizer : An error occured while closing a resource.", e);
}
}
}
}
Now I am trying to test the function first using mockito. I am trying update the passed first argument (list parameter) i.e noAnonymize in second(noAnonymize,ANON_DIR + NO_ANON, "tmpLang","name");
public void testfirst() throws Exception {
Anonymizer mock = PowerMockito.mock(Anonymizer.class);
doAnswer(new Answer() {
public Object answer(InvocationOnMock invocation) {
List<String> args = invocation.getArgumentAt(0,List.class);
args.add("a");
args.add("b");
return null; // void method, so return null
}
}).when(mock).readNoAnonymizeFile(Mockito.anySet(),Mockito.anyString(),Mockito.anyString(),Mockito.anyString());
Method anonymizeNames = anon.getClass().getDeclaredMethod("anonymizeNames_test", String.class, String.class, Map.class);
String srcLang = "MSFT_EN";
Map mapTokens = new HashMap();
String result = (String) anonymizeNames.invoke(anon,"I am David",srcLang,mapTokens);
}
PROBLEM:
I am not able to mock the void second method to update list with value a and b. How can I have the mockto test case to update parameter in void method.
When unit testing a class, you test it through its public methods. If you can't test the class sufficiently through its public methods, it needs re-factored.
In this case, you're trying to unit test a private method for an edge case that doesn't exist. Why even provide the constant as a parameter? Why not reference it directly in the private method and save passing an argument? Instead, you could write:
fr = new FileReader(ANON_DIR + NO_ANON);
EDIT
After Laxmi and I had a discussion we came up with a solution using constructor based dependency injection and changing the void method to return Set<String>. This let us test in isolation and mock easily.
I have variable of type java.util.Properties. I am trying to write it to a JSON file, and as well as read from that file.
The Properties variable looks something like below:
Properties inner3 = new Properties();
inner3.put("i1", 1);
inner3.put("i2", 100);
Properties inner2 = new Properties();
inner2.put("aStringProp", "aStringValue");
inner2.put("inner3", inner3);
Properties inner1 = new Properties();
inner1.put("aBoolProp", true);
inner1.put("inner2", inner2);
Properties topLevelProp = new Properties();
topLevelProp.put("count", 1000000);
topLevelProp.put("size", 1);
topLevelProp.put("inner1", inner1);
Naturally, when I serialize the topLevelProp to JSON I expect the result to be as below.
{
"inner1": {
"inner2": {
"aStringProp": "aStringValue",
"inner3": {
"i2": 100,
"i1": 1
}
},
"aBoolProp": true
},
"size": 1,
"count": 1000000
}
The above JSON result can be produced by using Gson in a pretty straight forward way, but when it is fed the same JSON string to desrialize, it fails.
Gson gson = new GsonBuilder().create();
String json = gson.toJson(topLevelProp); //{"inner1":{"inner2":{"aStringProp":"aStringValue","inner3":{"i2":100,"i1":1}},"aBoolProp":true},"size":1,"count":1000000}
//following line throws error: Expected a string but was BEGIN_OBJECT at line 1 column 12 path $.
Properties propObj = gson.fromJson(json, Properties.class);
Tried with Jackson as well:
ObjectMapper mapper = new ObjectMapper();
mapper.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true);
mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
File file = new File("configs/config1.json");
mapper.writeValue(file, topLevelProp);
The last line throws error:
com.fasterxml.jackson.databind.JsonMappingException: java.util.Properties cannot be cast to java.lang.String (through reference chain: java.util.Properties["inner1"])
Tried to desrialize from the string as follows and it failed with the following error:
Properties jckProp = JsonSerializer.mapper.readValue(json, Properties.class);
Can not deserialize instance of java.lang.String out of START_OBJECT token
at [Source: {"inner1":{"inner2":{"aStringProp":"aStringValue","inner3":{"i2":100,"i1":1}},"aBoolProp":true},"size":1,"count":1000000}; line: 1, column: 11] (through reference chain: java.util.Properties["inner1"])
How this can be handled?
Update: Following the idea of cricket_007, found com.fasterxml.jackson.databind.node.ObjectNode, can be used as follows:
ObjectNode jckProp = JsonSerializer.mapper.readValue(json, ObjectNode.class);
System.out.println(jckProp.get("size").asInt());
System.out.println("jckProp: " + jckProp);
System.out.println("jckProp.inner: " + jckProp.get("inner1"));
I think this can be the way forward for me, as I mostly have to read from JSON file.
The problem you have is that you are misusing java.util.Properties: it is NOT a multi-level tree structure, but a simple String-to-String map.
So while it is technically possibly to add non-String property values (partly since this class was added before Java generics, which made allowed better type safety), this should not be done. For nested structured, use java.util.Map or specific tree data structures.
As to Properties, javadocs say for example:
The Properties class represents a persistent set of properties.
The Properties can be saved to a stream or loaded from a stream.
Each key and its corresponding value in the property list is a string.
...
If the store or save method is called on a "compromised" Properties
object that contains a non-String key or value, the call will fail.
Now: if and when you have such "compromised" Properties instance, your best bet with Jackson or Gson is to construct a java.util.Map (or perhaps older Hashtable), and serialize it. That should work without issues.
As it was said above by StaxMan, you're misusing the Properties class and you're close about having heavy issues for using it like that due to lack of type information. However, you might also face the same case for weakly-typed maps. If it's a must for you, then you can use your custom Gson JsonDeserializer (note the JSON arrays issue):
final class PropertiesJsonDeserializer
implements JsonDeserializer<Properties> {
private static final JsonDeserializer<Properties> propertiesJsonDeserializer = new PropertiesJsonDeserializer();
private PropertiesJsonDeserializer() {
}
static JsonDeserializer<Properties> getPropertiesJsonDeserializer() {
return propertiesJsonDeserializer;
}
#Override
public Properties deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
throws JsonParseException {
final Properties properties = new Properties();
final JsonObject jsonObject = jsonElement.getAsJsonObject();
for ( final Entry<String, JsonElement> e : jsonObject.entrySet() ) {
properties.put(e.getKey(), parseValue(context, e.getValue()));
}
return properties;
}
private static Object parseValue(final JsonDeserializationContext context, final JsonElement valueElement) {
if ( valueElement instanceof JsonObject ) {
return context.deserialize(valueElement, Properties.class);
}
if ( valueElement instanceof JsonPrimitive ) {
final JsonPrimitive valuePrimitive = valueElement.getAsJsonPrimitive();
if ( valuePrimitive.isBoolean() ) {
return context.deserialize(valueElement, Boolean.class);
}
if ( valuePrimitive.isNumber() ) {
return context.deserialize(valueElement, Number.class); // depends on the JSON literal due to the lack of real number type info
}
if ( valuePrimitive.isString() ) {
return context.deserialize(valueElement, String.class);
}
throw new AssertionError();
}
if ( valueElement instanceof JsonArray ) {
throw new UnsupportedOperationException("Arrays are unsupported due to lack of type information (a generic list or a concrete type array?)");
}
if ( valueElement instanceof JsonNull ) {
throw new UnsupportedOperationException("Nulls cannot be deserialized");
}
throw new AssertionError("Must never happen");
}
}
Hence, it might be used like this:
private static final Gson gson = new GsonBuilder()
.registerTypeAdapter(Properties.class, getPropertiesJsonDeserializer())
.create();
public static void main(final String... args) {
final Properties outgoingProperties = createProperties();
out.println(outgoingProperties);
final String json = gson.toJson(outgoingProperties);
out.println(json);
final Properties incomingProperties = gson.fromJson(json, Properties.class);
out.println(incomingProperties);
}
private static Properties createProperties() {
final Properties inner3 = new Properties();
inner3.put("i1", 1);
inner3.put("i2", 100);
final Properties inner2 = new Properties();
inner2.put("aStringProp", "aStringValue");
inner2.put("inner3", inner3);
final Properties inner1 = new Properties();
inner1.put("aBoolProp", true);
inner1.put("inner2", inner2);
final Properties topLevelProp = new Properties();
topLevelProp.put("count", 1000000);
topLevelProp.put("size", 1);
topLevelProp.put("inner1", inner1);
return topLevelProp;
}
with the following output:
{inner1={inner2={aStringProp=aStringValue, inner3={i2=100, i1=1}}, aBoolProp=true}, size=1, count=1000000}
{"inner1":{"inner2":{"aStringProp":"aStringValue","inner3": {"i2":100,"i1":1}},"aBoolProp":true},"size":1,"count":1000000}
{inner1={inner2={aStringProp=aStringValue, inner3={i2=100, i1=1}}, aBoolProp=true}, size=1, count=1000000}
Type info injection
You could save some type information though, if you inject the type information in the result JSON. Let's assume you are fine with storing numeric values as not primitives, but JSON objects having two keys like _$T and _$V to hold the actual type (a class indeed, not any java.reflect.Type, unfortunately) and the associated value respectively in order to restore the real type of the property. This can be applied to arrays either, but it's still not possible to hold a parameterized type due to the lack of type paremerization for instances that are parameterized somehow (unless you can reach it via a Class instance):
final class PropertiesJsonDeserializer
implements JsonDeserializer<Properties> {
private static final JsonDeserializer<Properties> propertiesJsonDeserializer = new PropertiesJsonDeserializer();
private PropertiesJsonDeserializer() {
}
static JsonDeserializer<Properties> getPropertiesJsonDeserializer() {
return propertiesJsonDeserializer;
}
#Override
public Properties deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
throws JsonParseException {
final Properties properties = new Properties();
final JsonObject jsonObject = jsonElement.getAsJsonObject();
for ( final Entry<String, JsonElement> e : jsonObject.entrySet() ) {
properties.put(e.getKey(), parseValue(context, e.getValue()));
}
return properties;
}
private static Object parseValue(final JsonDeserializationContext context, final JsonElement valueElement) {
if ( valueElement instanceof JsonObject ) {
return context.deserialize(valueElement, Properties.class);
}
if ( valueElement instanceof JsonPrimitive ) {
final JsonPrimitive valuePrimitive = valueElement.getAsJsonPrimitive();
if ( valuePrimitive.isBoolean() ) {
return context.deserialize(valueElement, Boolean.class);
}
if ( valuePrimitive.isNumber() ) {
return context.deserialize(valueElement, Number.class); // depends on the JSON literal due to the lack of real number type info
}
if ( valuePrimitive.isString() ) {
return context.deserialize(valueElement, String.class);
}
throw new AssertionError();
}
if ( valueElement instanceof JsonArray ) {
throw new UnsupportedOperationException("Arrays are unsupported due to lack of type information (a generic list or a concrete type array?)");
}
if ( valueElement instanceof JsonNull ) {
throw new UnsupportedOperationException("Nulls cannot be deserialized");
}
throw new AssertionError("Must never happen");
}
}
final class TypeAwarePropertiesSerializer
implements JsonSerializer<Properties> {
private static final JsonSerializer<Properties> typeAwarePropertiesSerializer = new TypeAwarePropertiesSerializer();
private TypeAwarePropertiesSerializer() {
}
static JsonSerializer<Properties> getTypeAwarePropertiesSerializer() {
return typeAwarePropertiesSerializer;
}
#Override
public JsonElement serialize(final Properties properties, final Type type, final JsonSerializationContext context) {
final JsonObject propertiesJson = new JsonObject();
for ( final Entry<Object, Object> entry : properties.entrySet() ) {
final String property = (String) entry.getKey();
final Object value = entry.getValue();
if ( value instanceof Boolean ) {
propertiesJson.addProperty(property, (Boolean) value);
} else if ( value instanceof Character ) {
propertiesJson.addProperty(property, (Character) value);
} else if ( value instanceof Number ) {
final JsonObject wrapperJson = newWrapperJson(value);
wrapperJson.addProperty("_$V", (Number) value);
propertiesJson.add(property, wrapperJson);
} else if ( value instanceof String ) {
propertiesJson.addProperty(property, (String) value);
} else if ( value instanceof Properties || value instanceof Collection || value instanceof Map ) {
propertiesJson.add(property, context.serialize(value));
} else if ( value != null ) {
final Class<?> aClass = value.getClass();
if ( aClass.isArray() ) {
final JsonObject wrapperJson = newWrapperJson(value);
wrapperJson.add("_$V", context.serialize(value));
propertiesJson.add(property, wrapperJson);
} else {
throw new UnsupportedOperationException("Cannot process: " + value);
}
} else /* now the value is always null, Properties cannot hold nulls */ {
throw new AssertionError("Must never happen");
}
}
return propertiesJson;
}
private static JsonObject newWrapperJson(final Object value) {
final JsonObject wrapperJson = new JsonObject();
wrapperJson.addProperty("_$T", value.getClass().getName());
return wrapperJson;
}
}
final class TypeAwarePropertiesDeserializer
implements JsonDeserializer<Properties> {
private static final JsonDeserializer<Properties> typeAwarePropertiesDeserializer = new TypeAwarePropertiesDeserializer();
private TypeAwarePropertiesDeserializer() {
}
static JsonDeserializer<Properties> getTypeAwarePropertiesDeserializer() {
return typeAwarePropertiesDeserializer;
}
#Override
public Properties deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
throws JsonParseException {
try {
final Properties properties = new Properties();
final JsonObject jsonObject = jsonElement.getAsJsonObject();
for ( final Entry<String, JsonElement> e : jsonObject.entrySet() ) {
properties.put(e.getKey(), parseValue(context, e.getValue()));
}
return properties;
} catch ( final ClassNotFoundException ex ) {
throw new JsonParseException(ex);
}
}
private static Object parseValue(final JsonDeserializationContext context, final JsonElement valueElement)
throws ClassNotFoundException {
if ( valueElement instanceof JsonObject ) {
final JsonObject valueObject = valueElement.getAsJsonObject();
if ( isWrapperJson(valueObject) ) {
return context.deserialize(getWrapperValueObject(valueObject), getWrapperClass(valueObject));
}
return context.deserialize(valueElement, Properties.class);
}
if ( valueElement instanceof JsonPrimitive ) {
final JsonPrimitive valuePrimitive = valueElement.getAsJsonPrimitive();
if ( valuePrimitive.isBoolean() ) {
return context.deserialize(valueElement, Boolean.class);
}
if ( valuePrimitive.isNumber() ) {
throw new AssertionError("Must never happen because of 'unboxing' above");
}
if ( valuePrimitive.isString() ) {
return context.deserialize(valueElement, String.class);
}
throw new AssertionError("Must never happen");
}
if ( valueElement instanceof JsonArray ) {
return context.deserialize(valueElement, Collection.class);
}
if ( valueElement instanceof JsonNull ) {
throw new UnsupportedOperationException("Nulls cannot be deserialized");
}
throw new AssertionError("Must never happen");
}
private static boolean isWrapperJson(final JsonObject valueObject) {
return valueObject.has("_$T") && valueObject.has("_$V");
}
private static Class<?> getWrapperClass(final JsonObject valueObject)
throws ClassNotFoundException {
return Class.forName(valueObject.get("_$T").getAsJsonPrimitive().getAsString());
}
private static JsonElement getWrapperValueObject(final JsonObject valueObject) {
return valueObject.get("_$V");
}
}
Now the topLevelProp can be filled also with:
topLevelProp.put("ARRAY", new String[]{ "foo", "bar" });
topLevelProp.put("RAW_LIST", asList("foo", "bar"));
if you have these special JSON deserializers applied:
private static final Gson typeAwareGson = new GsonBuilder()
.registerTypeAdapter(Properties.class, getTypeAwarePropertiesSerializer())
.registerTypeAdapter(Properties.class, getTypeAwarePropertiesDeserializer())
.create();
A sample output:
{RAW_LIST=[foo, bar], inner1={inner2={aStringProp=aStringValue, inner3={i2=100, i1=1}}, aBoolProp=true}, size=1, count=1000000, ARRAY=[Ljava.lang.String;#b81eda8}
{"RAW_LIST":["foo","bar"],"inner1":{"inner2":{"aStringProp":"aStringValue","inner3":{"i2":{"_$T":"java.lang.Integer","_$V":100},"i1":{"_$T":"java.lang.Integer","_$V":1}}},"aBoolProp":true},"size":{"_$T":"java.lang.Integer","_$V":1},"count":{"_$T":"java.lang.Integer","_$V":1000000},"ARRAY":{"_$T":"[Ljava.lang.String;","_$V":["foo","bar"]}}
{RAW_LIST=[foo, bar], inner1={inner2={aStringProp=aStringValue, inner3={i2=100, i1=1}}, aBoolProp=true}, size=1, count=1000000, ARRAY=[Ljava.lang.String;#e2144e4}
Summarizing up two approaches, you might want to eliminate the need of weak-typing and introduce explicit POJO mappings if possible.
Since I only needed a deserialization feature, i.e. generate Java properties for an incoming Json (in my case a REST endpoint), I quickly hacked this solution:
public class Configuration extends Properties {
public void load(JsonElement json) {
addJson("", json);
return;
}
public void addJson(String root, JsonElement json) {
// recursion for objects
if (json instanceof JsonObject) {
if (!root.equals("")) root += ".";
final JsonObject jsonObject = json.getAsJsonObject();
for ( final Entry<String, JsonElement> e : jsonObject.entrySet() ) {
addJson(root + e.getKey(), e.getValue());
}
return;
}
// recursion for arrays
if (json instanceof JsonArray) {
final JsonArray jsonArray = json.getAsJsonArray();
if (!root.equals("")) root += ".";
int count = 0;
for(final JsonElement e : jsonArray) {
addJson(root+count, e);
count++;
}
return;
}
// leaves: add property
this.setProperty(root, json.getAsString());
}
}
As you can see, this is extending the Properties class. Another option would of course be to initialize a Properties object beforehand and pass it into the recursion.
I hope this is useful to someone :-)
I have int, float, boolean and string from Properties file. Everything has loaded in Properties. Currently, I am parsing values as I know expected value for particular key.
Boolean.parseBoolean("false");
Integer.parseInt("3")
What is better way of setting these constants values, If I don't know what could be primitive value datatype for a key.
public class Messages {
Properties appProperties = null;
FileInputStream file = null;
public void initialization() throws Exception {
appProperties = new Properties();
try {
loadPropertiesFile();
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
}
}
public void loadPropertiesFile() throws IOException {
String path = "./cfg/message.properties";
file = new FileInputStream(path);
appProperties.load(file);
file.close();
}
}
Properties File.
messassge.properties
SSO_URL = https://example.com/connect/token
SSO_API_USERNAME = test
SSO_API_PASSWORD = Uo88YmMpKUp
SSO_API_SCOPE = intraday_api
SSO_IS_PROXY_ENABLED = false
SSO_MAX_RETRY_COUNT = 3
SSO_FLOAT_VALUE = 3.0
Constant.java
public class Constants {
public static String SSO_URL = null;
public static String SSO_API_USERNAME = null;
public static String SSO_API_PASSWORD = null;
public static String SSO_API_SCOPE = null;
public static boolean SSO_IS_PROXY_ENABLED = false;
public static int SSO_MAX_RETRY_COUNT = 0;
public static float SSO_FLOAT_VALUE = 0;
}
If you have a class of configuration values, like your Constants class, and you want to load all values from a configuration (properties) file, you can create a little helper class and use reflection:
public class ConfigLoader {
public static void load(Class<?> configClass, String file) {
try {
Properties props = new Properties();
try (FileInputStream propStream = new FileInputStream(file)) {
props.load(propStream);
}
for (Field field : configClass.getDeclaredFields())
if (Modifier.isStatic(field.getModifiers()))
field.set(null, getValue(props, field.getName(), field.getType()));
} catch (Exception e) {
throw new RuntimeException("Error loading configuration: " + e, e);
}
}
private static Object getValue(Properties props, String name, Class<?> type) {
String value = props.getProperty(name);
if (value == null)
throw new IllegalArgumentException("Missing configuration value: " + name);
if (type == String.class)
return value;
if (type == boolean.class)
return Boolean.parseBoolean(value);
if (type == int.class)
return Integer.parseInt(value);
if (type == float.class)
return Float.parseFloat(value);
throw new IllegalArgumentException("Unknown configuration value type: " + type.getName());
}
}
Then you call it like this:
ConfigLoader.load(Constants.class, "/path/to/constants.properties");
You can extend the code to handle more types. You can also change it to ignore missing properties, instead of failing like it does now, such that assignments in the field declaration will remain unchanged, i.e. be the default.
If you know the type of constant, you can use Apache Commons Collections.
For example, you can use some utilities method based on type of your constant.
booelan SSO_IS_PROXY_ENABLED = MapUtils.getBooleanValue(appProperties, "SSO_IS_PROXY_ENABLED", false);
String SSO_URL = MapUtils.getString(appProperties, "SSO_URL", "https://example.com/connect/token");
You can even use default values to avoid errors.
Dambros is right, every thing you store inside a Properties file is as a String value.
You can track your different primitive data types after retrieving properties value as below like ref. -
Java Properties File: How to Read config.properties Values in Java?
package crunchify.com.tutorial;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.Properties;
/**
* #author Crunchify.com
*
*/
public class CrunchifyGetPropertyValues {
String result = "";
InputStream inputStream;
public String getPropValues() throws IOException {
try {
Properties prop = new Properties();
String propFileName = "config.properties";
inputStream = getClass().getClassLoader().getResourceAsStream(propFileName);
if (inputStream != null) {
prop.load(inputStream);
} else {
throw new FileNotFoundException("property file '" + propFileName + "' not found in the classpath");
}
Date time = new Date(System.currentTimeMillis());
// get the property value and print it out
String user = prop.getProperty("user");
String company1 = prop.getProperty("company1");
String company2 = prop.getProperty("company2");
String company3 = prop.getProperty("company3");
result = "Company List = " + company1 + ", " + company2 + ", " + company3;
System.out.println(result + "\nProgram Ran on " + time + " by user=" + user);
} catch (Exception e) {
System.out.println("Exception: " + e);
} finally {
inputStream.close();
}
return result;
}
}
and later convert to primitive -
How to convert String to primitive type value?
I suggest you to track your data types value by putting the key values inside String type switch statement and later retrieve the related data type value by using key name cases.
String type switch case is possible after Java 7.
Not entirely sure whether I exactly understand the problem but a possibility could be to include the type of the property value in the (String) value. So for example the properties you showed would become something like:
SSO_URL = URL:https://example.com/connect/token
SSO_API_USERNAME = STRING:test
SSO_API_PASSWORD = STRING:Uo88YmMpKUp
SSO_API_SCOPE = STRING:intraday_api
SSO_IS_PROXY_ENABLED = BOOLEAN:false
SSO_MAX_RETRY_COUNT = INTEGER:3
SSO_FLOAT_VALUE = FLOAT:3.0
During the parsing of the property values you first determine the type of the property by looking at the part before : and use the part after for the actual parsing.
private static Object getValue(Properties props, String name) {
String propertyValue = props.getProperty(name);
if (propertyValue == null) {
throw new IllegalArgumentException("Missing configuration value: " + name);
} else {
String[] parts = string.split(":");
switch(parts[0]) {
case "STRING":
return parts[1];
case "BOOLEAN":
return Boolean.parseBoolean(parts[1]);
....
default:
throw new IllegalArgumentException("Unknown configuration value type: " + parts[0]);
}
}
}
Follow the dropwizard configuration pattern where you define your constants using YAML instead of Properties and use Jackson to deserialize it into your Class. Other than type safety, dropwizard's configuration pattern goes one step further by allowing Hibernate Validator annotations to validate that the values fall into your expected ranges.
For dropwizard's example...
http://www.dropwizard.io/0.9.2/docs/getting-started.html#creating-a-configuration-class
For more information about the technology involved...
github.com/FasterXML/jackson-dataformat-yaml
hibernate.org/validator/
Spring Boot has ready to use and feature reach solution for type-safe configuration properties.
Definitely, use of the Spring just for this task is overkill but Spring has a lot of cool features and this one can attract you to right side ;)
You can define your configurable parameters as 'static' in your class of choice, and from a static init call a method that loads the parameter values from a properties file.
For example:
public class MyAppConfig {
final static String propertiesPath="/home/workspace/MyApp/src/config.properties";
static String strParam;
static boolean boolParam;
static int intParam;
static double dblParam;
static {
// Other static initialization tasks...
loadParams();
}
private static void loadParams(){
Properties prop = new Properties();
try (InputStream propStream=new FileInputStream(propertiesPath)){
// Load parameters from config file
prop.load(propStream);
// Second param is default value in case key-pair is missing
strParam=prop.getProperty("StrParam", "foo");
boolParam=Boolean.parseBoolean(prop.getProperty("boolParam", "false"));
intParam= Integer.parseInt(prop.getProperty("intParam", "1"));
dblParam=Double.parseDouble(prop.getProperty("dblParam", "0.05"));
} catch (IOException e) {
logger.severe(e.getMessage());
e.printStackTrace();
}
}
}
This might help:
props.getProperty("name", Integer.class);
My plan is to make a JSON results by my database. What I mean is that I have made a Class where I connect to a API (etc. TMDB) and take the information I need.
public class MovieDataBase {
Attributes att = new Attributes(); //A klass where I have setter-getter
//methods for titel, overview, vote, release.
public String searchMovie(String filmtitel) {
HttpResponse<JsonNode> response;
try {
response = Unirest.get("http://api.themoviedb.org/3/search/movie")
.queryString("api_key", "123")
.queryString("query", filmtitel)
.asJson();
JsonNode json = response.getBody();
JSONObject envelope = json.getObject();
JSONArray results = envelope.getJSONArray("results");
filmtitel += att.title = results.getJSONObject(0).getString("title");
filmtitel += att.release = results.getJSONObject(0).getString("release_date");
filmtitel += att.vote = results.getJSONObject(0).getInt("vote_average");
filmtitel += att.overview = results.getJSONObject(0).getString("overview");
return filmtitel
}
catch (JSONException e) {
and I also have a get method where I want to create a JSON construction of the program to easier use it in the future (for html)
public static void main(String[] args) {
Gson gson = new Gson();
setPort(8080);
Youtube yt = new Youtube();
MovieDataBase mdb = new MovieDataBase();
get("/search/:movie", (req, res) -> {
String movie = req.queryParams(":movie");
String movies = mdb.searchMovie(movie);
String json = att.title + att.release + att.vote + att.vote + att.overview
json = gson.toJson(json);
return json
});
This did not work. I'm getting a null when I'm running it, not a error but only a null. So I have to combine String movies = mdb.searchMovie(movie); with the String Json and then make it look like a json, but I don't know if that's right.