I have an entity where I set the max for every String field like the following:
#Column(name = "abc")
#Size(max = 10)
private String abc;
#Column(name = "xyz")
#Size(max = 50)
private String xyz;
I want to write a Converter to truncate that field if exceeds max size. Something like this:
import javax.persistence.AttributeConverter;
import javax.persistence.Convert;
#Convert
public class TruncatedStringConverter implements AttributeConverter<String, String> {
private static final int LIMIT = 999;
#Override
public String convertToDatabaseColumn(String attribute) {
if (attribute == null) {
return null;
} else if (attribute.length() > LIMIT) {
return attribute.substring(0, LIMIT);
} else {
return attribute;
}
}
#Override
public String convertToEntityAttribute(String dbData) {
return dbData;
}
}
But the thing is that I can't use LIMIT since I need to look to the Size annotation. Is there any way to access the field name from the converter?
This way I could use reflection to read the max size.
Thank you!
Related
I have a room service which returns detail for rooms when requesting http://localhost:8082/room/search/byRoomChar
#GetMapping(path = "/search/byRoomChar")
public #ResponseBody List<Room> byCapacity(#RequestParam(required = false) Integer capacity,
#RequestParam(required = false) Boolean isUnderMaintenance,
#RequestParam(required = false) String equipment) {
return roomRepository.findByRoomChar(capacity, isUnderMaintenance, equipment);
}
Now I want to request this #GetMapping from the booking service since this is the application gateway that users are going to interact with using the http://localhost:8081/booking/search/byRoomChar.
#GetMapping(path = "/search/byRoomChar")
public #ResponseBody List<Room> byCapacity(#RequestParam(required = false) Integer capacity,
#RequestParam(required = false) Boolean isUnderMaintenance,
#RequestParam(required = false) String equipment) {
ResponseEntity<Room[]> roomsResponse = restTemplate.getForEntity("http://localhost:8082/room/search/byRoomChar?capacity=" + capacity + "&isUnderMaintenance=" +
isUnderMaintenance + "&equipment=" + equipment, Room[].class);
return Arrays.asList(roomsResponse.getBody());
}
Room entity code:
package nl.tudelft.sem.template.entities;
import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
#Entity
#Table(name = "Room")
public class Room {
#EmbeddedId
private RoomId id;
#Column(name = "capacity")
private int capacity;
#Column(name = "numberOfPeople")
private int numberOfPeople;
#Column(name = "isUnderMaintenance", nullable = false)
private boolean isUnderMaintenance;
#Column(name = "equipment")
private String equipment;
public Room() {
}
public Room(long roomNumber, long buildingNumber, int capacity,
int numberOfPeople, boolean isUnderMaintenance, String equipment) {
RoomId id = new RoomId(roomNumber, buildingNumber);
this.id = id;
this.capacity = capacity;
this.numberOfPeople = numberOfPeople;
this.isUnderMaintenance = isUnderMaintenance;
this.equipment = equipment;
}
public RoomId getId() {
return id;
}
public void setId(RoomId id) {
this.id = id;
}
public int getCapacity() {
return capacity;
}
public void setCapacity(int capacity) {
this.capacity = capacity;
}
public int getNumberOfPeople() {
return numberOfPeople;
}
public void setNumberOfPeople(int numberOfPeople) {
this.numberOfPeople = numberOfPeople;
}
public boolean getIsUnderMaintenance() {
return isUnderMaintenance;
}
public void setUnderMaintenance(boolean underMaintenance) {
isUnderMaintenance = underMaintenance;
}
public String getEquipment() {
return equipment;
}
public void setEquipment(String equipment) {
this.equipment = equipment;
}
}
Room repository code:
package nl.tudelft.sem.template.repositories;
import java.util.List;
import nl.tudelft.sem.template.entities.Room;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
#Repository
public interface RoomRepository extends JpaRepository<Room, Integer> {
#Query("SELECT r FROM Room r WHERE (:number is null or r.id.number = :number)"
+ "and r.id.buildingNumber = :buildingNumber")
List<Room> findByRoomNum(#Param("number") Long number,
#Param("buildingNumber") Long buildingNumber);
#Query("SELECT r FROM Room r WHERE (:capacity is null or r.capacity = :capacity) and"
+ "(:isUnderMaintenance is null or r.isUnderMaintenance = :isUnderMaintenance) and"
+ "(:equipment is null or r.equipment = :equipment)")
List<Room> findByRoomChar(#Param("capacity") Integer capacity,
#Param("isUnderMaintenance") Boolean isUnderMaintenance,
#Param("equipment") String equipment);
}
However, this does not work because when omitting parameters when calling the getmapping from the booking service all parameter values are turned into null because of the required=false. And this are converted into Strings inside the hard coded url.
2021-12-04 17:13:03.883 WARN 16920 --- [nio-8082-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.lang.Boolean'; nested exception is java.lang.IllegalArgumentException: Invalid boolean value [null]]
How can I make a get http request with optional parameters from within the code?
UriComponentsBuilder can help with URI construction. It correctly handles nullable query parameters.
String uri = UriComponentsBuilder.fromHttpUrl("http://localhost:8082/room/search/byRoomChar")
.queryParam("capacity", capacity)
.queryParam("isUnderMaintenance", isUnderMaintenance)
.queryParam("equipment", equipment)
.encode().toUriString();
ResponseEntity<Room[]> roomsResponse = restTemplate.getForEntity(uri, Room[].class);
Also, following answer can be helpful: https://stackoverflow.com/a/25434451/5990117
If the parameters are not mandatory in your Room API but you still use them in your call to the Database either you have sensible defaults if they are actually not provided by the user. Something along the following lines (in this case you actually don't need to explicitly define required = false):
#GetMapping(path = "/search/byRoomChar")
public #ResponseBody List<Room> byCapacity(#RequestParam(defaultValue = "10") Integer capacity,
#RequestParam(defaultValue = "false") Boolean isUnderMaintenance,
#RequestParam(defaultValue = "default-equipment") String equipment) {
return roomRepository.findByRoomChar(capacity, isUnderMaintenance, equipment);
}
Or you define a Repository method with no additional parameters, but this might be trickier since you basically need all the possibilities of null and non-null parameters.
This is because the URI string being built when the parameters are null is like the follwing:
http://localhost:8082/room/search/byRoomChar?isUnderMaintenance=null
Since the "null" is being appended as a value of a parameter, the room server fails trying to deserialize it to a different type.
For example in the error message you gave means that the "isUnderMaintenance" should be boolean but is "null" string.
To solve this problem, I recommend using the UriComponentBuilder.
#Test
fun constructUriWithQueryParameter() {
val uri = UriComponentsBuilder.newInstance()
.scheme("http")
.host("localhost")
.port(8082)
.path("/room/search/byRoomChar")
.query("capacity={capa}")
.query("isUnderMaintenance={isUnderMaintenance}")
.query("equipment={equip}")
.buildAndExpand(null, null, null)
.toUriString()
assertEquals(
"http://localhost:8082/room/search/byRoomChar
capacity=&isUnderMaintenance=&equipment=",
uri
)
}
I tried Petr Aleksandrov answer. Looks very clean and it is most probably the way to go, but I was getting an "not absolute uri" exception.
Didn't have time to look for answers so I created my workaround code. Messy but it worked.
#GetMapping(path = "/search/byRoomChar")
public #ResponseBody List<Room> byCapacity(#RequestParam(required = false) Integer capacity,
#RequestParam(required = false) Boolean isUnderMaintenance,
#RequestParam(required = false) String equipment) {
if(capacity == null && isUnderMaintenance == null && equipment == null) {
ResponseEntity<Room[]> roomsResponse = restTemplate.getForEntity("http://localhost:8082/room/search/byRoomChar", Room[].class);
return Arrays.asList(roomsResponse.getBody());
}
String url = "http://localhost:8082/room/search/byRoomChar?";
if(capacity != null) {
url += "capacity=" + capacity + "&";
}
if(isUnderMaintenance != null) {
url += "isUnderMaintenance=" + isUnderMaintenance + "";
}
if(equipment != null) {
url += "equipment=" + equipment;
}
ResponseEntity<Room[]> roomsResponse = restTemplate.getForEntity(url, Room[].class);
return Arrays.asList(roomsResponse.getBody());
}
I wan to use ENUM to map values into database table rows:
BusinessCustomersSearchParams:
#Getter
#Setter
public class BusinessCustomersSearchParams {
private String title;
private List<String> status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
Specification:
#Override
public Page<BusinessCustomersFullDTO> findBusinessCustomers(BusinessCustomersSearchParams params, Pageable pageable)
{
Specification<BusinessCustomers> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (params.getTitle() != null) {
predicates.add(cb.like(cb.lower(root.get("description")), "%" + params.getTitle().toLowerCase() + "%"));
}
final List<String> statuses = Optional.ofNullable(params.getStatus()).orElse(Collections.emptyList());
if (statuses != null && !statuses.isEmpty()){
List<BusinessCustomersStatus> statusesAsEnum = statuses.stream()
.map(status -> BusinessCustomersStatus.fromStatus(status))
.collect(Collectors.toList())
;
predicates.add(root.get("status").in(statusesAsEnum));
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
};
return businessCustomersService.findAll(spec, pageable).map(businessCustomersMapper::toFullDTO);
}
AttributeConverter:
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
#Converter
public class BusinessCustomersStatusAttributeConverter
implements AttributeConverter<BusinessCustomersStatus, String> {
public String convertToDatabaseColumn( BusinessCustomersStatus value ) {
if ( value == null ) {
return null;
}
return value.getStatus();
}
public BusinessCustomersStatus convertToEntityAttribute( String value ) {
if ( value == null ) {
return null;
}
return BusinessCustomersStatus.fromStatus( value );
}
}
Enum:
package org.merchant.database.service.businesscustomers;
public enum BusinessCustomersStatus {
A("active"),
O("onboarding"),
N("not_verified"),
V("verified"),
S("suspended"),
I("inactive");
private String status;
BusinessCustomersStatus(String status)
{
this.status = status;
}
public String getStatus() {
return status;
}
public static BusinessCustomersStatus fromStatus(String status) {
switch (status) {
case "active": {
return A;
}
case "onboarding": {
return O;
}
case "not_verified": {
return NV;
}
case "verified": {
return V;
}
case "suspended": {
return S;
}
case "inactive": {
return I;
}
default: {
throw new UnsupportedOperationException(
String.format("Unkhown status: '%s'", status)
);
}
}
}
}
Entity:
#Entity
#Table(name = "business_customers")
public class BusinessCustomers implements Serializable {
..........
#Convert( converter = BusinessCustomersStatusAttributeConverter.class )
private BusinessCustomersStatus status;
......
}
Full code example: https://github.com/rcbandit111/Search_specification_POC
I send http query with params list?size=5&page=0&status=active,suspended and I get result with capital letters "status": "ACTIVE".
I wan to search and get status from FE for status using status=active but store into database row field only symbol A.
How I can store into database the ENUM key A?
In order to store the actual enumeration value in the database, you can do two things.
One, as suggested by #PetarBivolarski, modify the method convertToDatabaseColumn in AttributeConverter and return value.name() instead of value.getStatus(). But please, be aware that in addition you will need to update the convertToEntityAttribute as well to take into account that change:
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
#Converter
public class BusinessCustomersStatusAttributeConverter
implements AttributeConverter<BusinessCustomersStatus, String> {
public String convertToDatabaseColumn( BusinessCustomersStatus value ) {
if ( value == null ) {
return null;
}
return value.name();
}
public BusinessCustomersStatus convertToEntityAttribute( String value ) {
if ( value == null ) {
return null;
}
return BusinessCustomersStatus.valueOf( value );
}
}
If you think about it, a more straightforward solution will be just keep the status field as #Enumerated:
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#Builder(toBuilder = true)
#Entity
#Table(name = "business_customers")
public class BusinessCustomers {
//...
#Enumerated(EnumType.STRING)
#Column(name = "status", length = 20)
private BusinessCustomersStatus status;
//...
}
It is in addition more according to the rest of your code.
Regarding your second problem, the application is returning "status":"ACTIVE" because in BusinessCustomersFullDTO you are defining the status field as String and this field receives the result of the mapping process performed by #Mapstruct and BusinessCustomersMapper.
To solve that issue, as I suggested you previously, you can modify your Mapper to handle the desired custom conversion:
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.merchant.config.BaseMapperConfig;
import org.merchant.database.entity.BusinessCustomers;
import org.merchant.database.service.businesscustomers.BusinessCustomersStatus;
import org.merchant.dto.businesscustomers.BusinessCustomersFullDTO;
#Mapper(config = BaseMapperConfig.class)
public interface BusinessCustomersMapper {
#Mapping(source = "status", target = "status", qualifiedByName = "businessCustomersToDTOStatus")
BusinessCustomersFullDTO toFullDTO(BusinessCustomers businessCustomers);
#Named("busineessCustomersToDTOStatus")
public static String businessCustomersToDTOStatus(final BusinessCustomersStatus status) {
if (status == null) {
return null;
}
return status.getStatus();
}
}
If you do not prefer this solution, perhaps you can take a different approach: it will consist in the following. The idea is modifying the Jackson serialization and deserialization behavior of BusinessCustomersFullDTO. In fact, in your use case only is necessary to modify the serialization logic.
First, define the status field in BusinessCustomersFullDTO in terms of BusinessCustomersStatus as well:
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#Builder(toBuilder = true)
public class BusinessCustomersFullDTO {
private long id;
private String name;
private String businessType;
private BusinessCustomersStatus status;
private String description;
private String country;
private String address1;
}
To complete the solution, please, perform the following changes in the BusinessCustomersStatus enum:
public enum BusinessCustomersStatus {
A("active"),
O("onboarding"),
NV("not_verified"),
V("verified"),
S("suspended"),
I("inactive");
private String status;
BusinessCustomersStatus(String status)
{
this.status = status;
}
// Define the status field as the enum representation by using #JsonValue
#JsonValue
public String getStatus() {
return status;
}
// Use the fromStatus method as #JsonCreator
#JsonCreator
public static BusinessCustomersStatus fromStatus(String status) {
if (StringUtils.isEmpty(status)) {
return null;
}
switch (status) {
case "active": {
return A;
}
case "onboarding": {
return O;
}
case "not_verified": {
return NV;
}
case "verified": {
return V;
}
case "suspended": {
return S;
}
case "inactive": {
return I;
}
default: {
throw new UnsupportedOperationException(
String.format("Unkhown status: '%s'", status)
);
}
}
}
}
Note the inclusion of the #JsonValue and #JsonCreator annotations: the later is using for deserialization which seems unnecessary to me in your application, but just in case.
Please, see the relevant documentation of the provided Jackson annotations.
Notice your convertToDatabaseColumn() method in BusinessCustomersStatusAttributeConverter.
It should return value.name() instead of value.getStatus().
I want to use Simple XML to deserialize the following XML into a POJO:
<shippingInfo>
<shippingServiceCost currencyId="USD">9.8</shippingServiceCost>
<shippingType>Flat</shippingType>
<shipToLocations>Worldwide</shipToLocations>
<expeditedShipping>true</expeditedShipping>
<oneDayShippingAvailable>false</oneDayShippingAvailable>
<handlingTime>3</handlingTime>
</shippingInfo>
I have created the following class to do so. However, I'm having trouble in that the currencyId attribute isn't being properly deserialized.
#Root(name = "shippingInfo")
public class ShippingInfo {
#Element(name = "shippingServiceCost", required = false)
private BigDecimal shippingServiceCost;
#Attribute(name = "currencyId", required = false)
private String currencyId;
#Element(name = "shippingType", required = false)
private String shippingType;
#Element(name = "shipToLocations" ,required = false)
private String shipToLocations;
#Element(name = "expeditedShipping", required = false)
private Boolean expeditedShipping;
#Element(name = "oneDayShippingAvailable", required = false)
private Boolean oneDayShippingAvailable;
#Element(name = "handlingTime", required = false)
private Integer handlingTime;
// Getters & Setters
public BigDecimal getShippingServiceCost() {
return shippingServiceCost;
}
public void setShippingServiceCost(BigDecimal shippingServiceCost) {
this.shippingServiceCost = shippingServiceCost;
}
public String getCurrencyId() {
return currencyId;
}
public void setCurrencyId(String currencyId) {
this.currencyId = currencyId;
}
public String getShippingType() {
return shippingType;
}
public void setShippingType(String shippingType) {
this.shippingType = shippingType;
}
public String getShipToLocations() {
return shipToLocations;
}
public void setShipToLocations(String shipToLocations) {
this.shipToLocations = shipToLocations;
}
public Boolean isExpeditedShipping() {
return expeditedShipping;
}
public void setExpeditedShipping(Boolean bool) {
this.expeditedShipping = bool;
}
public Boolean isOneDayShippingAvailable() {
return oneDayShippingAvailable;
}
public void setOneDayShippingAvailable(Boolean bool) {
this.oneDayShippingAvailable = bool;
}
public Integer getHandlingTime() {
return handlingTime;
}
public void setHandlingTime(Integer days) {
this.handlingTime = days;
}
}
I would expect the value of currencyId to be "USD" after deserializing, but I'm getting null. All the element values appear to deserialize properly. Does anyone have a suggestion on how to fix this?
Moreover, in a case such as the following instance:
<sellingStatus>
<currentPrice currencyId="USD">125.0</currentPrice>
<convertedCurrentPrice currencyId="USD">125.0</convertedCurrentPrice>
<bidCount>2</bidCount>
<sellingState>EndedWithSales</sellingState>
</sellingStatus>
Where there are two attributes named currencyId on two distinct elements, how can I go about deserializing these into separate fields? I have created a similar SellingStatus class but am unsure how to distinguish between the currencyId attributes.
Thank you!
Edit: Per suggestions I tried adding a custom ShippingServiceCost class to ShippingInfo as follows:
#Element(name = "shippingServiceCost", required = false)
private ShippingServiceCost shippingServiceCost;
Which in turn looks like this:
public class ShippingServiceCost {
#Element(name = "shippingServiceCost", required = false)
private BigDecimal shippingServiceCost;
#Attribute(name = "currencyId", required = false)
private String currencyId;
// getters and setters
}
But when I try to access both the shippingServiceCost field and the currencyId field, I get null in every instance (even though I know there is data). Any suggestions are greatly appreciated.
For the above code, SimpleXML expects the currencyId to be present as <shippingInfo currencyId="USD">.
So to solve it, you need to create another class called ShippingServiceCost which will contain the currencyId attribute and the BigDecimal
This will also solve your second query. You can do it by creating two classes CurrentPrice and ConvertedCurrentPrice which will contain the currencyId attribute.
The only working solution is creating a Converter class, see code below:
public class ShippingInfoConverter implements Converter<ShippingInfo> {
#Override
public ShippingInfo read(InputNode inputNode) throws Exception {
ShippingInfo shippingInfo = new ShippingInfo();
InputNode shippingServiceCostNode = inputNode.getNext("shippingServiceCost");
shippingInfo.setShippingServiceCost(new BigDecimal(shippingServiceCostNode.getValue()));
shippingInfo.setCurrencyId(shippingServiceCostNode.getAttribute("currencyId").getValue());
shippingInfo.setShippingType(inputNode.getNext("shippingType").getValue());
shippingInfo.setShipToLocations(inputNode.getNext("shipToLocations").getValue());
shippingInfo.setExpeditedShipping(Boolean.parseBoolean(inputNode.getNext("expeditedShipping").getValue()));
shippingInfo.setOneDayShippingAvailable(Boolean.parseBoolean(inputNode.getNext("oneDayShippingAvailable").getValue()));
shippingInfo.setHandlingTime(Integer.valueOf(inputNode.getNext("handlingTime").getValue()));
return shippingInfo;
}
#Override
public void write(OutputNode outputNode, ShippingInfo shippingInfo) throws Exception {
OutputNode shippingServiceCostNode = outputNode.getChild("shippingServiceCost");
shippingServiceCostNode.setValue(shippingInfo.getShippingServiceCost().toString());
shippingServiceCostNode.setAttribute("currencyId", shippingInfo.getCurrencyId());
outputNode.getChild("shippingType").setValue(shippingInfo.getShippingType());
outputNode.getChild("shipToLocations").setValue(shippingInfo.getShipToLocations());
outputNode.getChild("expeditedShipping").setValue(Boolean.toString(shippingInfo.isExpeditedShipping()));
outputNode.getChild("oneDayShippingAvailable").setValue(Boolean.toString(shippingInfo.isOneDayShippingAvailable()));
outputNode.getChild("handlingTime").setValue(Integer.toString(shippingInfo.getHandlingTime()));
}
}
Note how 'currencyId' is set, using the node's getAttribute method.
shippingInfo.setCurrencyId(shippingServiceCostNode.getAttribute("currencyId").getValue());
Also note how the element 'shippingServiceCost' gets the attribute
shippingServiceCostNode.setAttribute("currencyId", shippingInfo.getCurrencyId());
A few other things are need to get this working, starting with your POJO
#Root(name = "shippingInfo")
#Convert(ShippingInfoConverter.class)
public class ShippingInfo {
#Element(name = "shippingServiceCost", required = false)
private BigDecimal shippingServiceCost;
private String currencyId;
#Element(name = "shippingType", required = false)
private String shippingType;
#Element(name = "shipToLocations" ,required = false)
private String shipToLocations;
#Element(name = "expeditedShipping", required = false)
private Boolean expeditedShipping;
#Element(name = "oneDayShippingAvailable", required = false)
private Boolean oneDayShippingAvailable;
#Element(name = "handlingTime", required = false)
private Integer handlingTime;
// Getters & Setters
public BigDecimal getShippingServiceCost() {
return shippingServiceCost;
}
public void setShippingServiceCost(BigDecimal shippingServiceCost) {
this.shippingServiceCost = shippingServiceCost;
}
public String getCurrencyId() {
return currencyId;
}
public void setCurrencyId(String currencyId) {
this.currencyId = currencyId;
}
public String getShippingType() {
return shippingType;
}
public void setShippingType(String shippingType) {
this.shippingType = shippingType;
}
public String getShipToLocations() {
return shipToLocations;
}
public void setShipToLocations(String shipToLocations) {
this.shipToLocations = shipToLocations;
}
public Boolean isExpeditedShipping() {
return expeditedShipping;
}
public void setExpeditedShipping(Boolean bool) {
this.expeditedShipping = bool;
}
public Boolean isOneDayShippingAvailable() {
return oneDayShippingAvailable;
}
public void setOneDayShippingAvailable(Boolean bool) {
this.oneDayShippingAvailable = bool;
}
public Integer getHandlingTime() {
return handlingTime;
}
public void setHandlingTime(Integer days) {
this.handlingTime = days;
}
}
Adding the line below points SimpleXML to the converter class
#Convert(ShippingInfoConverter.class)
The other change is removing the #Attribute annotation.
One last thing required is that your driver class needs to have AnnotationStrategy enabled
when serialising and deserialing your objects.
Serializer serializer = new Persister(new AnnotationStrategy());
I'm working on a Spring Boot 2.0.5.RELEASE project.
I have a field in an Oracle database declared as CHAR(1) with a JPA converter as follows:
public class CharToBooleanConverter implements AttributeConverter<String, Boolean> {
#Override
public Boolean convertToDatabaseColumn(String s) {
return s.equalsIgnoreCase("t");
}
#Override
public String convertToEntityAttribute(Boolean aBoolean) {
if(aBoolean.equals(true)){
return "t";
} else {
return "f";
}
}
}
This converter is used in my StructureElement class twice:
#Entity
#Table(name = "OBS_STRUCTURE_ELEMENT2")
#SequenceGenerator(name = "structure_element_seq", sequenceName = "structure_element_seq", allocationSize = 1)
public class StructureElement {
#Id
#Column(name = "NO_ELEMENT")
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "structure_element_seq")
private long id;
#Column(name = "TAG")
private String tag;
#Column(name = "DESCRIPTION")
private String description;
#Column(name = "SUITE")
private int sequence;
#Column(name = "OPTIONNEL")
#Convert(converter = CharToBooleanConverter.class)
private boolean optional;
#Column(name = "REPETITIF")
#Convert(converter = CharToBooleanConverter.class)
private boolean repetitive;
#ManyToOne
#JoinColumn(name = "NOM_STRUCTURE_TYPE")
private Structure typeStructure;
#Embedded
private PersistenceSignature signature;
}
The problem is that when I try to send a Structure through a RestController I receive the following in console:
Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Could not set field value [t] value by reflection : [class be.solodoukhin.domain.StructureElement.optional] setter of be.solodoukhin.domain.StructureElement.optional; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Could not set field value [t] value by reflection : [class be.solodoukhin.domain.StructureElement.optional] setter of be.solodoukhin.domain.StructureElement.optional (through reference chain: be.solodoukhin.domain.Structure["elements"])]
With an 500 Internal Server Error response.
Here is my method:
#RestController
#RequestMapping("/structure")
public class StructuresController {
#GetMapping("/{name}")
public ResponseEntity<Structure> getOne(#PathVariable("name") String name)
{
LOGGER.info("Call to StructuresController.getOne with name = " + name);
Optional<Structure> found = this.structureRepository.findById(name);
if(found.isPresent()){
return ResponseEntity.ok(found.get());
}
else
{
return ResponseEntity.badRequest().body(null);
}
}
}
I've tried to use hibernate specific annotation #Type(type = "true_false"). It works but this produces an uppercase T or F in the database.
I've tried to write a JSON serializer that extends com.fasterxml.jackson.databind.ser.std.StdSerializer using this link without success.
Isn't your converter the wrong way round? Your convertToDatabaseColumn has it going from a String to a Boolean. Surely you want it going from Boolean to String. And the convertToEntityAttribute going from String to Boolean.
I suspect that the convert is returning a value of "t" or "f" which can't then be put into the boolean field in the entity.
I think it should be...
public class CharToBooleanConverter implements AttributeConverter<Boolean, String> {
#Override
public Boolean convertToEntityAttribute(String s) {
return s != null && s.equalsIgnoreCase("t");
}
#Override
public String convertToDatabaseColumn(Boolean aBoolean) {
return (aBoolean != null && aBoolean) ? "t" : "f";
}
}
the following class exists which consist from predefined UUID's that describe possible entires of the database.
public class Predefined {
#NotNull
#Size(min = 1, max = 25)
public UUID phone = UUID.fromString("47b58767-c0ad-43fe-8e87-c7dae489a4f0");
#NotNull
#Size(min = 1, max = 40)
public UUID company = UUID.fromString("f9a1e8f4-b8c0-41f2-a626-49c11da8d5c2");
}
Those values are received as a key pair value trough web service: and then they are put to a hashmap.
47b58767-c0ad-43fe-8e87-c7dae489a4f0 = +00112233445566778899
f9a1e8f4-b8c0-41f2-a626-49c11da8d5c2 = someVirtualCompnayName
When i receive an UUID that i know i am creating an instance of the Predefined class and then getting the annotations of the filed in the Predefined class i.e.:
Annotation[] annon = field.getDeclaredAnnotations();
Now I need to check those annotation agains the values that I got from the web services i.e. “+00112233445566778899” and “someVirtualCompnayName” at runtime
Is this possible?
I am especially interesting in example covering JSR 303.
Shortly why I have such construct:
The DAO , #Repository classes have different structure i.e.
contact
contact_attrbute
contact_attibute_type
where the databse “contact_attibute_type” is meant for “company” and “phone”. The second table i.e. “contact_attrbute” is meant for the actual values of “company” and “phone”.
Now I need a way to validate those values before I write them in hibernate, thus I am getting the “public UUID phone” and then trying to apply those constrains to the actual value I got from the user i.e. “+00112233445566778899”.
I'll post the complete code I have come up with to validate your test-case (including a simple executable demo):
Annotations:
package annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Target( {ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
public #interface NotNull
{
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Target( {ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
public #interface Size
{
int min() default 0;
int max();
}
The Predefined class:
public class Predefined
{
#NotNull
#Size(min = 1, max = 25)
public UUID phone;
#NotNull
#Size(min = 1, max = 40)
public UUID company;
public Predefined(UUID phone, UUID company)
{
this.phone = phone;
this.company = company;
}
}
The validator class which iterates through the declared fields and checks their annotation and field/value mappings:
public class PredefinedValidator
{
public boolean validate(Predefined predefined, Map<UUID, String> mappings)
{
if (predefined == null)
return false;
for (Field field :predefined.getClass().getDeclaredFields())
{
if (field.getType().equals(UUID.class))
{
try
{
Annotation[] annotations = field.getDeclaredAnnotations();
UUID uuid = (UUID)field.get(predefined);
if (!this.validateField(uuid, annotations, mappings))
return false;
}
catch (IllegalArgumentException | IllegalAccessException ex)
{
Logger.getLogger(PredefinedValidator.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
return true;
}
private boolean validateField(UUID field, Annotation[] annotations, Map<UUID, String> mapping)
{
boolean containsSize = false;
boolean containsNotNull = false;
int minSize = -1;
int maxSize = -1;
// fetch which annotations are available for the provided field
for (Annotation annotation : annotations)
{
if (annotation instanceof Size)
{
containsSize = true;
Size size = (Size)annotation;
minSize = size.min();
maxSize = size.max();
}
else if (annotation instanceof NotNull)
containsNotNull = true;
}
// check if the provided value is null and an annotatition for #NotNull
// is set
if (field == null && containsNotNull)
return false;
if (containsSize)
{
// get the value of the mapped UUID which we are going to validate
String value = mapping.get(field);
if (value == null && containsNotNull)
return false;
else if (value == null)
return true;
// check if the length of the value matches
if (value.length() <= minSize || value.length() >= maxSize)
return false;
}
// passed all tests
return true;
}
}
Last but not least a simple demo:
public static void main(String ... args)
{
Map<UUID, String> mappings = new HashMap<>();
mappings.put(UUID.fromString("47b58767-c0ad-43fe-8e87-c7dae489a4f0"), "+00112233445566778899");
mappings.put(UUID.fromString("f9a1e8f4-b8c0-41f2-a626-49c11da8d5c2"), "someVirtualCompnayName");
Predefined predefined = new Predefined(
UUID.fromString("47b58767-c0ad-43fe-8e87-c7dae489a4f0"),
UUID.fromString("f9a1e8f4-b8c0-41f2-a626-49c11da8d5c2"));
Predefined predefined2 = new Predefined(
UUID.randomUUID(),
UUID.fromString("f9a1e8f4-b8c0-41f2-a626-49c11da8d5c2"));
Predefined predefined3 = new Predefined(
null,
UUID.fromString("f9a1e8f4-b8c0-41f2-a626-49c11da8d5c2"));
PredefinedValidator validator = new PredefinedValidator();
System.out.println("predefined is valid: "+validator.validate(predefined, mappings));
System.out.println("predefined is valid: "+validator.validate(predefined2, mappings));
System.out.println("predefined is valid: "+validator.validate(predefined3, mappings));
mappings.put(UUID.fromString("f9a1e8f4-b8c0-41f2-a626-49c11da8d5c2"), "someVirtualCompnayNamesomeVirtualCompnayNamesomeVirtualCompnayNamesomeVirtualCompnayName");
System.out.println("predefined is valid: "+validator.validate(predefined, mappings));
}
HTH