Using a generic type for handling capabilities can be a valid approach, especially when we want to create a flexible and fluent API for configuring different types of capabilities (String, int, boolean…).
Let's map together the following Chrome capabilities of our JSON file:
{
"excludeSwitches": "enable-automation",
"download.prompt_for_download": false,
"binary": "/usr/bin/google-chrome",
"chrome.driver.path": "/root/.cache/selenium/chromedriver/linux64/",
"args": [
"--start-maximized",
"--no-sandbox",
"--disable-dev-shm-usage",
"--headless"
]
}
Note: these capabilities are String and boolean types, so the generic implementation makes it type safety.
Generic Capabilities Configuration Loader implementation:
public class CapabilitiesConfiguration<T extends CapabilitiesConfiguration<T>> {
public static CapabilitiesConfiguration instance;
private static final Object lock = new Object();
public static CapabilitiesConfiguration getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new CapabilitiesConfiguration();
}
}
}
return instance;
}
public synchronized CapabilitiesConfiguration<T> getConfigurationFromJSON(String JSONPath, Class<T> configurationClass) {
CapabilitiesConfiguration<T> configuration = null;
try {
File file = new File(JSONPath);
FileReader fileReader = new FileReader(file);
BufferedReader bufferedReader = new BufferedReader(fileReader);
ObjectMapper objectMapper = new ObjectMapper();
configuration = objectMapper.readValue(bufferedReader, configurationClass);
bufferedReader.close();
} catch (Exception e) {
Log.error(e.getMessage());
}
return configuration;
}
}
The type of generic we are referring to in CapabilitiesConfiguration> is often known as a recursive bounded type parameter or curiously recurring generic pattern.
Let's break down the components:
T: This is a type parameter that represents a type that extends or is equal to something.
extends CapabilitiesConfiguration: This is a bounded type parameter. It specifies that T there must be a subtype of CapabilitiesConfiguration.
Putting it together:
T extends CapabilitiesConfiguration: This means that T is a type parameter that must be a subtype of CapabilitiesConfiguration. In other words, any class that extends CapabilitiesConfiguration must use itself (or a subclass of itself) as the type parameter.
Now let's implement our sub-class of the generic CapabilitiesConfiguration class:
@Getter
@Setter
public class ChromeConfiguration extends CapabilitiesConfiguration<ChromeConfiguration> {
@JsonProperty("excludeSwitches")
private String excludeSwitches;
@JsonProperty("download.prompt_for_download")
private boolean downloadPromptForDownload;
@JsonProperty("binary")
private String binaryPath;
@JsonProperty("chrome.driver.path")
private String driverPath;
@JsonProperty("args")
private List<String> args;
}
@Getter and @setter: These Lombok annotations generate getter and setter methods for the class fields. So, we don't need to write them explicitly; Lombok takes care of that during compilation.
ChromeConfiguration extends CapabilitiesConfiguration: It means that ChromeConfiguration is a subtype of CapabilitiesConfiguration with itself as the type parameter. This enforces that any subclass must use itself as the type parameter when extending CapabilitiesConfiguration, as specified by the generic class.
@JsonProperty("property_name"): These annotations are from Jackson library and are used to map the Java class fields to corresponding property names in JSON during serialization and deserialization.
How to load and use the capabilities?
ChromeConfiguration chromeConfiguration = capsLoader.getConfigurationFromJSON(CHROME_CONFIG_FILE_PATH, ChromeConfiguration.class);
ChromeOptions chromeOptions = new ChromeOptions();
HashMap<String, Object> chromePrefs = new HashMap<>();
chromePrefs.put("download.prompt_for_download", chromeConfiguration.isDownloadPromptForDownload());
chromeOptions.setExperimentalOption("excludeSwitches", new String[]{chromeConfiguration.getExcludeSwitches()});
chromeOptions.setExperimentalOption("prefs", chromePrefs);
chromeOptions.setBinary(chromeConfiguration.getBinaryPath());
chromeConfiguration.getArgs().forEach(chromeOptions::addArguments);
System.setProperty("webdriver.chrome.driver", Objects.requireNonNull(Utils.findAndGetFileName(chromeConfiguration.getDriverPath(), "chromedriver")));
return chromeOptions;
When dealing with a configuration framework that needs to handle a variety of capabilities, using generics in loading capabilities can provide various benefits. Here are some benefits:
Type Safety:
Generics provide compile-time type checking, ensuring that the correct types are used during the loading process. This helps catch type-related errors early in the development process.
Flexibility:
Generics allow us to create a loading mechanism that can handle different types of capabilities without knowing the specific types at compile time. This flexibility is beneficial when dealing with a diverse set of capabilities.Reusability:
A generic loading mechanism can be reused for various types of capabilities. The same loading logic can be applied to different configurations, reducing code duplication and promoting a more maintainable codebase.Consistency:
Using generics ensures a consistent approach to loading capabilities across different types. This consistency can make the codebase more understandable and easier to maintain.Adaptability to Future Changes:
When new types of capabilities are introduced, the generic loading mechanism can often adapt without significant modifications. This adaptability is valuable in scenarios where the set of capabilities may evolve.Easier Maintenance:
With a generic approach, maintaining the loading mechanism becomes more straightforward. Changes and improvements to the loading logic can be applied uniformly across different types of capabilities.
Thanks :)
Top comments (0)