I am creating java app which will allow storing objects in database. What I want to do is generic implementation so it could load json and create java class from it. This is what a code should look like:
SomeClass someObject= data.getValue(SomeClass.class);
Lets say that data would be a json object. How should I implement getValue() method so it will allow me to create class from it. I don't want SomeClass to extend anything other then Object. I think that this should be done using generic classes but so far I have not worked with generic classes like this. Can you please point to a best way on how to acomplish this? Example code would be best.
Many thanks
You can consult the source code of Jackson library and look inside (or debug) the method BeanDeserializer#vanillaDeserialize(), there you'll find the loop which traverse through all json tokens, finds the corresponding fields and sets their values.
As a proof of concept, I've extracted part of the logic from Jacskson and wrapped it inside a naive (and fragile) object mapper and a naive (and fragile) json parser:
public static class NaiveObjectMapper {
private Map<String, Object> fieldsAndMethods;
private NaiveJsonParser parser;
public <T> T readValue(String content, Class<T> valueType) {
parser = new NaiveJsonParser(content);
try {
// aggregate all value type fields and methods inside a map
fieldsAndMethods = new HashMap<>();
for (Field field : valueType.getDeclaredFields()) {
fieldsAndMethods.put(field.getName(), field);
}
for (Method method : valueType.getMethods()) {
fieldsAndMethods.put(method.getName(), method);
}
// create an instance of value type by calling its default constructor
Constructor<T> constructor = valueType.getConstructor();
Object bean = constructor.newInstance(new Object[0]);
// loop through all json nodes
String propName;
while ((propName = parser.nextFieldName()) != null) {
// find the corresponding field
Field prop = (Field) fieldsAndMethods.get(propName);
// get and set field value
deserializeAndSet(prop, bean);
}
return (T) bean;
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
private void deserializeAndSet(Field prop, Object bean) {
Class<?> propType = prop.getType();
Method setter = (Method) fieldsAndMethods.get(getFieldSetterName(prop));
try {
if (propType.isPrimitive()) {
if (propType.getName().equals("int")) {
setter.invoke(bean, parser.getIntValue());
}
} else if (propType == String.class) {
setter.invoke(bean, parser.getTextValue());
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
private String getFieldSetterName(Field prop) {
String propName = prop.getName();
return "set" + propName.substring(0, 1).toUpperCase() + propName.substring(1);
}
}
class NaiveJsonParser {
String[] nodes;
int currentNodeIdx = -1;
String currentProperty;
String currentValueStr;
public NaiveJsonParser(String content) {
// split the content into 'property:value' nodes
nodes = content.replaceAll("[{}]", "").split(",");
}
public String nextFieldName() {
if ((++currentNodeIdx) >= nodes.length) {
return null;
}
String[] propertyAndValue = nodes[currentNodeIdx].split(":");
currentProperty = propertyAndValue[0].replace("\"", "").trim();
currentValueStr = propertyAndValue[1].replace("\"", "").trim();
return currentProperty;
}
public String getTextValue() {
return String.valueOf(currentValueStr);
}
public int getIntValue() {
return Integer.valueOf(currentValueStr).intValue();
}
}
public static class User {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#Override
public String toString() {
return "id = " + id + ", name = \"" + name + "\"";
}
}
To see the deserialization in action run:
String json = "{\"id\":1, \"name\":\"jsmith\"}";
NaiveObjectMapper objectMapper = new NaiveObjectMapper();
User user = objectMapper.readValue(json, User.class);
System.out.println(user);
Or try online.
However I recommend not to reinvent the wheel and use Jackson and in case you need some custom actions you can use custom deserialization, see here and here.
Related
I want to make a Map (String ,Object) like this
{AssessmentId=0, Physical_name='ram', Physical_height=20, Physical_weight=60}
from my Pojo Class - InitialAssessment
public class InitialAssessment {
private long AssessmentId;
private String physical_name;
private String physical_gender;
private int physical_height;
private float physical_weight;
// all getter And setter is Created here
}
without using any external Library like Gson etc.
You can use this approach:
public Map getMapFromPojo(InitialAssessment assessment) throws Exception {
Map<String, Object> map = new HashMap<>();
if (assessment != null) {
Method[] methods = assessment.getClass().getMethods();
for (Method method : methods) {
String name = method.getName();
if (name.startsWith("get") && !name.equalsIgnoreCase("getClass")) {
Object value = "";
try {
value = method.invoke(assessment);
map.put(name.substring(name.indexOf("get") + 3), value);
} catch (Exception e) {
e.printStackTrace();
}
}
}
return map;
}
return null;
}
It will give you map for pojo class like this:
Output:
{AssessmentId=0, Physical_name='ram', Physical_gender='Male' , Physical_height=20, Physical_weight=60}
I have a simple entity User.
public class User {
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
And his corresponding DTO
public class UsuarioDTO {
String name;
String getName(){
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
I want to achieve something like I show below to avoid multiple classes of transformers.
#Dto(entity = "Usuario")
public class UsuarioDTO {
#BasicElement(name = "name")
String name;
String getName(){
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface BasicElement {
String name();
}
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
public #interface Dto {
String entity() default "";
}
With this example classes I can do:
public class Transformer {
public static void main(String[] args) {
UserDTO usuarioDTO = new UserDTO("Gabriel");
Class<UserDTO> obj = UserDTO.class;
if (obj.isAnnotationPresent(Dto.class)) {
Dto annotation = obj.getAnnotation(Dto.class);
Class<?> clazz;
try {
clazz = Class.forName(annotation.entity());
Constructor<?> constructor = clazz.getConstructor();
Object instance = constructor.newInstance();
for (Field originField : UserDTO.class.getDeclaredFields()) {
originField.setAccessible(true);
if (originField.isAnnotationPresent(BasicElement.class)) {
BasicElement basicElement = originField.getAnnotation(BasicElement.class);
Field destinationField = instance.getClass().getDeclaredField(basicElement.name());
destinationField.setAccessible(true);
destinationField.set(instance, originField.get(usuarioDTO));
}
}
System.out.println(((User) instance).getName());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
But this would be to expensive because consumes the annotations in each transformation.
It's possible with Byte-buddy to read the annotations and create a class transformer whose decompiled code look like this:
public class TransformerImpl implements ITransformer{
public Object toEntity(Object dto){
User user = new User();
user.setName(dto.getName());
}
}
UPDATE:
#Rafael Winterhalter, something like this?
public class Transformer<D,E> {
List<Field> dtoFields = new ArrayList<Field>();
Constructor<D> dtoConstructor;
List<Field> entityFields = new ArrayList<Field>();
Constructor<E> entityConstructor;
public Transformer(Class<D> dtoClass){
try {
Dto annotation = dtoClass.getAnnotation(Dto.class);
Class<E> entityClass = (Class<E>) annotation.entity();
//entityConstructor = entityClass.getConstructor();
entityConstructor = entityClass.getDeclaredConstructor();
entityConstructor.setAccessible(true);
dtoConstructor = dtoClass.getConstructor();
dtoConstructor.setAccessible(true);
lookupFields(entityClass, dtoClass);
} catch (Exception e) {
e.printStackTrace();
}
}
private void lookupFields(Class<E> entityClass, Class<D> dtoClass) throws NoSuchFieldException {
for (Field dtoField : dtoClass.getDeclaredFields()) {
if (dtoField.isAnnotationPresent(BasicElement.class)) {
BasicElement basicElement = dtoField.getAnnotation(BasicElement.class);
String entityFieldName = (basicElement.name().equals("")) ? dtoField.getName() : basicElement.name();
Field entityField = entityClass.getDeclaredField(entityFieldName);
dtoField.setAccessible(true);
entityField.setAccessible(true);
dtoFields.add(dtoField);
entityFields.add(entityField);
}
}
}
public E toEntity(D dto) throws ReflectiveOperationException {
E entity = entityConstructor.newInstance();
for (int i = 0; i < entityFields.size(); i++){
Field destination = entityFields.get(i);
Field origin = dtoFields.get(i);
destination.set(entity, origin.get(dto));
}
return entity;
}
public D toDto(E entity) throws ReflectiveOperationException {
D dto = dtoConstructor.newInstance();
for (int i = 0; i < entityFields.size(); i++){
Field origin = entityFields.get(i);
Field destination = dtoFields.get(i);
destination.set(dto, origin.get(entity));
}
return dto;
}
}
To answer your question: Yes, it is possible. You can ask Byte Buddy to create instances of ITransformer for you where you implement the only method to do what you want. You would however need to implement your own Implementation instance for doing so.
However, I would not recommend you to do so. I usually tell users that Byte Buddy should not be used for performance work and for a majority of use-cases, this is true. Your use case is one of them.
If you implemented classes, you would have to cache these classes for any mapping. Otherwise, the class generation-costs would be the significant share. Instead, you rather want to maintain a transformer that caches the objects of the reflection API (reflective lookups are the expensive part of your operation, reflective invocation is not so problematic) and reuses previously looked-up values. This way, you gain on performance without dragging in code generation as another (complex) element of your application.
I have JSON request and response, I want to print the JSONs in the log, but there are some secured fields which I want to avoid to print in the log, I am trying to mask fields keys:
example:
before masking:
{"username":"user1","password":"123456","country":"US","creditCardNumber":"1283-1238-0458-3458"}
after masking
{"username":"user1","password":"XXXXXX","country":"US","creditCardNumber":"XXXXXX"}
I am using java Gson lib, please help me to do that
EDIT
I want to pass the keys dynamically, so in function a I want to mask these fields, but in function b different fields.
I think you should exclude that fields from log. Below is a simple example using Gson and #Expose annotation.
public static void main(String[] args) throws IOException {
String json = "{\"username\":\"user1\",\"password\":\"123456\",\"country\":\"US\",\"creditCardNumber\":\"1283-1238-0458-3458\"}";
Gson gson = new Gson();
User user = gson.fromJson(json, User.class);
System.out.println(gson.toJson(user));
Gson gsonExpose = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
System.out.println(gsonExpose.toJson(user));
}
public class User {
#Expose
private String username;
private String password;
#Expose
private String country;
private String creditCardNumber;
}
Output will be:
{"username":"user1","password":"123456","country":"US","creditCardNumber":"1283-1238-0458-3458"}
{"username":"user1","country":"US"}
Another solution using Reflection:
public static void main(String[] args) throws IOException {
String json = "{\"username\":\"user1\",\"password\":\"123456\",\"country\":\"US\",\"creditCardNumber\":\"1283-1238-0458-3458\"}";
Gson gson = new Gson();
User user = gson.fromJson(json, User.class);
List<String> fieldNames = Arrays.asList("password", "creditCardNumber");
System.out.println(mask(user, fieldNames, "XXXXXXX"));
}
public static String mask(Object object, List<String> fieldNames, String mask) {
Field[] fields = object.getClass().getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
if (fieldNames.contains(fields[i].getName())) {
try {
fields[i].setAccessible(true);
if (fields[i].get(object) != null) {
fields[i].set(object, mask);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
Gson gson = new Gson();
return gson.toJson(object);
}
I like the above solution to mask using reflection but wanted to extend same for other field types and saving masked field to unmask again.
Create annotation #MaskedField on top of field.
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface MaskedField {
}
public <T> Map<String,? super Object> maskObjectFields(T object){
Map<String,? super Object> values = new HashMap<>();
Arrays.stream(object.getClass().getDeclaredFields()).filter(field->null != field.getAnnotation(MaskedField.class)).
forEach(annotatedField->{
try {
if(annotatedField.getType().isAssignableFrom(String.class)) {
annotatedField.setAccessible(true);
values.put(annotatedField.getName(),annotatedField.get(object));
annotatedField.set(object, maskString((String) annotatedField.get(object)));
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
});
return values;
}
public <T> void unMaskObjectFields(T object,Map values){
Arrays.stream(object.getClass().getDeclaredFields()).filter(field->null != field.getAnnotation(MaskedField.class)).
forEach(annotatedField->{
try {
annotatedField.setAccessible(true);
annotatedField.set(object,values.get(annotatedField.getName()));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
});
}
private String maskString(String value){
if(Objects.isNull(value)) return null;
return null; //TODO: your logic goes here for masking
}
I'm trying to use flexjson to deserialize a string I get from a web call. The problem is that a few elements in there have a dot in the property/key for example:
[{...
"contact.name": "Erik Svensson",
"contact.mail": "erik.svensson#foo.bar",
"contact.phone": "0731123243",
...}]
Now everything else falls in place except these strings with the dots, they end up null in my target class. I'm guessing it's because it doesn't know what to map them to as I can't declare a variable in my container class that has a dot.
This is the code I'm runnign to deserialize now,
mData = new JSONDeserializer<List<Thing>>()
.use("values", Thing.class)
.deserialize(reader);
How do I modify this to catch the strings with the dot and put them in my Things class as:
String contactName;
String contactMail;
String contactPhone;
// getters&setters
Note I don't have any control over the Serialization..
OK So I've solved this but I had to abandon flexJson. Searched all over the place for a simple way but couldn't find one.
Instead I went with Jackson and this is what I ended up with:
ObjectMapper mapper = new ObjectMapper();
mThings = mapper.readValue(url, new TypeReference<List<Thing>>() {});
And in my class Thing:
#JsonProperty("contact.name")
private String contactName;
#JsonProperty("contact.mail")
private String contactMail;
#JsonProperty("contact.phone")
private String contactPhone;
// getters and setters..
If anyone knows how to do this with FlexJson feel free to post an answer, I would like to see it.
As I was curious, too, if this type of assignment can be done easily, I've played with some code, and this is what I came up with. (I'm posting it here because maybe it's helpful for somebody having some related question, or just as a point to start from.)
The PrefixedObjectFactory (see below) will cut off a fixed prefix from the JSON object's field name and use this name to find a matching bean property. The code can be easily changed to do a replacement instead (e.g. setting the first letter after a . to uppercase and remove the .)
It can be used like this:
List<Thing> l = new JSONDeserializer<List<Thing>>().use("values", new PrefixedObjectFactory(Thing.class, "contact.")).deserialize(source);
The code:
import flexjson.ObjectBinder;
import flexjson.ObjectFactory;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Type;
import java.util.Map;
public class PrefixedObjectFactory<T> implements ObjectFactory {
protected Class<T> clazz;
protected String prefix;
public PrefixedObjectFactory(Class<T> c, String prefix) {
this.clazz = c;
this.prefix = (prefix == null) ? "" : prefix;
}
#Override
public Object instantiate(ObjectBinder context, Object value, Type targetType, Class targetClass) {
try {
Class useClass = this.clazz;
T obj = (T)useClass.newInstance();
if (value instanceof Map) {
// assume that the value is provided as a map
Map m = (Map)value;
for (Object entry : m.entrySet()) {
String propName = (String)((Map.Entry)entry).getKey();
Object propValue = ((Map.Entry)entry).getValue();
propName = fixPropertyName(propName);
propValue = fixPropertyValue(propValue);
assignValueToProperty(useClass, obj, propName, propValue);
}
} else {
// TODO (left out here, to keep the code simple)
return null;
}
return obj;
} catch (Exception ex) {
return null;
}
}
protected String fixPropertyName(String propName) {
if (propName.startsWith(this.prefix)) {
propName = propName.substring(this.prefix.length());
}
return propName;
}
protected Object fixPropertyValue(Object propValue) {
return propValue;
}
protected PropertyDescriptor findPropertyDescriptor(String propName, Class clazz) {
try {
return new PropertyDescriptor(propName, clazz);
} catch (Exception ex) {
return null;
}
}
protected void assignValueToProperty(Class clazz, Object obj, String propName, Object propValue) {
try {
PropertyDescriptor propDesc = findPropertyDescriptor(propName, clazz);
if (propDesc != null) {
propDesc.getWriteMethod().invoke(obj, propValue);
}
} catch (Exception ex) {
}
}
}
I actually have some web-services make with restfullyii in PHP.
But I have some troubles to deserialize response of my web-services with jackson.
This is an example of response :
{"success":true,"message":"Record(s) Found","data":{"totalCount":"1","user":{...}}}
To deserialize this response I make this model :
#JsonIgnoreProperties(ignoreUnknown = true)
public class response {
#JsonProperty("data")
private HashMap<String, Object> data;
#JsonProperty("message")
private String message;
#JsonProperty("success")
private Boolean success;
public HashMap<String, Object> getData() {
return data;
}
public void setData(HashMap<String, Object> data) {
this.data = data;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Boolean getSuccess() {
return success;
}
public void setSuccess(Boolean success) {
this.success = success;
}
}
And to deserialize user I use these lines :
(rst is a result of deserialize response)
ObjectMapper mapper = new ObjectMapper();
try {
String rstTxt = String.valueOf(rst.getData().get("user"));
System.out.println(rstTxt);
user user = mapper.readValue(rstTxt, user.class);
} catch (JsonParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (JsonMappingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
But it doesn't work because "rst.getData().get("user")" returns a string in this schema :
{ attribute = value }
Indeed, the following exception is returned :
org.codehaus.jackson.JsonParseException: Unexpected character ('i' (code 105)): was expecting double-quote to start field name
Have you an idea about how I could do to deserialize user attribute ?
Thank you.
I'm quite guessing but maybe, since you've defined a Map<String, Object>, your User should be already deserialized into an Object, and maybe you should just try to cast it:
User user = (User) rst.getData().get("user");
otherwise you can modify slightly your code to match exactly the response, like:
public class Response {
private String message;
private Boolean success;
private Data data;
}
and
public class Data {
private String totalCount;
private User user;
}
In this way you should get everything immeadiately deserialized.
Tip: if your variable names are the same you don't need the #JsonProperty annotation!