package com.nordstrom.automation.selenium.utility; import static net.bytebuddy.matcher.ElementMatchers.*; import static org.openqa.selenium.remote.BrowserType.SAFARI; import static org.openqa.selenium.remote.CapabilityType.BROWSER_NAME; import java.lang.reflect.Field; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.commons.lang3.reflect.FieldUtils; import org.openqa.grid.internal.utils.DefaultCapabilityMatcher; import net.bytebuddy.ByteBuddy; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.implementation.MethodDelegation; /** * This capability matcher is functionally equivalent to {@link DefaultCapabilityMatcher}, implemented to avoid direct * references to the {@code SafariOptions} class. This avoids the need to include the path to the <b>safari-driver</b> * JAR on the class path provided to the Selenium Grid hub process. */ public class RevisedCapabilityMatcher extends DefaultCapabilityMatcher { private static final String SAFARI_SPECIFIC_VALIDATOR = "org.openqa.grid.internal.utils.DefaultCapabilityMatcher$SafariSpecificValidator"; private static final String REVISED_SAFARI_VALIDATOR = "org.openqa.grid.internal.utils.DefaultCapabilityMatcher$RevisedSafariValidator"; private static Class<?> safariValidator; /** * This constructor replaces the {@code SafariSpecificValidator} instance in the <b>validators</b> list of the * {@link DefaultCapabilityMatcher} with an instance of a dynamically-generated {@code RevisedSafariValidator} * class. This dynamic validator is functionally equivalent, but is implemented without explicit references to * the {@code SafariOptions} class. */ @SuppressWarnings("unchecked") public RevisedCapabilityMatcher() { super(); Field field = FieldUtils.getField(DefaultCapabilityMatcher.class, "validators", true); if (field != null) { field.setAccessible(true); try { List<Object> list = (List<Object>) field.get(this); Iterator<Object> iter = list.iterator(); while (iter.hasNext()) { Object item = iter.next(); Class<?> clazz = item.getClass(); if (SAFARI_SPECIFIC_VALIDATOR.equals(clazz.getName())) { Object validator = newSafariValidator(clazz); iter.remove(); list.add(validator); break; } } } catch (IllegalArgumentException | IllegalAccessException | ClassCastException | InstantiationException e) { // just eat the exception } } } /** * Create a new instance of the dynamically-generated {@code RevisedSafariValidator} class. * * @param validatorClass {@code SafariSpecificValidator} class * @return instance of dynamically-generated replacement class * @throws InstantiationException if this class represents an abstract class, an interface, an array class, a * primitive type, or {@code void}; if the class lacks a no-argument constructor; or if instantiation * fails for some other reason. * @throws IllegalAccessException if the class or its no-argument constructor are inaccessible. */ private static Object newSafariValidator(Class<?> validatorClass) throws InstantiationException, IllegalAccessException { if (safariValidator == null) { safariValidator = subclassSafariValidator(validatorClass); } return safariValidator.newInstance(); } /** * Dynamically generate a replacement for the {@code SafariSpecificValidator} class. * * @param validatorClass {@code SafariSpecificValidator} class * @return dynamically-generated {@code RevisedSafariValidator} class */ private static Class<?> subclassSafariValidator(Class<?> validatorClass) { Class<?> validatorIntfc = validatorClass.getInterfaces()[0]; return new ByteBuddy() .subclass(validatorIntfc) .name(REVISED_SAFARI_VALIDATOR) .method(named("apply")) .intercept(MethodDelegation.to(SafariValidator.class)) .make() .load(validatorClass.getClassLoader(), ClassLoadingStrategy.Default.INJECTION) .getLoaded(); } /** * This class implements to {@code apply()} method declared by the {@code Validator} interface. It also implements * methods to extract Safari-specific settings from specified capabilities maps. */ public static class SafariValidator { static final String SAFARI_TECH_PREVIEW = "Safari Technology Preview"; static final String AUTOMATIC_INSPECTION = "safari:automaticInspection"; static final String AUTOMATIC_PROFILING = "safari:automaticProfiling"; static final String TECHNOLOGY_PREVIEW = "technologyPreview"; public static Boolean apply(Map<String, Object> providedCapabilities, Map<String, Object> requestedCapabilities) { if (!SAFARI.equals(getBrowserName(requestedCapabilities)) && !SAFARI_TECH_PREVIEW.equals(getBrowserName(requestedCapabilities))) { return true; } return getAutomaticInspection(requestedCapabilities) == getAutomaticInspection(providedCapabilities) && getAutomaticProfiling(requestedCapabilities) == getAutomaticProfiling(providedCapabilities) && getUseTechnologyPreview(requestedCapabilities) == getUseTechnologyPreview(providedCapabilities); } public static String getBrowserName(Map<String, Object> capabilities) { return (String) capabilities.get(BROWSER_NAME); } public static boolean getAutomaticInspection(Map<String, Object> capabilities) { return Boolean.TRUE.equals(capabilities.get(AUTOMATIC_INSPECTION)); } public static boolean getAutomaticProfiling(Map<String, Object> capabilities) { return Boolean.TRUE.equals(capabilities.get(AUTOMATIC_PROFILING)); } public static boolean getUseTechnologyPreview(Map<String, Object> capabilities) { return SAFARI_TECH_PREVIEW.equals(getBrowserName(capabilities)) || Boolean.TRUE.equals(capabilities.get(TECHNOLOGY_PREVIEW)); } } }