Reputation: 1680
I want to create a search Specification using ENUM values:
Search ENUM:
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;
}
}
Search DTO:
@Getter
@Setter
public class BusinessCustomersSearchParams {
private String title;
private List<BusinessCustomersStatus> status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
Search Specification:
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("title")), "%" + params.getTitle().toLowerCase() + "%"));
}
Optional<BusinessCustomersStatus> optStatus = EnumSet.allOf(BusinessCustomersStatus.class)
.stream()
.filter(e -> e.name().equals(params.getStatus()))
.findAny();
if(optStatus.isPresent()){
final List<BusinessCustomersStatus> statuses = params.getStatus();
if (statuses != null && !statuses.isEmpty()){
predicates.add(root.get("status").in(statuses));
}
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
};
return businessCustomersService.findAll(spec, pageable).map(businessCustomersMapper::toFullDTO);
}
Entity:
@Entity
@Table(name = "business_customers")
public class BusinessCustomers implements Serializable {
..........
@Enumerated(EnumType.STRING)
@Column(name = "status", length = 20)
private BusinessCustomersStatus status;
......
}
But I get this error:
java.lang.IllegalArgumentException: No enum constant org.service.businesscustomers.BusinessCustomersStatus.4d29e059cf] with root cause
java.lang.IllegalArgumentException: No enum constant org.service.businesscustomers.BusinessCustomersStatus.4d29e059cf
at java.base/java.lang.Enum.valueOf(Enum.java:273)
Do you know how I can fix this issue?
POC example: https://github.com/rcbandit111/Search_specification_POC
Upvotes: 1
Views: 955
Reputation: 53421
I think you are mixing several of the options exposed in my answer to your original question.
Assuming that you are defining status
as a List
as in the example you provided:
@Getter
@Setter
public class BusinessCustomersSearchParams {
private String title;
private List<BusinessCustomersStatus> status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
You can try using in your Specification
either an in
clause:
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("title")), "%" + params.getTitle().toLowerCase() + "%"));
}
// According to your comments, please, see this example
// about how to wrap the List returned from your form
final List<BusinessCustomersStatus> statuses = Optional.ofNullable(params.getStatus()).orElse(Collections.emptyList());
if (statuses != null && !statuses.isEmpty()){
predicates.add(root.get("status").in(statuses));
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
};
return businessCustomersService.findAll(spec, pageable).map(businessCustomersMapper::toFullDTO);
}
Or you can iterate over the status
collection in order to verify each value and build an or
predicate with the required filter criteria:
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("title")), "%" + params.getTitle().toLowerCase() + "%"));
}
final List<BusinessCustomersStatus> statuses = params.getStatus();
if (statuses != null && !statuses.isEmpty()){
List<Predicate> statusPredicates = new ArrayList<Predicate>();
EnumSet<BusinessCustomersStatus> businessCustomersStatusEnumSet = EnumSet.allOf(BusinessCustomersStatus.class);
statuses.forEach(status -> {
Optional<BusinessCustomersStatus> optStatus = businessCustomersStatusEnumSet
.stream()
.filter(e -> e.equals(status))
.findAny();
if(optStatus.isPresent()){
statusPredicates.add(cb.equal(root.get("status"), cb.literal(status)));
}
});
predicates.add(
cb.or(statusPredicates.toArray(new Predicate[statusPredicates.size()]))
);
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
};
return businessCustomersService.findAll(spec, pageable).map(businessCustomersMapper::toFullDTO);
}
In fact, as you are already using the enumeration and not String
s I think the forEach
loop presented in the previous code can be simplified as follows:
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("title")), "%" + params.getTitle().toLowerCase() + "%"));
}
final List<BusinessCustomersStatus> statuses = params.getStatus();
if (statuses != null && !statuses.isEmpty()){
List<Predicate> statusPredicates = new ArrayList<Predicate>();
EnumSet<BusinessCustomersStatus> businessCustomersStatusEnumSet = EnumSet.allOf(BusinessCustomersStatus.class);
statuses.forEach(status -> {
if(businessCustomersStatusEnumSet.contains(status)){
statusPredicates.add(cb.equal(root.get("status"), cb.literal(status)));
}
});
predicates.add(
cb.or(statusPredicates.toArray(new Predicate[statusPredicates.size()]))
);
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
};
return businessCustomersService.findAll(spec, pageable).map(businessCustomersMapper::toFullDTO);
}
Or even, with a different approach:
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("title")), "%" + params.getTitle().toLowerCase() + "%"));
}
final List<BusinessCustomersStatus> statuses = params.getStatus();
if (statuses != null && !statuses.isEmpty()){
EnumSet<BusinessCustomersStatus> businessCustomersStatusEnumSet = EnumSet.allOf(BusinessCustomersStatus.class);
List<Predicate> statusPredicates = businessCustomersStatusEnumSet.stream()
.filter(status -> statuses.contains(status))
.map(status -> cb.equal(root.get("status"), cb.literal(status)))
.collect(Collectors.toList())
;
if (statusPredicates != null && !statusPredicates.isEmpty()) {
predicates.add(
cb.or(statusPredicates.toArray(new Predicate[statusPredicates.size()]))
);
}
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
};
return businessCustomersService.findAll(spec, pageable).map(businessCustomersMapper::toFullDTO);
}
The first of the provided approaches is the one that should be used in a typical use case, although the last can probably solve the problem by removing the incorrect values, but the actual question remains: why you are receiving the incorrect enum value in first place? Please, review your database records seeking for incorrect ones, and be sure - sorry for saying, but sometimes it happens, I faced myself - that you use everywhere the annotation value, A
, for instance, and not the value associated with it, active
, continuing with the example; they are the values that should be stored in the database, those to be submitted by your frontend, etc.
If you need or prefer to use the status
literal associated with your enumeration, you can follow the following approach.
First, modify your enum and provide a helper method in the conversion process:
public enum BusinessCustomersStatus {
ACTIVE("active"),
ONBOARDING("onboarding"),
NOT_VERIFIED("not_verified"),
VERIFIED("verified"),
SUSPENDED("suspended"),
INACTIVE("inactive");
private String status;
BusinessCustomersStatus(String status)
{
this.status = status;
}
public static BusinessCustomersStatus fromStatus(String status) {
switch (status) {
case "active": {
return ACTIVE;
}
case "onboarding": {
return ONBOARDING;
}
case "not_verified": {
return NOT_VERIFIED;
}
case "verified": {
return VERIFIED;
}
case "suspended": {
return SUSPENDED;
}
case "inactive": {
return INACTIVE;
}
default: {
throw new UnsupportedOperationException(
String.format("Unkhown status: '%s'", status)
);
}
}
}
}
Then, modify BusinessCustomersSearchParams
to use String
instead of the enumeration for the status
field:
@Getter
@Setter
public class BusinessCustomersSearchParams {
private String title;
private List<String> status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
Finally, modify your Specification to handle the conversion process. For instance:
@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);
}
You can use the fromStatus
method in the insertion process as well.
Having said that, I don't know if it what you need, but I think here you can use an AttributeConverter
. Consider for instance:
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 );
}
}
You need to provide the additional getStatus
method in your enumeration:
package org.merchant.database.service.businesscustomers;
public enum BusinessCustomersStatus {
ACTIVE("active"),
ONBOARDING("onboarding"),
NOT_VERIFIED("not_verified"),
VERIFIED("verified"),
SUSPENDED("suspended"),
INACTIVE("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 ACTIVE;
}
case "onboarding": {
return ONBOARDING;
}
case "not_verified": {
return NOT_VERIFIED;
}
case "verified": {
return VERIFIED;
}
case "suspended": {
return SUSPENDED;
}
case "inactive": {
return INACTIVE;
}
default: {
throw new UnsupportedOperationException(
String.format("Unkhown status: '%s'", status)
);
}
}
}
}
Now, your BusinessCustomers
entity should be modified to something like:
@Entity
@Table(name = "business_customers")
public class BusinessCustomers implements Serializable {
..........
@Convert( converter = BusinessCustomersStatusAttributeConverter.class )
private BusinessCustomersStatus status;
......
}
Please, be aware that if you implement the AttributeConverter
approach in the database the values stored will be as well the literal status
associated with the enumeration, not the enumeration name
itself. Please, keep the @Enumerated
approach in BusinessCustomers
if you prefer to store the enumeration values. If you used the AttributeConverter
approach for testing, please, remember to update the values of the status
column to those corresponding to the enumeration.
Finally, regarding your last comment, you are receiving "status": "ACTIVE"
: this is because your Mapstruct Mapper
is providing the name
of the enum as the value for the status
field in BusinessCustomersFullDTO
. To handle the appropriate conversion, please, try the following:
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();
}
}
In the code we are using @Named
and qualifiedByName
to provide the custom conversion. Please, see the relevant documentation.
Upvotes: 1
Reputation: 76789
You should define that enum
with constant values, then the values won't be instanceof
something (maybe it doesn't like the enumeration for some other reason, but it still reads no enum constant
).
public static final int STATUS_ACTIVE = 0;
public static final int STATUS_ONBOARDING = 1;
public static final int STATUS_NOT_VERIFIED = 2;
public static final int STATUS_VERIFIED = 3;
public static final int STATUS_SUSPENDED = 4;
public static final int STATUS_INACTIVE = 5;
public enum BusinessCustomersStatus {
STATUS_ACTIVE,
STATUS_ONBOARDING,
STATUS_NOT_VERIFIED,
STATUS_VERIFIED,
STATUS_SUSPENDED,
STATUS_INACTIVE
}
And @Enumerated
should then be EnumType.ORDINAL
:
@Enumerated(EnumType.ORDINAL)
@Column(name = "status")
Upvotes: 1