Peter Penzov
Peter Penzov

Reputation: 1680

java.lang.IllegalArgumentException: No enum BusinessCustomersStatus.4d29e059cf

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

Answers (2)

jccampanero
jccampanero

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 Strings 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

Martin Zeitler
Martin Zeitler

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

Related Questions