1

I have an HTTP client class that uses Generics. I would like to create a factory class that has a map with (key, value) => (String type, HttpClient<?>).

The HttpClient class has one function that just sends data to the server depending on the generic class type. Essentially what I want to have is a generic class that sends data, that's it.

The issue is that I have multiple classes and I wanted to use a factory class in order to:

  1. Simplify the creation of HttpClient's objects.
  2. Avoid using the "new" keyword all the time because I can use the same instance of the HttpClient<Some Object> class throughout my application.

Please see code below:

Factory

public class SystemPreferencesFactory {

    private static SystemPreferencesFactory factory = null;
    private  Map<String, PreferencesHTTPClient<? extends ISystemPreferences>> preferencesMap;

    private SystemPreferencesFactory(){
        this.preferencesMap = Map.of
                (PreferencesHTTPType.DUT_PREFERENCES.getName(), new PreferencesHTTPClient<DutPreferencesDTO>(PreferencesHTTPType.DUT_PREFERENCES.getUrl()),
                PreferencesHTTPType.MACHINE_PROPERTIES.getName(), new PreferencesHTTPClient<MachineProperties>(PreferencesHTTPType.MACHINE_PROPERTIES.getUrl())
                );
    }

    public static PreferencesHTTPClient<? extends ISystemPreferences> getPreferencesHTTPClient(String type) {
        if (factory == null) {
            factory = new SystemPreferencesFactory();
        }
        return factory.preferencesMap.get(type);
    }

}

HttpClient

public class PreferencesHTTPClient<T> extends HTTPClient {
    private static final Logger logger = LoggerManager.getLogger();
    private static String route = "";

    public PreferencesHTTPClient(String route){
        this.route = route;
    }

    public Future<HttpResponse> put(T dto) {
        try {
            System.err.println(dto.getClass());
            HttpPut request = new HttpPut(URIAffix + route + "/" + SystemPreferences.systemPreferences().getSetupName());
            request.addHeader("Authorization", authorizationPassword);
            request.addHeader("Content-Type","application/json");
            request.setEntity(new StringEntity(objectMapper.writeValueAsString(dto)));

            return getClient().execute(request,
                    new FutureCallback<>() {
                        @Override
                        public void completed(final HttpResponse response) {
                            logger.info("update request succeeded");
                        }

                        @Override
                        public void failed(Exception ex) {
                            logger.error("update request failed: {}", ex.getMessage());
                        }

                        @Override
                        public void cancelled() {
                            logger.error("update request canceled");
                        }
                    });
        } catch (JsonProcessingException | UnsupportedEncodingException e) {
            logger.error("update request failed: {}", e.getMessage());
        }

        return CompletableFuture.completedFuture(null);
    }
}

When I try to call it in main I get an compile error

SystemPreferencesFactory.getPreferencesHTTPClient(PreferencesHTTPType.DUT_PREFERENCES.getName())
                 .put(converterUtil.DutFromEntityToDTO(dp));

The compile error is:

Required type:
capture of ? extends ISystemPreferences
Provided:
DutPreferencesDTO

My DutPreferencesDTO class is declared as so:

public class DutPreferencesDTO implements ISystemPreferences

What is wrong with my syntax?

samabcde
  • 6,988
  • 2
  • 25
  • 41
Gilad Dahan
  • 508
  • 5
  • 19
  • You cannot call `put` at it accepts a wildcard method argument. As soon at `T` is bound to `?` you cannot use the method any more - this is the classic Java generics covariance example. The idea is that the type of your client is `?` (a specific unknown type) and therefore the compiler cannot determine whether what you’re trying to pass in is of the correct type - classic example being a `List extends Number>` - you cannot `add` as the compiler cannot know whether you’re adding the right type. To solve you need to declare your `put` method as bounded on `? extends T`. – Boris the Spider Jul 25 '21 at 09:26
  • Thanks for the response. However, I am not sure I followed you suggestion, can you give me an example of how to declare my put function? – Gilad Dahan Jul 25 '21 at 09:28
  • `public Future put(E dto)`. I’m not certain that will work with all your other wildcards - I haven’t got an IDE in front of me and you haven’t really posted a MCVE – Boris the Spider Jul 25 '21 at 09:31
  • I added it and now gets error: Inferred type 'E' for type parameter 'E' is not within its bound; should extend 'capture extends com.altair.infrastructures.dataobjects.systemPreferences.ISystemPreferences>' Also, where is the use of E, it is only declared but I never used it – Gilad Dahan Jul 25 '21 at 09:40
  • Also I want the put method to return a Future – Gilad Dahan Jul 25 '21 at 09:43

1 Answers1

1

We can modify getPreferencesHTTPClient as below:

public class SystemPreferencesFactory {

...
                  // Change to generic method
    public static <T extends ISystemPreferences> PreferencesHTTPClient<T> getPreferencesHTTPClient(String type) {
        if (factory == null) {
            factory = new SystemPreferencesFactory();
        }
        // cast result explicitly
        return (PreferencesHTTPClient<T>) factory.preferencesMap.get(type);
    }

    public static void main(String[] args) {
        SystemPreferencesFactory.getPreferencesHTTPClient(PreferencesHTTPType.DUT_PREFERENCES.name())
                .put(new DutPreferencesDTO());
        // Oops wrong dto still compiles
        SystemPreferencesFactory.getPreferencesHTTPClient(PreferencesHTTPType.DUT_PREFERENCES.name())
                .put(new MachineProperties());
        // Assign to a local variable to infer
        PreferencesHTTPClient<DutPreferencesDTO> preferencesHTTPClient = SystemPreferencesFactory.getPreferencesHTTPClient(PreferencesHTTPType.DUT_PREFERENCES.name());
        preferencesHTTPClient.put(new DutPreferencesDTO());
        // Compile error;
        preferencesHTTPClient.put(new MachineProperties());
        // Use type witness to guard
        SystemPreferencesFactory
                .<DutPreferencesDTO>getPreferencesHTTPClient(PreferencesHTTPType.DUT_PREFERENCES.name())
                // Compile error;
                .put(new MachineProperties());
    }
}

Seems not bad. But as demonstrated in the main method, there is one drawback.

We need to explicitly infer the type when we call getPreferencesHTTPClient, to get desired type checking. But Others using this method may easily forget to do so and may put wrong type of DTO when calling put method.

So to avoid such problem, we use the Class of DTO, instead of String in getPreferencesHTTPClient as follows:

public class SystemPreferencesFactorySafe {
    private static SystemPreferencesFactorySafe factory = null;
    private Map<Class<?>, PreferencesHTTPClient<? extends ISystemPreferences>> preferencesMap;

    private SystemPreferencesFactorySafe() {
        this.preferencesMap = Map.of
                (DutPreferencesDTO.class, new PreferencesHTTPClient<DutPreferencesDTO>(PreferencesHTTPType.DUT_PREFERENCES.getUrl()),
                MachineProperties.class, new PreferencesHTTPClient<MachineProperties>(PreferencesHTTPType.MACHINE_PROPERTIES.getUrl())
                );
    }
                  // T is inferred by type this time
    public static <T extends ISystemPreferences> PreferencesHTTPClient<T> getPreferencesHTTPClient(Class<T> type) {
        if (factory == null) {
            factory = new SystemPreferencesFactorySafe();
        }
        return (PreferencesHTTPClient<T>) factory.preferencesMap.get(type);
    }

    public static void main(String[] args) {
        SystemPreferencesFactorySafe.getPreferencesHTTPClient(DutPreferencesDTO.class)
                .put(new DutPreferencesDTO());
        // Good we see compile error.
        SystemPreferencesFactorySafe.getPreferencesHTTPClient(DutPreferencesDTO.class)
                .put(new MachineProperties());
    }
}

Since the type is inferred using the parameter, we don't need explicit infer to get compile error when someone put wrong type of DTO.

samabcde
  • 6,988
  • 2
  • 25
  • 41
  • Thank you for the answer, I'm checking it out. I am not sure I understand the syntax of the function declaration: "public static PreferencesHTTPClient func()" can you please explain it? What the function returns? – Gilad Dahan Jul 26 '21 at 08:32
  • What the method return depends on how you infer `T`. Check [how does the method infer the type of ](https://stackoverflow.com/questions/22280055/how-does-the-method-infer-the-type-of-t) for details. – samabcde Jul 26 '21 at 14:44