Store ENUM value into database - java

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().

Related

Why does Jackson think I have that value [assigned] because of the function [isAssigned]

Jackson thinks that because of the method I have a value and need to assign it, although this is not the case. Same problem with [getDocumentView] if I use #JsonIgnore only on [isAssigned]. Although with methods in inherited class - no such problems.
I got an error
Could not write JSON: (was java.lang.NullPointerException); nested exception is com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: com.utg.acadsys.api.model.order.OrderDetailView["orderLineItems"]->org.hibernate.collection.internal.PersistentBag[0]->com.utg.acadsys.dao.model.order.OrderLineItem["assigned"])]
while sending that request in JSON format
{
"existingSupplier": true,
"systemInternalNumber": true,
"orderLineItems": [
{
"count": 1,
"vat": 20,
"text": "123",
"pricePerUnit": "1"
}
],
"subCompanyId": "39ee3eb9-605c-47b6-87e2-a20405708979",
"approvalLineId": "04fa4a2c-9bce-11e8-aa4e-f23c910b0044",
"email": null,
"supplierId": "d83c7f3f-cd49-11e8-bf92-f23c910b0044",
"deliveryDate": "2022-05-25",
"reason": "12313",
"note": "123123"
}
OrderLineItem.java
#Entity
#Getter
#Setter
public class OrderLineItem extends LineItem {
#ManyToOne(optional = false)
#JsonIgnore
private Order order;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "orderLineItem")
#JsonIgnore
private List<OrderLineAssignment> orderLineAssignments;
public BigDecimal getAmountForCheck() {
if (order.getCompany().isUseTotalPriceForCheck()) {
return getTotalPrice();
}
return getPriceWithoutVat();
}
public boolean isAssigned() { // <---here
return !orderLineAssignments.isEmpty();
}
public List<AssignedDocumentView> getAssignedDocumentsView() {
if (!orderLineAssignments.isEmpty()) {
List<AssignedDocumentView> assignedDocuments = new ArrayList<>();
orderLineAssignments.forEach(assignment -> assignedDocuments.add(new AssignedDocumentView(assignment.getAssignedDocument().getId(), assignment.getAssignedDocument().getInvoiceNumber(), assignment.getAdditionDate())));
return assignedDocuments;
}
return null;
};
}
and extender class LineItem.java
#MappedSuperclass
#Getter
#Setter
public class LineItem extends EntityWithUUID {
#NotNull
private String text;
#NotNull
private BigDecimal count;
#NotNull
private BigDecimal pricePerUnit;
private BigDecimal vat;
public BigDecimal getTotalPrice() {
if (count.compareTo(BigDecimal.ZERO) > 0) {
return pricePerUnit.add(getVatAmount()).multiply(count);
}
return pricePerUnit.add(getVatAmount());
}
public BigDecimal getVatAmount() {
if (vat != null) {
return pricePerUnit.multiply(vat.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_EVEN));
}
return BigDecimal.ZERO;
}
public BigDecimal getPriceWithoutVat() {
if (count.compareTo(BigDecimal.ZERO) > 0) {
return pricePerUnit.multiply(count);
}
return pricePerUnit;
}
public boolean isVat() {
return vat != null && vat.compareTo(BigDecimal.ZERO) != 0;
}
}
If I use #JsonIgnore - everything works properly, but I need it in GET requests.
If you need anything else, let me know and I'll attach it.
Yes, I managed to fix this by adding #JsonIgnore above the method. But I can no longer get the value of this method in GET requests as before with vatAmount and priceWithoutVat. And I'm wondering why this is, and how I can fix it. I've already googled options through #JsonIgnoreProperties to do a ban for POST requests and open for GET. (#JsonIgnoreProperties(allowSetters = false)) But it doesn't work as I have one method, not a value.
delomboked OrderLineItem.java
package com.utg.acadsys.dao.model.order;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.utg.acadsys.dao.model.LineItem;
import javax.persistence.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
#Entity
public class OrderLineItem extends LineItem {
#ManyToOne(optional = false)
#JsonIgnore
private Order order;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "orderLineItem")
#JsonIgnore
private List<OrderLineAssignment> orderLineAssignments;
public BigDecimal getAmountForCheck() {
if (order.getCompany().isUseTotalPriceForCheck()) {
return getTotalPrice();
}
return getPriceWithoutVat();
}
public boolean isAssigned() {
return !orderLineAssignments.isEmpty();
}
public List<AssignedDocumentView> getAssignedDocumentsView() {
if (!orderLineAssignments.isEmpty()) {
List<AssignedDocumentView> assignedDocuments = new ArrayList<>();
orderLineAssignments.forEach(assignment -> assignedDocuments.add(new AssignedDocumentView(assignment.getAssignedDocument().getId(), assignment.getAssignedDocument().getInvoiceNumber(), assignment.getAdditionDate())));
return assignedDocuments;
}
return null;
}
#SuppressWarnings("all")
public Order getOrder() {
return this.order;
}
#SuppressWarnings("all")
public List<OrderLineAssignment> getOrderLineAssignments() {
return this.orderLineAssignments;
}
#JsonIgnore
#SuppressWarnings("all")
public void setOrder(final Order order) {
this.order = order;
}
#JsonIgnore
#SuppressWarnings("all")
public void setOrderLineAssignments(final List<OrderLineAssignment> orderLineAssignments) {
this.orderLineAssignments = orderLineAssignments;
}
}

Simple xml returns null value for attribute field

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());

How to get around HTTP Error 415 - Unsupported Media Type

I'm using Restet and I wanted to know if its possible if a ServerResource entity type is set, for example for this type of entity:
#XStreamAlias("role")
#ApiModel
public class Role {
private String entityId;
private String name;
#ApiModelProperty(required = false, value = "")
private List<String> aclRead;
#ApiModelProperty(required = false, value = "")
private List<String> acLWrite;
#ApiModelProperty(required = false, value = "")
private Boolean publicRead;
#ApiModelProperty(required = false, value = "")
private Boolean publicWrite;
public String getEntityId() {
return entityId;
}
public void setEntityId(String entityId) {
this.entityId = entityId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getAclRead() {
return aclRead;
}
public void setAclRead(List<String> aclRead) {
this.aclRead = aclRead;
}
public List<String> getAcLWrite() {
return acLWrite;
}
public void setAcLWrite(List<String> acLWrite) {
this.acLWrite = acLWrite;
}
public Boolean getPublicRead() {
return publicRead;
}
public void setPublicRead(Boolean publicRead) {
this.publicRead = publicRead;
}
public Boolean getPublicWrite() {
return publicWrite;
}
public void setPublicWrite(Boolean publicWrite) {
this.publicWrite = publicWrite;
}
}
How can we be able to POST without the need of passing all the fields, for this example we only wanted to pass the "name" from the Client Request, but doing that throws 415 - Unsupported Media Type
In our client we only do pass this JSON
{
"role" : {
"name" : "AdminRole"
}
}
For the Get response of the Resource, the return type is Role also so the client will get all the fields, entityId, name, aclRead, aclWrite, publicRead and publicWrite through the Restlet marshalling.
The problem we have is that we cannot POST.
In the post request, the json data should only consist of the the entity parameters without the entity name as root key. So, your post request should look like this:
{
"name" : "AdminRole"
}

Jersey not working for POST, PUT

I have a problem which seems to have a simple solution but I haven't been able to solve for months:
I'm trying to POST/PUT JSONs, and having the server recognize and deserialize them as their corresponding classes. But the server is not able to parse the entity, and returns a 500 error code.
This has to do with the fact that the generated code expects an interface (Apple) instead of a 'normal' Java class (AppleImpl).
I know this because the methods work if I change the interface (Apple) with the implementation (AppleImpl) in the endpoint, since the server detects the type and deserializes it correctly, but it is tedious and error-prone to manually change the interfaces to implementations every time that a change is made in the RAML.
In the Java backend, I'm using an entity, interface and endpoint, all of which were generated using raml-to-jax-rs v3.0.2. I'm using Maven with Jersey (jersey-bom) v2.27.
To summarize, I would like to know how to successfully deserialize an interface in Jersey/JAX-RS. Am I missing an annotation, are my Maven package versions wrong, or is it something else?
I would very much appreciate if someone could help me with this, and I will provide more information if necessary. Below is some sample generated code so that the problem is clearer.
Sample generated code:
(1/3) Sample generated interface Apple:
#JsonDeserialize(
as = AppleImpl.class
)
public interface Apple {
#JsonAnyGetter
Map<String, Object> getAdditionalProperties();
#JsonAnySetter
void setAdditionalProperties(String key, Object value);
#JsonProperty("name")
String getName();
#JsonProperty("name")
void setName(String name);
#JsonProperty("values")
List<String> getValues();
#JsonProperty("values")
void setValues(List<String> values);
#JsonProperty("defaultValue")
String getDefaultValue();
#JsonProperty("defaultValue")
void setDefaultValue(String defaultValue);
}
(2/3) Sample generated class AppleImpl:
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonPropertyOrder({
"name",
"values",
"defaultValue"
})
public class AppleImpl implements Apple {
#NotNull
#JsonProperty("name")
private String name;
#JsonProperty("values")
#NotNull
private List<String> values;
#JsonProperty("defaultValue")
#NotNull
private String defaultValue;
#JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
#JsonProperty("name")
public String getName() {
return this.name;
}
#JsonProperty("name")
public void setName(String name) {
this.name = name;
}
#JsonProperty("values")
public List<String> getValues() {
return this.values;
}
#JsonProperty("values")
public void setValues(List<String> values) {
this.values = values;
}
#JsonProperty("defaultValue")
public String getDefaultValue() {
return this.defaultValue;
}
#JsonProperty("defaultValue")
public void setDefaultValue(String defaultValue) {
this.defaultValue = defaultValue;
}
#JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
return additionalProperties;
}
#JsonAnySetter
public void setAdditionalProperties(String key, Object value) {
this.additionalProperties.put(key, value);
}
#Override
public boolean equals(Object o) {
if (o == null) return false;
if (this == o) return true;
if (getClass() != o.getClass()) return false;
AppleImpl other = (AppleImpl) o;
return java.util.Objects.equals(this.name, other.name) && java.util.Objects.equals(this.values, other.values) && java.util.Objects.equals(this.defaultValue, other.defaultValue) && java.util.Objects.equals(this.additionalProperties, other.additionalProperties);
}
#Override
public int hashCode() {
return Objects.hash(name,values,defaultValue,additionalProperties);
}
}
(3/3) Sample generated POST endpoint:
#Override
public PostApplesResponse postApples(String xSessionToken, Apple bean) {
try {
MUser user = AuthUtils.authenticates(xSessionToken);
ModelMapperApple.get().validateBean(bean);
MApple entity = ModelMapperApple.get().b2e(bean);
MApple entityOut = AppleService.getInstance().post(user, entity);
AppleResponse beanOut = ModelMapperApple.get().e2b(entityOut);
return PostApplesResponse.respond201WithApplicationJson(beanOut);
} catch (BadRequestException e) {
CRUDUtils.logException(e);
return PostApplesResponse.respond400WithApplicationJson(ErrorMessageUtils.build(e));
} catch (UnauthorizedException e) {
CRUDUtils.logException(e);
return PostApplesResponse.respond401();
} catch (ConflictException e) {
CRUDUtils.logException(e);
return PostApplesResponse.respond409();
} catch (Exception e) {
CRUDUtils.logException(e);
return PostApplesResponse.respond500();
}
}

JSR 303 Validation, If one field equals "something", then these other fields should not be null

I'm looking to do a little custom validation with JSR-303 javax.validation.
I have a field. And If a certain value is entered into this field I want to require that a few other fields are not null.
I'm trying to figure this out. Not sure exactly what I would call this to help find an explanation.
Any help would be appreciated. I am pretty new to this.
At the moment I'm thinking a Custom Constraint. But I'm not sure how to test the value of the dependent field from within the annotation. Basically I'm not sure how to access the panel object from the annotation.
public class StatusValidator implements ConstraintValidator<NotNull, String> {
#Override
public void initialize(NotNull constraintAnnotation) {}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if ("Canceled".equals(panel.status.getValue())) {
if (value != null) {
return true;
}
} else {
return false;
}
}
}
It's the panel.status.getValue(); giving me trouble.. not sure how to accomplish this.
Define method that must validate to true and put the #AssertTrue annotation on the top of it:
#AssertTrue
private boolean isOk() {
return someField != something || otherField != null;
}
The method must start with 'is'.
In this case I suggest to write a custom validator, which will validate at class level (to allow us get access to object's fields) that one field is required only if another field has particular value. Note that you should write generic validator which gets 2 field names and work with only these 2 fields. To require more than one field you should add this validator for each field.
Use the following code as an idea (I've not test it).
Validator interface
/**
* Validates that field {#code dependFieldName} is not null if
* field {#code fieldName} has value {#code fieldValue}.
**/
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Repeatable(NotNullIfAnotherFieldHasValue.List.class) // only with hibernate-validator >= 6.x
#Constraint(validatedBy = NotNullIfAnotherFieldHasValueValidator.class)
#Documented
public #interface NotNullIfAnotherFieldHasValue {
String fieldName();
String fieldValue();
String dependFieldName();
String message() default "{NotNullIfAnotherFieldHasValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Documented
#interface List {
NotNullIfAnotherFieldHasValue[] value();
}
}
Validator implementation
/**
* Implementation of {#link NotNullIfAnotherFieldHasValue} validator.
**/
public class NotNullIfAnotherFieldHasValueValidator
implements ConstraintValidator<NotNullIfAnotherFieldHasValue, Object> {
private String fieldName;
private String expectedFieldValue;
private String dependFieldName;
#Override
public void initialize(NotNullIfAnotherFieldHasValue annotation) {
fieldName = annotation.fieldName();
expectedFieldValue = annotation.fieldValue();
dependFieldName = annotation.dependFieldName();
}
#Override
public boolean isValid(Object value, ConstraintValidatorContext ctx) {
if (value == null) {
return true;
}
try {
String fieldValue = BeanUtils.getProperty(value, fieldName);
String dependFieldValue = BeanUtils.getProperty(value, dependFieldName);
if (expectedFieldValue.equals(fieldValue) && dependFieldValue == null) {
ctx.disableDefaultConstraintViolation();
ctx.buildConstraintViolationWithTemplate(ctx.getDefaultConstraintMessageTemplate())
.addNode(dependFieldName)
.addConstraintViolation();
return false;
}
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) {
throw new RuntimeException(ex);
}
return true;
}
}
Validator usage example (hibernate-validator >= 6 with Java 8+)
#NotNullIfAnotherFieldHasValue(
fieldName = "status",
fieldValue = "Canceled",
dependFieldName = "fieldOne")
#NotNullIfAnotherFieldHasValue(
fieldName = "status",
fieldValue = "Canceled",
dependFieldName = "fieldTwo")
public class SampleBean {
private String status;
private String fieldOne;
private String fieldTwo;
// getters and setters omitted
}
Validator usage example (hibernate-validator < 6; the old example)
#NotNullIfAnotherFieldHasValue.List({
#NotNullIfAnotherFieldHasValue(
fieldName = "status",
fieldValue = "Canceled",
dependFieldName = "fieldOne"),
#NotNullIfAnotherFieldHasValue(
fieldName = "status",
fieldValue = "Canceled",
dependFieldName = "fieldTwo")
})
public class SampleBean {
private String status;
private String fieldOne;
private String fieldTwo;
// getters and setters omitted
}
Note that validator implementation uses BeanUtils class from commons-beanutils library but you could also use BeanWrapperImpl from Spring Framework.
See also this great answer: Cross field validation with Hibernate Validator (JSR 303)
You should make use of custom DefaultGroupSequenceProvider<T>:
ConditionalValidation.java
// Marker interface
public interface ConditionalValidation {}
MyCustomFormSequenceProvider.java
public class MyCustomFormSequenceProvider
implements DefaultGroupSequenceProvider<MyCustomForm> {
#Override
public List<Class<?>> getValidationGroups(MyCustomForm myCustomForm) {
List<Class<?>> sequence = new ArrayList<>();
// Apply all validation rules from ConditionalValidation group
// only if someField has given value
if ("some value".equals(myCustomForm.getSomeField())) {
sequence.add(ConditionalValidation.class);
}
// Apply all validation rules from default group
sequence.add(MyCustomForm.class);
return sequence;
}
}
MyCustomForm.java
#GroupSequenceProvider(MyCustomFormSequenceProvider.class)
public class MyCustomForm {
private String someField;
#NotEmpty(groups = ConditionalValidation.class)
private String fieldTwo;
#NotEmpty(groups = ConditionalValidation.class)
private String fieldThree;
#NotEmpty
private String fieldAlwaysValidated;
// getters, setters omitted
}
See also related question on this topic.
Here's my take on it, tried to keep it as simple as possible.
The interface:
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = OneOfValidator.class)
#Documented
public #interface OneOf {
String message() default "{one.of.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] value();
}
Validation implementation:
public class OneOfValidator implements ConstraintValidator<OneOf, Object> {
private String[] fields;
#Override
public void initialize(OneOf annotation) {
this.fields = annotation.value();
}
#Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(value);
int matches = countNumberOfMatches(wrapper);
if (matches > 1) {
setValidationErrorMessage(context, "one.of.too.many.matches.message");
return false;
} else if (matches == 0) {
setValidationErrorMessage(context, "one.of.no.matches.message");
return false;
}
return true;
}
private int countNumberOfMatches(BeanWrapper wrapper) {
int matches = 0;
for (String field : fields) {
Object value = wrapper.getPropertyValue(field);
boolean isPresent = detectOptionalValue(value);
if (value != null && isPresent) {
matches++;
}
}
return matches;
}
private boolean detectOptionalValue(Object value) {
if (value instanceof Optional) {
return ((Optional) value).isPresent();
}
return true;
}
private void setValidationErrorMessage(ConstraintValidatorContext context, String template) {
context.disableDefaultConstraintViolation();
context
.buildConstraintViolationWithTemplate("{" + template + "}")
.addConstraintViolation();
}
}
Usage:
#OneOf({"stateType", "modeType"})
public class OneOfValidatorTestClass {
private StateType stateType;
private ModeType modeType;
}
Messages:
one.of.too.many.matches.message=Only one of the following fields can be specified: {value}
one.of.no.matches.message=Exactly one of the following fields must be specified: {value}
A different approach would be to create a (protected) getter that returns an object containing all dependent fields. Example:
public class MyBean {
protected String status;
protected String name;
#StatusAndSomethingValidator
protected StatusAndSomething getStatusAndName() {
return new StatusAndSomething(status,name);
}
}
StatusAndSomethingValidator can now access StatusAndSomething.status and StatusAndSomething.something and make a dependent check.
Sample below:
package io.quee.sample.javax;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validator;
import javax.validation.constraints.Pattern;
import java.util.Set;
/**
* Created By [**Ibrahim Al-Tamimi **](https://www.linkedin.com/in/iloom/)
* Created At **Wednesday **23**, September 2020**
*/
#SpringBootApplication
public class SampleJavaXValidation implements CommandLineRunner {
private final Validator validator;
public SampleJavaXValidation(Validator validator) {
this.validator = validator;
}
public static void main(String[] args) {
SpringApplication.run(SampleJavaXValidation.class, args);
}
#Override
public void run(String... args) throws Exception {
Set<ConstraintViolation<SampleDataCls>> validate = validator.validate(new SampleDataCls(SampleTypes.TYPE_A, null, null));
System.out.println(validate);
}
public enum SampleTypes {
TYPE_A,
TYPE_B;
}
#Valid
public static class SampleDataCls {
private final SampleTypes type;
private final String valueA;
private final String valueB;
public SampleDataCls(SampleTypes type, String valueA, String valueB) {
this.type = type;
this.valueA = valueA;
this.valueB = valueB;
}
public SampleTypes getType() {
return type;
}
public String getValueA() {
return valueA;
}
public String getValueB() {
return valueB;
}
#Pattern(regexp = "TRUE")
public String getConditionalValueA() {
if (type.equals(SampleTypes.TYPE_A)) {
return valueA != null ? "TRUE" : "";
}
return "TRUE";
}
#Pattern(regexp = "TRUE")
public String getConditionalValueB() {
if (type.equals(SampleTypes.TYPE_B)) {
return valueB != null ? "TRUE" : "";
}
return "TRUE";
}
}
}

Categories