I have the following entity class:
public class Conversation {
private String id;
private String ownerId;
private Long creationDate;
public Conversation(String id, String ownerId, Long creationDate){
this.id = id;
this.ownerId = ownerId;
this.creationDate = creationDate;
}
}
On other submodule through an external service, on each insertion, I recive a map of the following entities:
public class AttributeValue {
private Sring s; //string attribute
private String n; //number attribute
public String getS() {
return this.s;
}
public String getN() {
return this.n;
}
public AttributeValue(String s, String n){
this.s = s;
this.n = n;
}
}
//Example if I insert this conversation: new Conversation("1", "2", 1623221757971)
// I recive this map:
Map<String, AttributeValue> insertStream = Map.ofEntries(
entry("id", new AttributeValue("1", null)),
entry("ownerId", new AttributeValue("2", null)),
entry("creationDate", new AttributeValue(null, "1623221757971"))
);
To read the ownerId field from the map, I have to do this:
String ownerId = insertStream.get("ownerId").getS();
My question is, instead of have to write: insertStream.get("ownerId"), exists any way through Reflection to read the name of the field from the entity (Conversation.ownerId)?
This is because we want to mantain the submodule and If we make a change on the entitity, for example change ownerId for ownerIdentifier, the submodule shows a compilation error or is changed automatically.
Is this what you want? Field#getName()
Example code:
Field[] conversationFields = Conversation.class.getDeclaredFields();
String field0Name = conversationFields[0].getName();
Depending on the JVM used, field0Name can be "id". You can also use Class#getFields(), this method includes all Fields that are accessible in this class (super class's fields).
Another option (not using reflection) would be to refactor your code.
import java.util.Map;
import java.util.HashMap;
public class Conversation {
public static String[] names = {
"id", "ownerId", "creationDate"
};
private Map<String, Object> data = new HashMap<String,Object>();
public Conversation(Object... data) {
if(data.length!=names.length)
throw new IllegalArgumentException("You need to pass "+names.length+" arguments!");
for(int i=0; i<names.length; i++)
data.put(names[i],data[i]);
}
public Map<String,Object> getData() { return data; }
// You can pass "id"/"ownerId" or names[0]/names[1]
public String getString(String key) {
return (String)data.get(key);
}
// You can pass "creationDate" or names[2]
public long getLong(String key) {
return (long)data.get(key);
}
}
You could then create Conversation Objects like before:
Conversation c = new Conversation("myId","myOwnerId",123456789L);
You could also add public static String fields like ID="id", but changing the value of a field will never change the field's name.
Related
I'm trying to expand on this Baeldung tutorial https://www.baeldung.com/rest-api-search-language-spring-data-specifications
But I want the Specification to be Generic and I wanted to allow the client to search by values of embedded objects. Everything works for String and some numbers, but not for ids and other more complicated objects like Date.
My Model: (assume a person can only have 1 pet)
#Entity
public Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private ID id;
private String name;
private Date dateOfBirth
private Integer age;
private Pet pet;
// Getter & Setters etc
}
#Entity
public Pet {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private ID id;
private String type;
private String name;
private Integer numOfLegs;
// Getter & Setters etc
}
Person repository:
#Repository
public interface PersonRepository extends JpaRepository<Person, Integer>, JpaSpecificationExecutor<Person>{}
Search Criteria that will hold the key, operator and value that we can search by.
public class EntitySearchCriteria {
private String key;
private String operation;
private Object value;
public EntitySearchCriteria(final String key, final String operation, final Object value) {
this.key = key;
this.operation = operation;
this.value = value;
}
// Getters and Setters etc
My Generic Specification class (this is really where the action is to build the predicates that are to be used). This also allows the client to set a SearchCriteria on a value of a joined table. e.g. "Pet.name=Muffins"
public abstract class AbstractEntitySpecification<T, ID extends Serializable> implements Specification<T> {
protected EntitySearchCriteria criteria;
#Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
if (criteria.getOperation().equalsIgnoreCase(">")) {
return criteriaBuilder.greaterThanOrEqualTo(root.<String>get(criteria.getKey()), criteria.getValue().toString());
} else if (criteria.getOperation().equalsIgnoreCase("<")) {
return criteriaBuilder.lessThanOrEqualTo(root.<String>get(criteria.getKey()), criteria.getValue().toString());
} else if (criteria.getOperation().equalsIgnoreCase(":")) {
if (criteria.getKey().contains(".")) {
String[] joinCriteriaArray = criteria.getKey().split("\\.");
Class<?> joinedClass = root.get(joinCriteriaArray[0]).getClass();
Join<T, ?> joinedRelationship = root.join(joinCriteriaArray[0]);
return criteriaBuilder.equal(joinedRelationship.get(joinCriteriaArray[1]), criteria.getValue());
}
if (root.get(criteria.getKey()).getJavaType() == String.class) {
return criteriaBuilder.like(root.<String>get(criteria.getKey()), "%" + criteria.getValue() + "%");
} else {
return criteriaBuilder.equal(root.get(criteria.getKey()), criteria.getValue());
}
}
return null;
}
}
Any Entity that I want to allow this type of Querying then just needs to have a concrete implementation of the AbstractEntitySpecification
public class PersonSpecification extends AbstractEntitySpecification<Person, Integer> {
public PersonSpecification (final EntitySearchCriteria entitySearchCriteria) {
this.criteria = entitySearchCriteria;
}
}
These are the tests that I have run. Any search on a attribute of Person that is a String or Int (i.e. Person.name, Person.age) will work, but a search on dateOfBirth will not.
Any search on an attribute of the pet that is a string will work using the join, but searching on the id(Integer) will not, no matter if I pass the id as an Int, or a String. I have put the behaviour in a comment for each test.
public class PersonSpecificationMediumTest extends AbstractMediumTest {
#Autowired
private PersonRepository personRepository;
#Autowired
private PetRepository petRepository;
Person person1;
Person person2;
#Before
public void setUp() {
Pet muffins = new Pet(1, "cat", "muffins", 4);
Pet rex= new Pet(2, "dog", "rex", 4);
petRepository.saveAll(Arrays.asList(muffins , rex));
person1 = new Person();
person1.setName("David");
person1.setDateOfBirth(Date.parse("1979-03-01");
person1.setPet(muffins);
person1 = personRepository.saveAndFlush(person1);
person2 = new Person();
person2.setName("Mary");
person2.setDateOfBirth(Date.parse("1982-03-01");
person2.setPet(rex);
person2 = personRepository.saveAndFlush(person2);
}
#Test //Works
public void toPredicate_findByNameEquals_assertCorrectResult() {
PersonSpecification spec
= new PersonSpecification(new EntitySearchCriteria("name", ":", "David"));
List<Person> results = personRepository.findAll(spec);
Assert.assertEquals(person1, results.get(0));
}
#Test // Works
public void toPredicate_findByPetNameEquals_assertCorrectResult() {
PersonSpecification spec
= new PersonSpecification(new EntitySearchCriteria("client.name", ":", "Rex"));
List<Person> results = personRepository.findAll(spec);
Assert.assertEquals(person2, results.get(0));
}
#Test // Return empty list. Cannot find the pet by Id.
public void toPredicate_findByPetIdEquals_assertCorrectResult() {
PersonSpecification spec
= new PersonSpecification(new EntitySearchCriteria("pet.id", ":", 2));
List<Person> results = personRepository.findAll(spec);
Assert.assertEquals(person2, results.get(0));
}
#Test // org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [2] did not match expected type [java.lang.Integer (n/a)];
public void toPredicate_findByPetIdAsStringEquals_assertCorrectResult() {
PersonSpecification spec
= new PersonSpecification(new EntitySearchCriteria("pet.id", ":", "2"));
List<Person> results = personRepository.findAll(spec);
Assert.assertEquals(person2, results.get(0));
}
#Test // Fails on org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [2020-01-01] did not match expected type [java.util.Date (n/a)]
public void toPredicate_findByDateOfBirthBetween_assertCorrectResult() {
PersonSpecification spec1
= new PersonSpecification(new EntitySearchCriteria("dateOfBirth", "<", "1990-01-01"));
PersonSpecification spec2
= new PersonSpecification(new EntitySearchCriteria("dateOfBirth", ">", "1970-01-01"));
List<Person> results = personRepository.findAll(spec1.and(spec2));
Assert.assertTrue(results.size() == 2);
}
}
Any idea why Date is so problematic? I wanted use the date in the greaterThanOrEqualTo and lessThanOrEqualTo, but passing in criteria.getValue(Object) gives a compile error so it forces me to use a string representation of the object. But the error shown is org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [2020-01-01] did not match expected type [java.util.Date (n/a)] which indicates to me that it cannot compare a String to a Date, which makes sense, but why stop me from passing the Date object?
Also, why is Id such an issue on the joined table? Why can it not find id = 2, I would have thought it would be straight forward, especially since I can search by the number of legs of the Pets successfully. It must have something to do with id being Serializable.
Check out JavaDoc for Date.parse. The essential part is already with the declaration:
#Deprecated
public static long parse(String s)
As it is clearly stated it returns a long value. To get a Date object you could use SimpleDateFormat that inherits DateFormat.parse(String s), like:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date d1 = sdf.parse("1979-03-01");
I have the following Json
{
"coreId" : "1",
"name" : "name",
"additionalValueList" : [
{
"columnName" : "allow_duplicate",
"rowId" : "10",
"value" : "1"
},
{
"columnName" : "include_in_display",
"rowId" : "11",
"value" : "0"
},
...e.t.c
]
},
...e.t.c
and Java class
class DTO {
#JsonProperty("coreId")
private Integer id;
private String name;
private Boolean allowDuplicate;
private Boolean includeInDisplay;
}
How I can easily map values from 'additionalValueList' to corresponding java fields.For example Json value from field 'columnName' - 'allow_duplicate' = DTO.allowDuplicate.
Actually I know how to do it with custom deserializers with #JsonDeserialize annotation and smth like this.Bu I have 40+ DTO and it is not a good idea to create own deserializer for each filed. I am looking for solution to have for example 1 deserializer(since values structure in 'additionalValueList' are the same for all entities) and to pass parameter(field name that I want to map to that field) to custom deserializer that will find in 'additionalValueList' entity with 'column Name' = parameter(that I passed from annotation) and return 'value'.
Example
class DTO {
#JsonProperty("coreId")
private Integer id;
private String name;
#JsonDeserialize(using = MyCustDeser.class,param = allow_duplicate)
private Boolean allowDuplicate;
#JsonDeserialize(using = MyCustDeser.class,param = include_in_display)
private Boolean includeInDisplay;
}
It will be a good solution but maybe not easy to achieve.However I will be very grateful for all your advices.Thank you.
Create a Converter class, then specify it on the DTO class.
The following code uses public fields for the simplicity of the example.
/**
* Intermediate object used for deserializing FooDto from JSON.
*/
public final class FooJson {
/**
* Converter used when deserializing FooDto from JSON.
*/
public static final class ToDtoConverter extends StdConverter<FooJson, FooDto> {
#Override
public FooDto convert(FooJson json) {
FooDto dto = new FooDto();
dto.name = json.name;
dto.id = json.coreId;
dto.allowDuplicate = lookupBoolean(json, "allow_duplicate");
dto.includeInDisplay = lookupBoolean(json, "include_in_display");
return dto;
}
private static Boolean lookupBoolean(FooJson json, String columnName) {
String value = lookup(json, columnName);
return (value == null ? null : (Boolean) ! value.equals("0"));
}
private static String lookup(FooJson json, String columnName) {
if (json.additionalValueList != null)
for (FooJson.Additional additional : json.additionalValueList)
if (columnName.equals(additional.columnName))
return additional.value;
return null;
}
}
public static final class Additional {
public String columnName;
public String rowId;
public String value;
}
public Integer coreId;
public String name;
public List<Additional> additionalValueList;
}
You now simply annotate the DTO to use it:
#JsonDeserialize(converter = FooJson.ToDtoConverter.class)
public final class FooDto {
public Integer id;
public String name;
public Boolean allowDuplicate;
public Boolean includeInDisplay;
#Override
public String toString() {
return "FooDto[id=" + this.id +
", name=" + this.name +
", allowDuplicate=" + this.allowDuplicate +
", includeInDisplay=" + this.includeInDisplay + "]";
}
}
Test
ObjectMapper mapper = new ObjectMapper();
FooDto foo = mapper.readValue(new File("test.json"), FooDto.class);
System.out.println(foo);
Output
FooDto[id=1, name=name, allowDuplicate=true, includeInDisplay=false]
I am trying to have a class that has a certain list of objects (specified by another class) persisted in the database as a string (use JPA Converter - all good).
And then I want to use Specification to search inside that string.
What is the best way to create the predicates? I don't seem to understand the connection bettween the AttributeConverter and the Expression in the Specification.
The parent class:
#Entity #Table
public class A {
#Column #Id #GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#Column
private String name;
#Enumerated(EnumType.STRING)
private SomeType type;
#Column(length=1000) #Convert(converter = BConverter.class)
private List<B> bList;
private Integer no;
}
The listed object class:
public class B{
private String type;
private Integer quantity;
}
The converter:
#Converter
public class BConverter implements AttributeConverter<List<B>, String> {
private static final String SEPARATOR = ":";
private static final String LIST_SEPARATOR = ";";
#Override public String convertToDatabaseColumn(List<B> bList) {
return bList.stream().map(e->convertToString(e)).collect(Collectors.joining(LIST_SEPARATOR));
}
#Override public List<B> convertToEntityAttribute(String str) {
if(str==null || str.isEmpty() ) return null;
return Arrays.stream(str.split(LIST_SEPARATOR)).map(e->convertFromString(e)).collect(Collectors.toList());
}
private String convertToString(B b){
if(entity==null) return null;
return b.getType().toString() +SEPARATOR+ b.getQuantity().toString();
}
private B convertFromString(String subStr){
if(subStr==null || subStr.isEmpty() ) return null;
String[] pair = subStr.split(SEPARATOR);
return new B(pair[0],Integer.valueOf(pair[1]));
}
}
In the database should look something like:
Table A:
id: 1;
name: "Some Name";
type: "THISTYPE";
blist: "TYPE1:11;TYPE2:22";
no: 0;
id: 2;
name: "Other Name";
type: "THISTYPE";
blist: "TYPE1:45;TYPE2:56";
no: 12;
I would then like to create Specifications to search over this table for the attributes inside the bList.
For example, search by an entity that contains a B object where type=TYPE1 and a quantity>=30.
public static Specification<A> customSpecification(String type, Integer value) {
return (root, query, builder) -> ///?????????
}
Is there a way to use such specifications where the DB attribute is a String but JAVA only sees the objects?
I have this class:
public class Campaign
{
String Campaign_ID;
String Campaign_Description;
String startQuestion;
ArrayList Question_Array;
ArrayList workflow;
public Campaign(String ID, String description, String start, ArrayList quesArray, ArrayList workflow)
{
this.Campaign_ID = ID;
this.Campaign_Description = description;
this.startQuestion = start;
this.Question_Array = quesArray;
this.workflow = workflow;
}
public String getID() {return this.Campaign_ID;}
public String getDescription() {return this.Campaign_Description;}
public String getStartQuestion() {return this.startQuestion;}
public ArrayList getQuestionArray() {return this.Question_Array;}
public ArrayList getWorkflow() {return this.workflow;}
public Base_Question getQuestionByID(String ID){
Base_Question result = null;
for (int i=0;i<Question_Array.size();i++)
{
if ( ((Base_Question) Question_Array.get(i)).getQuestionID().equals(ID) )
result = (Base_Question) Question_Array.get(i);
}
return result;
}
}
The ArrayList Question_Array contains a list of instances from other classes.
Then I initialize an instance of Campaign:
Campaign campaign = new Campaign("Cam999","description blabla","Q001",list, workflow);
Then I serialize this instance into text:
Gson gson = new Gson();
String text = gson.toJson(campaign);
To get back the instance of Campaign class, I use:
Campaign campaign2 = gson.fromJson(text, Campaign.class);
I can print things from campaign2 correctly, like campaign2.getID() returns Cam999. But the problem is the ArrayList inside campaign2.
campaign2.getQuestionArray().get(index).getClass() prints com.google.gson.internal.LinkedTreeMap, not my custom class.
My whole source code is here https://github.com/khoiboo/Class_Hierarchy
How to solve this problem. I appreciate your help so much
What design-pattern, if any, would be most appropriate in this situation.
public class PersonFromDB1 {
private String firstName;
private String lastName;
private String Car;
}
public class PersonFromDB2 {
private String first_name;
private String last_name;
private String boat;
}
Out of these two person types, the only data I would like to work on is fist name and last name regardless of how it field name is name inside the different DBs. firstName and first_name represents the same - name of a person/customer - so does lastName and last-name. The car and boat fields are, in my example, completely irrelevant and should therefore be ignored.
Using, maybe polymorphism or the adapter pattern (?), I would like to create a list of objects that includes persons from DB1 and DB2 under the same type - of PersonInOurDB.
In the end, my goal is to be able to call GSON serialization/desarialization on myClass alone.
public class PersonInOurDB {
private String firstname;
private String lastname;
}
A simple selection based on the type is all you really need. This could be considered a builder pattern because it just initializes a new instance of myClass.
Note, this is rough pseudo code.
FunctionName(SomeType instance)
{
string aPostfix = "_1";
string bPostfix = "_2";
string selectedPostFix;
// This is your strategy selector
switch(typeof(SomeType.Name)
{
case "TypeA":
selectedPostFix = aPostFix;
case "TypeB":
selectedPostFix = bPostFix;
}
return new myClass()
{
A = instance.GetProperty("A" + selectedPostfix).Value,
B = instance.GetProperty("B" + selectedPostfix).Value,
...
}
}
If you want a common access api in java for both objects, then introduce an interface and let both implement it.
If you only want both objects (PersonFromDB1 and PersonFromDB2) to be serialized in the same way by json you can either:
use annotations - the #SerializedName annotation in combination with #Expose.
use the FieldNamingStratgy and ExclusionStrategy
Use annotations to control the serialization
public class PersonFromDB1 {
#Expose
#SerializedName("firstName")
private String firstName;
#Expose
#SerializedName("lastName")
private String lastName;
private String car;
}
public class PersonFromDB2 {
#Expose
#SerializedName("firstName")
private String first_Name;
#Expose
#SerializedName("lastName")
private String last_Name;
private String boat;
}
Then you can use the GsonBuilder
Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
PersonFromDB1 person1 = ...; // get the object
PersonFromDB2 person2 = ...; // get the object
System.out.println(gson.toJson(person1));
System.out.println(gson.toJson(person2));
Use FieldNamingStratgy and ExclusionStrategy to control the serialization
If you don't want to modify the db objects (you can't or you don't want to add annotations) than there is another way. You can use a FieldNamingStratgy and ExclusionStrategy.
class PersonFromDBNamingStrategy implements FieldNamingStrategy {
Map<String, String> fieldMapping = new HashMap<String, String>();
public PersonFromDBNamingStrategy() {
fieldMapping.put("first_Name", "firstName");
fieldMapping.put("last_Name", "lastName");
}
#Override
public String translateName(Field f) {
String name = f.getName();
if(fieldMapping.contains(name)){
return fieldMapping.get(name);
}
return name;
}
}
and the ExclusionStrategy
class PersonFromDExclusionStrategy implements ExclusionStrategy {
List<String> validNames = Arrays.asList("car", "boat");
#Override
public boolean shouldSkipField(FieldAttributes f) {
String name = f.getName();
return !validNames.contains(name);
}
#Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
}
after that just create Gson like this:
GsonBuilder gsonBuilder = new GsonBuilder();
sonBuilder.addSerializationExclusionStrategy(new PersonFromDExclusionStrategy());
gsonBuilder.setFieldNamingStrategy(new PersonFromDBNamingStrategy());
Gson gson = gsonBuilder.create();
PersonFromDB1 person1 = ...; // get the object
PersonFromDB2 person2 = ...; // get the object
System.out.println(gson.toJson(person1));
System.out.println(gson.toJson(person2));