diff --git a/bellatrix.core/pom.xml b/bellatrix.core/pom.xml index 23e2f014..478c5657 100644 --- a/bellatrix.core/pom.xml +++ b/bellatrix.core/pom.xml @@ -153,5 +153,20 @@ ${rest.assured.version} compile + + com.azure + azure-security-keyvault-secrets + 4.2.3 + + + com.azure + azure-identity + 1.2.0 + + + org.jsoup + jsoup + 1.17.2 + \ No newline at end of file diff --git a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/ConverterService.java b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/ConverterService.java index 466746b8..7023c17f 100644 --- a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/ConverterService.java +++ b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/ConverterService.java @@ -1,14 +1,33 @@ +/* + * Copyright 2024 Automate The Planet Ltd. + * Author: Miriam Kyoseva + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package solutions.bellatrix.core.utilities; import lombok.SneakyThrows; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + public class ConverterService { public static ResultT convert(SourceT source, int index) { ResultT object = InstanceFactory.createByTypeParameter(source.getClass(), index); + assert object != null; return convert(source, object); } + @SuppressWarnings("unchecked") public static ResultT convert(SourceT source, ResultT result) { var object = (ResultT) InstanceFactory.create(result.getClass()); @@ -38,8 +57,8 @@ public static ResultT convert(SourceT source, ResultT result) public static ResultT convertToClass(SourceT source, Class result) { var object = (ResultT) InstanceFactory.create(result); - var sourceFields = source.getClass().getDeclaredFields(); - var objectFields = object.getClass().getDeclaredFields(); + List sourceFields = getAllFields(source.getClass()); + List objectFields = getAllFields(object.getClass()); for (var sourceField : sourceFields) { for (var objectField : objectFields) { @@ -55,4 +74,19 @@ public static ResultT convertToClass(SourceT source, Class getAllFields(Class clazz) { + List fields = new ArrayList<>(); + Class currentClass = clazz; + + while (currentClass != null) { + var declaredFields = currentClass.getDeclaredFields(); + for (Field field : declaredFields) { + fields.add(field); + } + currentClass = currentClass.getSuperclass(); + } + + return fields; + } } diff --git a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/HtmlService.java b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/HtmlService.java new file mode 100644 index 00000000..306e62cc --- /dev/null +++ b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/HtmlService.java @@ -0,0 +1,128 @@ +/* + * Copyright 2024 Automate The Planet Ltd. + * Author: Miriam Kyoseva + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package solutions.bellatrix.core.utilities; + +import lombok.experimental.UtilityClass; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.Node; +import solutions.bellatrix.core.utilities.parsing.TypeParser; + +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Web Utility Class + */ +@UtilityClass +public class HtmlService { + private static final String CHILD_COMBINATOR = " > "; + private static final String NODE = "/"; + private static final String NODE_OR_SELF = "//"; + private static final String ROOT_ELEMENT_TAG = "bellatrix-root"; + + public static Document addRootElementIfNeeded(Document doc) { + boolean hasMultipleTopLevelElements = doc.childNodes().stream() + .filter(node -> node instanceof Element).count() > 1; + + if (hasMultipleTopLevelElements) { + var root = new Element(ROOT_ELEMENT_TAG); + + for (Node node : doc.childNodes()) { + root.appendChild(node.clone()); + } + + doc.empty(); + doc.appendChild(root); + } + + return doc; + } + + public static String getAbsoluteXpath(Element element) { + StringBuilder xpath = new StringBuilder(); + + Element currentElement = element; + while (currentElement != null) { + if (currentElement.tagName().equals("html") || currentElement.tagName().equals("body") || currentElement.tagName().startsWith("#") || currentElement.tagName().equals(ROOT_ELEMENT_TAG)) { + // ignore the and , because jsoup added them to the html fragment + // ignore added bellatrix root element + // ignore invalid element tags + break; + } + + xpath.insert(0, indexElement(currentElement)); + + currentElement = currentElement.parent(); + } + + return xpath.toString(); + } + + private String indexElement(Element element) { + int index = 1; + + Node previousSibling = element.previousSibling(); + while (previousSibling != null) { + if (previousSibling.nodeName().equals(element.nodeName())) { + index++; + } + previousSibling = previousSibling.previousSibling(); + } + + return NODE + element.tagName() + "[" + index + "]"; + } + + public T getAttribute(Element element, String attributeName, Class clazz) { + if (element.attribute(attributeName) == null || element.attribute(attributeName).getValue() == null || element.attribute(attributeName).getValue().isBlank()) { + return null; + } else { + return TypeParser.parse(element.attribute(attributeName).getValue(), clazz); + } + } + + public static String convertAbsoluteXpathToCss(String xpath) { + String cssSelector = xpath.replace(NODE, CHILD_COMBINATOR); + + // Use regular expression to replace [number] with :nth-of-type(number) + Pattern pattern = Pattern.compile("\\[(\\d+)\\]"); + Matcher matcher = pattern.matcher(cssSelector); + StringBuilder builder = new StringBuilder(); + + while (matcher.find()) { + matcher.appendReplacement(builder, ":nth-of-type(" + matcher.group(1) + ")"); + } + matcher.appendTail(builder); + + var semiFinalLocator = builder.toString(); + + if (semiFinalLocator.startsWith(CHILD_COMBINATOR)) { + semiFinalLocator = semiFinalLocator.substring(2); + } + + return semiFinalLocator; + } + + public static String removeDanglingChildCombinatorsFromCss(String css) { + // convert to array by splitting the css by the child combinator + // and remove from that array empty steps + var steps = Arrays.stream(css.split(CHILD_COMBINATOR)) + .filter(x -> !x.isBlank()) + .toArray(String[]::new); + + // join the remaining steps with child combinator operators + return String.join(CHILD_COMBINATOR, steps); + } +} diff --git a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/InstanceFactory.java b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/InstanceFactory.java index fc630195..dd8e5360 100644 --- a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/InstanceFactory.java +++ b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/InstanceFactory.java @@ -16,6 +16,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.ParameterizedType; +@SuppressWarnings("unchecked") public final class InstanceFactory { public static T create(Class classOf) { T obj = null; diff --git a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/PropertyReference.java b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/PropertyReference.java new file mode 100644 index 00000000..494f510b --- /dev/null +++ b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/PropertyReference.java @@ -0,0 +1,8 @@ +package solutions.bellatrix.core.utilities; + +import java.io.Serializable; + +@FunctionalInterface +public interface PropertyReference extends Serializable { + Object apply(T t); +} diff --git a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/PropertyReferenceNameResolver.java b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/PropertyReferenceNameResolver.java new file mode 100644 index 00000000..839e0282 --- /dev/null +++ b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/PropertyReferenceNameResolver.java @@ -0,0 +1,33 @@ +package solutions.bellatrix.core.utilities; + +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; + +import java.lang.invoke.SerializedLambda; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +@UtilityClass +public class PropertyReferenceNameResolver { + public static Field getMember(Class clazz, PropertyReference propertyReference) { + var methodName = getFunctionalMethodName(propertyReference); + var fields = clazz.getDeclaredFields(); + + for (var field : fields) { + if (methodName.toLowerCase().contains(field.getName().toLowerCase())) { + return field; + } + } + + return null; + } + + @SneakyThrows + public static String getFunctionalMethodName(PropertyReference functionalInterface) { + Method writeReplaceMethod = functionalInterface.getClass().getDeclaredMethod("writeReplace"); + writeReplaceMethod.setAccessible(true); + SerializedLambda serializedLambda = (SerializedLambda) writeReplaceMethod.invoke(functionalInterface); + + return serializedLambda.getImplMethodName(); + } +} diff --git a/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/utilities/functionalinterfaces/AssertionMethod.java b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/Ref.java similarity index 75% rename from bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/utilities/functionalinterfaces/AssertionMethod.java rename to bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/Ref.java index 66018e0c..fe74b7a6 100644 --- a/bellatrix.playwright/src/main/java/solutions/bellatrix/playwright/utilities/functionalinterfaces/AssertionMethod.java +++ b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/Ref.java @@ -11,9 +11,15 @@ * limitations under the License. */ -package solutions.bellatrix.playwright.utilities.functionalinterfaces; +package solutions.bellatrix.core.utilities; -@FunctionalInterface -public interface AssertionMethod { - void perform(); -} +/** + * Wrapper for passing primitives by reference. + */ +public class Ref { + public T value; + + public Ref(T value) { + this.value = value; + } +} \ No newline at end of file diff --git a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/SingletonFactory.java b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/SingletonFactory.java index 1988bff3..dd7d93b8 100644 --- a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/SingletonFactory.java +++ b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/SingletonFactory.java @@ -13,31 +13,42 @@ package solutions.bellatrix.core.utilities; +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; + import java.util.HashMap; import java.util.Map; // Based on http://neutrofoton.github.io/blog/2013/08/29/generic-singleton-pattern-in-java/ // Can be used inside App design pattern. +@SuppressWarnings("unchecked") public class SingletonFactory { private static final SingletonFactory SINGLETON_FACTORY = new SingletonFactory(); - private final Map mapHolder = new HashMap<>(); + private final Map, Object> mapHolder = new HashMap<>(); private SingletonFactory() { } + @SneakyThrows public static T getInstance(Class classOf, Object... initargs) { try { - if (!SINGLETON_FACTORY.mapHolder.containsKey(classOf.getName())) { - + if (!SINGLETON_FACTORY.mapHolder.containsKey(classOf)) { T obj = (T)classOf.getConstructors()[0].newInstance(initargs); - SINGLETON_FACTORY.mapHolder.put(classOf.getName(), obj); + SINGLETON_FACTORY.mapHolder.put(classOf, obj); } - - return (T)SINGLETON_FACTORY.mapHolder.get(classOf.getName()); + return (T)SINGLETON_FACTORY.mapHolder.get(classOf); } catch (Exception e) { Log.error("Failed to create instance of the object. Exception was: " + e); return null; } } + + public static void register(T instance) { + SINGLETON_FACTORY.mapHolder.put(instance.getClass(), instance); + } + + public static void register(Class classKey, T instance) { + SINGLETON_FACTORY.mapHolder.put(classKey, instance); + } } diff --git a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/Wait.java b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/Wait.java index 136a1d80..873f8fb9 100644 --- a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/Wait.java +++ b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/Wait.java @@ -77,4 +77,28 @@ public static boolean retry(Runnable action, Duration timeout, Duration sleepInt public static void retry(Runnable action, Duration timeout, Duration sleepInterval, Class ... exceptionsToIgnore) { Wait.retry(action, timeout, sleepInterval, true, exceptionsToIgnore); } + + public static boolean forConditionUntilTimeout(Comparator condition, long timeoutInMilliseconds, long pollingIntervalInMilliseconds) { + boolean isConditionMet = false; + + long startTime = System.currentTimeMillis(); + + while (!isConditionMet && (System.currentTimeMillis() - startTime) <= (timeoutInMilliseconds)) { + try { + if (condition.evaluate()) { + isConditionMet = true; + } else { + Thread.sleep(pollingIntervalInMilliseconds); + } + } catch (Exception ignored) { + } + } + + return isConditionMet; + } + + @FunctionalInterface + public interface Comparator { + boolean evaluate(); + } } diff --git a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/parsing/GenericDateTimeParser.java b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/parsing/GenericDateTimeParser.java new file mode 100644 index 00000000..532731f7 --- /dev/null +++ b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/parsing/GenericDateTimeParser.java @@ -0,0 +1,127 @@ +package solutions.bellatrix.core.utilities.parsing; + +import lombok.experimental.UtilityClass; +import solutions.bellatrix.core.utilities.Ref; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.List; + +@UtilityClass +public class GenericDateTimeParser { + public static List FORMATTERS = new ArrayList<>(); + public static List PRIORITIZED_FORMATTERS = new ArrayList<>(); + + public static void addPattern(String pattern) { + FORMATTERS.add(DateTimeFormatter.ofPattern(pattern)); + } + + public static void prioritizePattern(String pattern) { + PRIORITIZED_FORMATTERS.add(DateTimeFormatter.ofPattern(pattern)); + } + + static { + FORMATTERS.add(DateTimeFormatter.ISO_DATE_TIME); + FORMATTERS.add(DateTimeFormatter.ISO_TIME); + FORMATTERS.add(DateTimeFormatter.ISO_DATE); + FORMATTERS.add(DateTimeFormatter.BASIC_ISO_DATE); + FORMATTERS.add(DateTimeFormatter.ISO_LOCAL_DATE); + FORMATTERS.add(DateTimeFormatter.ISO_LOCAL_TIME); + FORMATTERS.add(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + FORMATTERS.add(DateTimeFormatter.ISO_OFFSET_DATE); + FORMATTERS.add(DateTimeFormatter.ISO_OFFSET_TIME); + FORMATTERS.add(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + FORMATTERS.add(DateTimeFormatter.ISO_ZONED_DATE_TIME); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss.SSS'Z'")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm:ss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("dd MMMM yyyy HH:mm:ss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("E, MMM dd yyyy HH:mm:ss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("EEE, MMM dd, yyyy HH:mm:ss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("EEEE, MMMM dd, yyyy HH:mm:ss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy.MM.dd G 'at' HH:mm:ss z")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy.MM.dd GGGGG 'at' HH:mm:ss z")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy.MM.dd GGGGG 'at' HH:mm:ss z")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss.SSS")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss,SSS")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyyMMdd HHmmss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyyMMdd HHmmssSSS")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyyMMdd HH:mm")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyyMMdd HHmm")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyyMMdd HH")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyyMMdd")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy/MM/dd")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.S")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy/MM/dd'T'HH:mm:ss.SSSXXX")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS")); + FORMATTERS.add(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")); + } + + public static LocalDateTime parse(String data) { + if (!PRIORITIZED_FORMATTERS.isEmpty()) { + for (var formatter : PRIORITIZED_FORMATTERS) { + try { + return LocalDateTime.parse(data, formatter); + } catch (DateTimeParseException ignored){ + } + } + } + for (var formatter : FORMATTERS) { + try { + return LocalDateTime.parse(data, formatter); + } catch (DateTimeParseException ignored){ + } + } + + throw new IllegalArgumentException(String.format("There was no proper format found for input string '%s'." + + "Please register your custom format in %s by using the add method!", data, GenericDateTimeParser.class.getName())); + } + + public static boolean tryParse(String data, Ref returnObject) { + if (!PRIORITIZED_FORMATTERS.isEmpty()) { + for (var formatter : PRIORITIZED_FORMATTERS) { + try { + returnObject.value = LocalDateTime.parse(data, formatter); + return true; + } catch (DateTimeParseException ignored){ + } + } + } + for (var formatter : FORMATTERS) { + try { + returnObject.value = LocalDateTime.parse(data, formatter); + return true; + } catch (DateTimeParseException ignored){ + } + } + returnObject.value = null; + return false; + } +} diff --git a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/parsing/ParsingInstructions.java b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/parsing/ParsingInstructions.java new file mode 100644 index 00000000..7da08d7a --- /dev/null +++ b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/parsing/ParsingInstructions.java @@ -0,0 +1,31 @@ +package solutions.bellatrix.core.utilities.parsing; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@SuppressWarnings({"unchecked", "rawtypes"}) +// TODO: RENAME ME +public class ParsingInstructions extends HashMap, Function> { + public ParsingInstructions() { + super(); + } + + public ParsingInstructions(Map, Function> map) { + super(map); + } + + public ParsingInstructions add(Class toClass, Function parsingInstructions) { + // TODO: EventHandler<> for changing existing instructions + super.put(toClass, parsingInstructions); + return this; + } + + public Function get(Class clazz) { + return super.get(clazz); + } + + public static ParsingInstructions ofEntries(Entry, Function>... entries) { + return new ParsingInstructions(Map.ofEntries(entries)); + } +} \ No newline at end of file diff --git a/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/parsing/TypeParser.java b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/parsing/TypeParser.java new file mode 100644 index 00000000..b3f975f0 --- /dev/null +++ b/bellatrix.core/src/main/java/solutions/bellatrix/core/utilities/parsing/TypeParser.java @@ -0,0 +1,252 @@ +/* + * Copyright 2024 Automate The Planet Ltd. + * Author: Miriam Kyoseva + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package solutions.bellatrix.core.utilities.parsing; + +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +/** + * Utility class that contains instructions how to convert from one type to another. + */ +@UtilityClass +@SuppressWarnings({"unchecked", "rawtypes"}) +public class TypeParser { + /** + * Checks for enum fields containing the specified value and prioritizes which enum value to return by the field, + * instead of the name of the enum value. + */ + @SneakyThrows + public static T parseEnumValue(Object value, Class enumClass) { + var fields = enumClass.getDeclaredFields(); + + for (T enumConstant : enumClass.getEnumConstants()) { + for (var field : fields) { + if (field.getClass() == value.getClass()) { + field.setAccessible(true); + Object fieldValue = field.get(enumConstant); + if (fieldValue != null && fieldValue.equals(value)) { + return enumConstant; + } + } + } + } + if (value.getClass() == String.class) { + return (T) Enum.valueOf((Class)enumClass, (String)value); + } else { + // TODO: EventHandler<> for failing to parse enum. + return null; + } + } + + /** + * Checks if instructions for parsing the value to the given class exist and uses them.
+ * In case instructions aren't provided, you can provide them dynamically in your code through {@link #instruct(Class, Class, Function)} + */ + public static T parse(Object value, Class clazz) { + var originalClass = value.getClass(); + + if (clazz == null) { + throw new IllegalArgumentException("Class cannot be null."); + } + + Function operation = TypeParser.ALLOWED_OPERATIONS.get(originalClass).get(clazz); + + if (operation != null) { + return operation.apply(value); + } else if (clazz.isEnum()) { + Function enumOperation = x -> TypeParser.parseEnumValue(x, clazz); + TypeParser.ALLOWED_OPERATIONS.get(String.class).add(clazz, enumOperation); + return enumOperation.apply(value); + } else { + throw new IllegalArgumentException("Unsupported class type: " + clazz.getName() + + "\nYou can add a custom type parser in " + TypeParser.class.getName()); + } + } + + public static void instruct(Class fromClass, Class toClass, Function instructions) { + if (ALLOWED_OPERATIONS.containsKey(fromClass)) { + ALLOWED_OPERATIONS.get(fromClass).add(toClass, instructions); + } else { + Objects.requireNonNull(ALLOWED_OPERATIONS.put(fromClass, new ParsingInstructions<>())).add(toClass, instructions); + } + } + + public static void instruct(Class fromClass, Map.Entry, Function>... entries) { + var map = ParsingInstructions.ofEntries(entries); + + if (ALLOWED_OPERATIONS.containsKey(fromClass)) { + ALLOWED_OPERATIONS.get(fromClass).putAll(map); + } else { + Objects.requireNonNull(ALLOWED_OPERATIONS.put(fromClass, new ParsingInstructions<>())).putAll(map); + } + } + + private static final ParsingInstructions STRING_ALLOWED_OPERATIONS = ParsingInstructions.ofEntries( + Map.entry(String.class, (Function) (x) -> x), + Map.entry(Integer.class, (Function) Integer::parseInt), + Map.entry(Double.class, (Function) Double::parseDouble), + Map.entry(Float.class, (Function) Float::parseFloat), + Map.entry(Byte.class, (Function) Byte::parseByte), + Map.entry(Long.class, (Function) Long::parseLong), + Map.entry(Short.class, (Function) Short::parseShort), + Map.entry(Boolean.class, (Function) Boolean::parseBoolean), + Map.entry(Character.class, (Function) (x) -> x.charAt(0)), + Map.entry(BigDecimal.class, (Function) BigDecimal::new), + Map.entry(BigInteger.class, (Function) BigInteger::new), + Map.entry(LocalDateTime.class, (Function)GenericDateTimeParser::parse) + // TODO: to DateTime + // TODO: to Date + ); + + private static final ParsingInstructions INTEGER_ALLOWED_OPERATIONS = ParsingInstructions.ofEntries( + Map.entry(Integer.class, (Function)(x) -> x), + Map.entry(String.class, (Function)String::valueOf), + Map.entry(Double.class, (Function)Integer::doubleValue), + Map.entry(Float.class, (Function)Integer::floatValue), + Map.entry(Byte.class, (Function)Integer::byteValue), + Map.entry(Long.class, (Function)Integer::longValue), + Map.entry(Short.class, (Function)Integer::shortValue), + Map.entry(Character.class, (Function)(x) -> String.valueOf(x).charAt(0)), + Map.entry(BigDecimal.class, (Function)BigDecimal::valueOf), + Map.entry(BigInteger.class, (Function)BigInteger::valueOf) + ); + + private static final ParsingInstructions DOUBLE_ALLOWED_OPERATIONS = ParsingInstructions.ofEntries( + Map.entry(Double.class, (Function)(x) -> x), + Map.entry(String.class, (Function)String::valueOf), + Map.entry(Integer.class, (Function)Double::intValue), + Map.entry(Float.class, (Function)Double::floatValue), + Map.entry(Byte.class, (Function)Double::byteValue), + Map.entry(Long.class, (Function)Double::longValue), + Map.entry(Short.class, (Function)Double::shortValue), + Map.entry(Character.class, (Function)(x) -> String.valueOf(x).charAt(0)), + Map.entry(BigDecimal.class, (Function)BigDecimal::valueOf), + Map.entry(BigInteger.class, (Function)(x) -> BigInteger.valueOf(x.intValue())) + ); + + private static final ParsingInstructions FLOAT_ALLOWED_OPERATIONS = ParsingInstructions.ofEntries( + Map.entry(Float.class, (Function)(x) -> x), + Map.entry(String.class, (Function)String::valueOf), + Map.entry(Integer.class, (Function)Float::intValue), + Map.entry(Double.class, (Function)Float::doubleValue), + Map.entry(Byte.class, (Function)Float::byteValue), + Map.entry(Long.class, (Function)Float::longValue), + Map.entry(Short.class, (Function)Float::shortValue), + Map.entry(Character.class, (Function)(x) -> String.valueOf(x).charAt(0)), + Map.entry(BigDecimal.class, (Function)BigDecimal::valueOf), + Map.entry(BigInteger.class, (Function)(x) -> BigInteger.valueOf(x.intValue())) + ); + + private static final ParsingInstructions BYTE_ALLOWED_OPERATIONS = ParsingInstructions.ofEntries( + Map.entry(Byte.class, (Function)(x) -> x), + Map.entry(String.class, (Function)String::valueOf), + Map.entry(Integer.class, (Function)Byte::intValue), + Map.entry(Double.class, (Function)Byte::doubleValue), + Map.entry(Float.class, (Function)Byte::floatValue), + Map.entry(Long.class, (Function)Byte::longValue), + Map.entry(Short.class, (Function)Byte::shortValue), + Map.entry(Character.class, (Function)(x) -> String.valueOf(x).charAt(0)), + Map.entry(BigDecimal.class, (Function)BigDecimal::valueOf), + Map.entry(BigInteger.class, (Function)(x) -> BigInteger.valueOf(x.intValue())) + ); + + private static final ParsingInstructions LONG_ALLOWED_OPERATIONS = ParsingInstructions.ofEntries( + Map.entry(Long.class, (Function)(x) -> x), + Map.entry(String.class, (Function)String::valueOf), + Map.entry(Integer.class, (Function)Long::intValue), + Map.entry(Double.class, (Function)Long::doubleValue), + Map.entry(Float.class, (Function)Long::floatValue), + Map.entry(Byte.class, (Function)Long::byteValue), + Map.entry(Short.class, (Function)Long::shortValue), + Map.entry(Character.class, (Function)(x) -> String.valueOf(x).charAt(0)), + Map.entry(BigDecimal.class, (Function)BigDecimal::valueOf), + Map.entry(BigInteger.class, (Function)(x) -> BigInteger.valueOf(x.intValue())) + ); + + private static final ParsingInstructions SHORT_ALLOWED_OPERATIONS = ParsingInstructions.ofEntries( + Map.entry(Short.class, (Function)(x) -> x), + Map.entry(String.class, (Function)String::valueOf), + Map.entry(Integer.class, (Function)Short::intValue), + Map.entry(Double.class, (Function)Short::doubleValue), + Map.entry(Float.class, (Function)Short::floatValue), + Map.entry(Byte.class, (Function)Short::byteValue), + Map.entry(Long.class, (Function)Short::longValue), + Map.entry(Character.class, (Function)(x) -> String.valueOf(x).charAt(0)), + Map.entry(BigDecimal.class, (Function)BigDecimal::valueOf), + Map.entry(BigInteger.class, (Function)(x) -> BigInteger.valueOf(x.intValue())) + ); + + private static final ParsingInstructions CHARACTER_ALLOWED_OPERATIONS = ParsingInstructions.ofEntries( + Map.entry(Character.class, (Function)(x) -> x), + Map.entry(String.class, (Function)String::valueOf), + Map.entry(Integer.class, (Function)Integer::valueOf), + Map.entry(Double.class, (Function)Double::valueOf), + Map.entry(Float.class, (Function)Float::valueOf), + Map.entry(Byte.class, (Function)(x) -> Byte.valueOf(String.valueOf(x))), + Map.entry(Long.class, (Function)Long::valueOf), + Map.entry(Short.class, (Function)(x) -> Short.valueOf(String.valueOf(x))), + Map.entry(BigDecimal.class, (Function)BigDecimal::valueOf), + Map.entry(BigInteger.class, (Function)(x) -> new BigInteger(String.valueOf(x))) + ); + + private static final ParsingInstructions BIG_DECIMAL_ALLOWED_OPERATIONS = ParsingInstructions.ofEntries( + Map.entry(BigDecimal.class, (Function)(x) -> x), + Map.entry(String.class, (Function)String::valueOf), + Map.entry(Integer.class, (Function)BigDecimal::intValue), + Map.entry(Double.class, (Function)BigDecimal::doubleValue), + Map.entry(Float.class, (Function)BigDecimal::floatValue), + Map.entry(Byte.class, (Function)BigDecimal::byteValue), + Map.entry(Long.class, (Function)BigDecimal::longValue), + Map.entry(Short.class, (Function)BigDecimal::shortValue), + Map.entry(Character.class, (Function)(x) -> String.valueOf(x).charAt(0)), + Map.entry(BigInteger.class, (Function)(x) -> new BigInteger(String.valueOf(x))) + ); + + private static final ParsingInstructions BIG_INTEGER_ALLOWED_OPERATIONS = ParsingInstructions.ofEntries( + Map.entry(BigInteger.class, (Function)(x) -> x), + Map.entry(String.class, (Function)String::valueOf), + Map.entry(Integer.class, (Function)BigInteger::intValue), + Map.entry(Double.class, (Function)BigInteger::doubleValue), + Map.entry(Float.class, (Function)BigInteger::floatValue), + Map.entry(Byte.class, (Function)BigInteger::byteValue), + Map.entry(Long.class, (Function)BigInteger::longValue), + Map.entry(Short.class, (Function)BigInteger::shortValue), + Map.entry(Character.class, (Function)(x) -> String.valueOf(x).charAt(0)), + Map.entry(BigDecimal.class, (Function)(x) -> new BigDecimal(String.valueOf(x))) + ); + + /** + * Instructions for converting from one type to another.
+ * You can specify it statically here, or you could add new instructions dynamically via {@link #instruct(Class, Class, Function)} + */ + private static Map ALLOWED_OPERATIONS = Map.ofEntries( + Map.entry(String.class, STRING_ALLOWED_OPERATIONS), + Map.entry(Integer.class, INTEGER_ALLOWED_OPERATIONS), + Map.entry(Double.class, DOUBLE_ALLOWED_OPERATIONS), + Map.entry(Float.class, FLOAT_ALLOWED_OPERATIONS), + Map.entry(Byte.class, BYTE_ALLOWED_OPERATIONS), + Map.entry(Long.class, LONG_ALLOWED_OPERATIONS), + Map.entry(Short.class, SHORT_ALLOWED_OPERATIONS), + Map.entry(Character.class, CHARACTER_ALLOWED_OPERATIONS), + Map.entry(BigDecimal.class, BIG_DECIMAL_ALLOWED_OPERATIONS), + Map.entry(BigInteger.class, BIG_INTEGER_ALLOWED_OPERATIONS) + ); +} diff --git a/bellatrix.playwright.getting.started/src/test/java/O24_iframes_and_shadow_dom/IFramesAndShadowDOMTests.java b/bellatrix.playwright.getting.started/src/test/java/O24_iframes_and_shadow_dom/IFramesAndShadowDOMTests.java deleted file mode 100644 index 8556178d..00000000 --- a/bellatrix.playwright.getting.started/src/test/java/O24_iframes_and_shadow_dom/IFramesAndShadowDOMTests.java +++ /dev/null @@ -1,52 +0,0 @@ -package O24_iframes_and_shadow_dom; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import solutions.bellatrix.playwright.components.*; -import solutions.bellatrix.playwright.infrastructure.junit.WebTest; - -public class IFramesAndShadowDOMTests extends WebTest { - @Test - public void testFindingIFramesOnThePage() { - app().navigate().to("https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_iframe"); - app().create().byId(Button.class, "accept-choices").click(); - - // If you create a Frame component, it automatically will start searching for elements inside an