1

I'm new to generics. I have an abstract class (1) with an abstract method. I have another abstract class (2) with a few abstract methods that I want shared in some DTOs (3,4). I want a concrete extension of (1) to work on anything that extends (2).

1.

//I want R to be anything here
public abstract class AggregatedEmailerRoute<R> extends RouteBuilder {

    public static String EMAILER_ROUTE = "direct:aggregated-emailer-route";
    
@Override
    public void configure() throws Exception {
        from(EMAILER_ROUTE)
                .bean(this, "convertToEmailBody(${body},${headers})")
                .to("mock:wherever");
    }

    protected abstract String convertToEmailBody(List<R> body, Map<String,Object> headers);
public abstract class StagingError { //or should this be an interface?
    public abstract Integer getId();
    public abstract String getErrorNote();
    public abstract LocalDateTime getErrorDate();
}

Two classes that extend StagingError: 3.

public class Bill extends StagingError {

    private Integer id;
    private String errorNote;
    private LocalDateTime getErrorDate;
    //other fields, getters/setters

public class Payment extends StagingError {

    private Integer id;
    private String errorNote;
    private LocalDateTime getErrorDate;
    //other fields, getters/setters

public class StagingEmailErrorRoute extends AggregatedEmailerRoute<R extends StagingError> {

    @Override
    public void configure() throws Exception {
        super.configure();

        from(jpa(Bill.class.getName())
                .query("some query")
        )
                .to(EMAILER_ROUTE);

        from(jpa(Payment.class.getName())
                .query("some other query")
        )
                .to(EMAILER_ROUTE);

    }


@Override //But here I want to work on anything that implements StagingError so this method can work on Payment OR Bill 
    protected String convertToEmailBody(List<T extends StagingError> body, Map<String,Object> headers) {
        return 
                body
                .stream()
                .map(e -> "Staging entity ID: " + e.getId() +
                            "\n\tError: " + e.getErrorNote() +
                            "\n\tWhen: " + format("%tc",e.getErrorDate().atZone(EST.get())) +
                            "\n")
                .collect(joining("\n"));

FaraBara
  • 115
  • 7

2 Answers2

1

Understanding the concept of variance is the key to answering your question.

The definitions you provided are almost correct, though still both StagingEmailErrorRoute and AggregatedEmailerRoute classes need to be slightly modified to fit your expectations.

In particular, the convertToEmailBody(List<R> body, Map<String,Object> headers) as it should meet at least these criteria:

  1. It has to accept a list of R or or a list of its subclasses.
  2. Inside the method you need have access to the elements of body as instances R (another words you need a producer of R).

This is achieved by declaring the body as List<? extends R>:

convertToEmailBody(List<? extends R> body, Map<String, Object> headers)

Then your specific subclass StagingEmailErrorRoute extends AggregatedEmailerRoute<StagingError> is allowed to accept either StagingError or its descendants and work with them as instances of R (inside the stream):

public class StagingEmailErrorRoute extends AggregatedEmailerRoute<StagingError> {

    @Override
    protected String convertToEmailBody(List<? extends StagingError> body, Map<String, Object> headers) {
        return body
            .stream()
            .map(e -> "Staging entity ID: " + e.getId() +
                "\n\tError: " + e.getErrorNote() +
                "\n\tWhen: " + format("%tc",e.getErrorDate().atZone(EST.get())) +
                "\n")
            .collect(joining("\n"));
    }
}

EDIT: Example usage of StagingEmailErrorRoute:

StagingEmailErrorRoute route = new StagingEmailErrorRoute();

//1. consume a list as a mix of `StagingError` descendants
List<StagingError> stagingErrors = new ArrayList<>();
stagingErrors.add(new Bill());
stagingErrors.add(new Payment());

route.convertToEmailBody(stagingErrors, headers);

//2. consume a list of specific subclasses of `StagingError` (i.e. Bill)
List<Bill> billErrors = new ArrayList<>();
billErrors.add(new Bill());

route.convertToEmailBody(billErrors, headers);
dshelya
  • 448
  • 3
  • 13
0

You can write AggregatedEmailerRoute class something like this:

public abstract class AggregatedEmailerRoute<R> extends RouteBuilder {
    //...
    protected abstract String convertToEmailBody(List<R> body, Map<String, Object> headers);
}

And for StagingEmailErrorRoute:

public class StagingEmailErrorRoute<T extends StagingError> extends AggregatedEmailerRoute<T> {

    @Override
    protected String convertToEmailBody(List<T> body, Map<String, Object> headers) {
        return null; 
    }
}

then you can pass any class that extends StaginError, in your case Payment and Bill:

sample code

I hope that is helpful.

  • The OP specified that, quote: "//I want R to be anything here public abstract class AggregatedEmailerRoute" , so you cannot put `StagingError` a bound there – dshelya Jan 03 '22 at 20:02
  • @dshelya You're right, and I updated the AggregatedEmailerRoute. – Mohammad Reza Khahani Jan 03 '22 at 20:37
  • The updated solution is still a bit unpractical. First, the class `StagingEmailErrorRoute` is now generic, so you must specify a type whenever you instantiate it: `StagingEmailErrorRoute route;`, moreover the type argument cannot be anything besides `StagingError` if you want a single `route` instance to be able to consume subclasses of StagingError. Also, because the `body` is declared as `List` - you have to use an invariant list (which cannot be anything else but `List`) – dshelya Jan 03 '22 at 21:09
  • Of course, if keeping multiple instances like `StagingEmailErrorRoute billRoute`, `StagingEmailErrorRoute paymentRoute` per each subclass is not a problem - the proposed solution would work. – dshelya Jan 03 '22 at 21:16