4

I have this Enum which I would like to use to set different statuses:

@Enumerated(EnumType.STRING)
@Column(name = "status", length = 20)
private OnboardingTaskStatus status; 

public enum OnboardingTaskStatus {
    NEW,
    IN_PROGRESS,
    DISABLED,
}

When I try to builds search specification I get this:

public Page<OnboardingTaskDto> findOnboardingTasks(OnboardingTaskSearchParams params, Pageable pageable){
        Specification<OnboardingTasks> spec = (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();
            if (params.getStatus() != null) {
                predicates.add(cb.equal(root.get("status"), params.getStatus()));
            }
            return cb.and(predicates.toArray(new Predicate[predicates.size()]));
        };
        return onboardingTasksService.findAll(spec, pageable).map(onboardingMapper::taskToTaskDTO);
    }

@Getter
@Setter
public class OnboardingTaskSearchParams {
    private String title;
    private String status;

    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

I tried to update the status to this filed:

predicates.add(cb.equal(root.get("status"), OnboardingTaskStatus.valueOf(params.getStatus())));

But I get error:

OnboardingTaskStatus.2769df0841; nested exception is java.lang.IllegalArgumentException: No enum constant OnboardingTaskStatus.2769df0841] with root cause java.lang.IllegalArgumentException: No enum constant OnboardingTaskStatus.2769df0841

Do you know what is the proper way to implement the Enum search without throwing an error if the value is not found?

catch23
  • 17,519
  • 42
  • 144
  • 217
Peter Penzov
  • 1,126
  • 134
  • 430
  • 808

7 Answers7

1

You can introduce static method inside your Enum to check a given string is a correct Enum value. Something like this,

enum OnBoardingTaskStatus {
    NEW,
    IN_PROGRESS,
    DISABLED;

    public static Optional<OnBoardingTaskStatus> check(String val) {
        try { return Optional.of(OnBoardingTaskStatus.valueOf(val)); }
        catch (Exception e) {/* do nothing */}
        return Optional.empty();
    }
}

Note here I have returned an Optional

Then you can change findOnboardingTasks method as follows,

public Page<OnboardingTaskDto> findOnboardingTasks(OnboardingTaskSearchParams params, Pageable pageable) {
    Specification<OnboardingTasks> spec = (root, query, cb) -> {
        List<Predicate> predicates = new ArrayList<>();
        predicates.add(cb.notEqual(cb.literal(1), 1));

        OnBoardingTaskStatus.check(params.getStatus())
                .ifPresent(e -> predicates.add(cb.equal(root.get("status"), e)));

        return cb.or(predicates.toArray(new Predicate[]{}));
    };
    return onboardingTasksService.findAll(spec, pageable).map(onboardingMapper::taskToTaskDTO);
}
ray
  • 1,512
  • 4
  • 11
1

Your java.lang.IllegalArgumentException: No enum constant [OnboardingTaskStatus.2769df0841] let me assume that params.getStatus() returns a String which is the result of a toString() operation on a class named OnboardingTaskStatus which does not have an implemented toString and is not the enum OnboardingTaskStatus.

You should check exactly what you get if you call getStatus maybe add a log statement or System.err.println for that.

S.Roeper
  • 309
  • 2
  • 6
1

The error reported indicates that you are trying converting the String OnboardingTaskStatus.2769df0841 to a OnboardingTaskStatus enumeration value which is not defined.

Please, don't look for workarounds on how to deal with the wrong value, try to find out why you got the wrong value in the first place instead. Test your Specification in isolation, with correct enum values, and see if it works. If it does - I think it will probably do - review the code you are using to provide values for the OnboardingTaskSearchParams form, and double check the value of the status parameter, perhaps you are iterating over the enumeration in a wrong way, etc. Probably not, but be sure as well that your status column doesn't contain incorrect values - inserted manually, in a previous version of the code...

In addition, to mitigate the problem, I would try to directly define your enum in the search criteria, Spring will handle the conversion between the String and the enum value automatically.

I mean, instead of defining status as String in OnboardingTaskSearchParams, just define the field as an enum:

@Getter
@Setter
public class OnboardingTaskSearchParams {

    private String title;

    private OnboardingTaskStatus status;

    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;
}

Or maybe better:

@Getter
@Setter
public class OnboardingTaskSearchParams {

    private String title;

    private List<OnboardingTaskStatus> statuses;

    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;
}

And use the enum as such in your Specification:

public Page<OnboardingTaskDto> findOnboardingTasks(OnboardingTaskSearchParams params, Pageable pageable){
    Specification<OnboardingTasks> spec = (root, query, cb) -> {
        List<Predicate> predicates = new ArrayList<>();
        if (params.getStatus() != null) {
            predicates.add(cb.equal(root.get("status"), params.getStatus()));
        }
        return cb.and(predicates.toArray(new Predicate[predicates.size()]));
    };
    return onboardingTasksService.findAll(spec, pageable).map(onboardingMapper::taskToTaskDTO);
}

Or, if you use multiple values as search criteria:

public Page<OnboardingTaskDto> findOnboardingTasks(OnboardingTaskSearchParams params, Pageable pageable){
    Specification<OnboardingTasks> spec = (root, query, cb) -> {
        List<Predicate> predicates = new ArrayList<>();
        final List<OnboardingTaskStatus> statuses = params.getStatuses();
        if (statuses != null && ! statuses.isEmpty()){
            predicates.add(root.get("status").in(statuses));
        }
        return cb.and(predicates.toArray(new Predicate[predicates.size()]));
    };
    return onboardingTasksService.findAll(spec, pageable).map(onboardingMapper::taskToTaskDTO);
}

You must be sure that in your frontend you provide the right values in your form for the possible OnboardingTaskStatus enum ones, NEW, IN_PROGRESS, and DISABLED.

It is important to note that Spring will immediately complaint at the Web layer if your form is submitted and if it finds any incorrect enumeration value before reaching your service or database related code.

If you actually need to check if the enumeration value exists and still use String for representing status in your search criteria, you have multiple options.

You can for example use the method isValidEnum in the EnumUtils class from the commons-lang library:

if(EnumUtils.isValidEnum(OnboardingTaskStatus.class, params.getStatus())){
  predicates.add(
    cb.equal(
      root.get("status"),
      OnboardingTaskStatus.valueOf(params.getStatus())
    )
  );
}

You can use the getIfPresent method from Guava's Enums class:

Optional<OnboardingTaskStatus> optStatus = Enums.getIfPresent(OnboardingTaskStatus.class, params.getStatus());
if(optStatus.isPresent()){
  predicates.add(
    cb.equal(
      root.get("status"),
      OnboardingTaskStatus.valueOf(optStatus.get())
    )
  );
}

This other SO question provides several alternatives from Jon Skeet and others.

With Java 8, you have several options as well. In addition to the ones provided in other answers, you can try for example:

Optional<OnboardingTaskStatus> optStatus = EnumSet.allOf(OnboardingTaskStatus.class)
  .stream()
  .filter(e -> e.name().equals(params.getStatus()))
  .findAny();

if(optStatus.isPresent()){
  predicates.add(
    cb.equal(
      root.get("status"),
      OnboardingTaskStatus.valueOf(optStatus.get())
    )
  );
}

Finally, it seems that you are using some kind of conversion mechanism under the hood for converting the status String to the corresponding enum value in your Specification; on the contrary, as you can check in the Hibernate source code, this line will raise a different error:

predicates.add(cb.equal(root.get("status"), params.getStatus()));

Something like:

Parameter value ... did not match expected type ...

If that is the case, please, review that logic as well, perhaps the problem is there.

jccampanero
  • 50,989
  • 3
  • 20
  • 49
1

Heres my solution

public static Optional<OnBoardingTaskStatus> convertStringToEnum(String enumString) {
    Set<String> knownOnBoardingTaskStatusValues = Arrays.asList(OnBoardingTaskStatus.values()).stream().map(Enum::name).collect(Collectors.toSet());
    if(knownOnBoardingTaskStatusValues.contains(enumString))
        return Optional.of(OnBoardingTaskStatus.valueOf(enumString));
    else
        return Optional.empty();
}

Add this method to your service and then add the following to the method findOnboardingTasks

    convertStringToEnum(params.getStatus())
            .ifPresent(e -> predicates.add(cb.equal(root.get("status"), e)));
Maurice
  • 6,698
  • 9
  • 47
  • 104
1

Have you tried to debug which exactly value has the status param?

The exception says that you tried to convert to enum incorrect string value -> no such enum instance found so the exception is thrown.

You could try few ways for solving this:

  • try to add much more logging logic for understanding:
    public Page<OnboardingTaskDto> findOnboardingTasks(OnboardingTaskSearchParams params, Pageable pageable){
        Specification<OnboardingTasks> spec = (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();
            if (params.getStatus() != null && !params.getStatus().isBlank()) { // or StringUtils.isNotBlank(..)
                String statusValue = params.getStatus();
                System.out.printf("STATUS_VALUE: %s%n", statusValue); // or use logging
                OnboardingTaskStatus status = OnboardingTaskStatus.valueOf(statusValue.toUpperCase());
                predicates.add(cb.equal(root.get("status"), status));
            }
            return cb.and(predicates.toArray(new Predicate[predicates.size()]));
        };
        return onboardingTasksService
                .findAll(spec, pageable)
                .map(onboardingMapper::taskToTaskDTO);
    }

I used toUpperCase() in the case when the status is like new instead of NEW. Logging will help to understand which exactly conversion should be done.

Or

  • try to add an additional method for your enum class and call it:
    public enum OnboardingTaskStatus {
        NEW,
        IN_PROGRESS,
        DISABLED;

        public static OnboardingTaskStatus parse(String type) {
            return Optional.of(OnboardingTaskStatus.valueOf(type.toUpperCase()))
                    .orElse(OnboardingTaskStatus.DISABLED); // or another default or undefined value
        }
    }

If type could be null use Optional.ofNullable() for convertion.

catch23
  • 17,519
  • 42
  • 144
  • 217
1

Is it possible that you have dirty stored data somewhere that has been passed to findOnboardingTasks or you forgot .toUpperCase() your enum value?

The string representation of enum object that you have doesn't make sense unless you have stored OnboardingTaskStatus without @Enumerated(EnumType.STRING)

And about your questions:

Do you know what is the proper way to implement the Enum search without throwing an error if the value is not found?

It depends on what do you want to do. Do you want to return an error or simply exclude the predicate if the enum is not found?

You can exclude the invalid enum with a try catch

    try {
        predicates.add(cb.equal(root.get("status"), OnboardingTaskStatus.valueOf(params.getStatus()).toUpperCase()));
    } catch (IllegalArgumentException ex) {
        //log always and
        //go on without adding enum predicate
    }
ValerioMC
  • 2,926
  • 13
  • 24
1

Your code is correct. It seems that you have some strange cached compilation problem. I've had some in the past using old versions of Project Lombok, but not anymore.

Try just cleaning your project from IDE, maven clean, deleting compiled classes folder... If this is not working try removing @Getter and @Setter of OnboardingTaskSearchParams.

sinuhepop
  • 20,010
  • 17
  • 72
  • 107