diff --git a/Dockerfile b/Dockerfile index 372b712..fdb667d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,3 @@ -FROM eclipse-temurin:17-jdk-alpine +FROM eclipse-temurin:22-alpine COPY /build/libs/healenium-backend-*.jar /healenium-backend.jar CMD java -jar /healenium-backend.jar diff --git a/build.gradle b/build.gradle index 30dce1c..a51e557 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.springframework.boot' version '3.2.3' + id 'org.springframework.boot' version '3.3.4' id 'io.spring.dependency-management' version "1.1.4" id 'java' id 'maven-publish' @@ -7,7 +7,7 @@ plugins { } group 'com.epam.healenium' -version '3.4.5' +version '3.4.6' repositories { @@ -23,6 +23,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.boot:spring-boot-starter-logging' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.liquibase:liquibase-core:4.25.1' implementation 'org.postgresql:postgresql:42.6.1' implementation 'com.zaxxer:HikariCP:3.3.1' @@ -30,14 +31,13 @@ dependencies { implementation 'javax.validation:validation-api:2.0.1.Final' implementation 'org.yaml:snakeyaml:2.0' implementation 'com.google.guava:guava:32.1.1-jre' - - implementation 'org.seleniumhq.selenium:selenium-java:4.16.1' - implementation 'com.epam.healenium:tree-comparing:0.4.13' + implementation 'com.epam.healenium:tree-comparing:0.4.14' + implementation 'org.seleniumhq.selenium:selenium-java:4.25.0' implementation 'org.projectlombok:lombok:1.18.22' implementation 'org.mapstruct:mapstruct:1.3.1.Final' implementation group: 'io.netty', name: 'netty-handler', version: '4.1.104.Final' - implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.14' - implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.4.14' + implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.5.8' + implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.5.8' implementation 'com.fasterxml.jackson.core:jackson-databind:2.16.0' implementation 'org.testcontainers:junit-jupiter:1.19.3' diff --git a/src/main/java/com/epam/healenium/controller/HealingController.java b/src/main/java/com/epam/healenium/controller/HealingController.java index b7c62c3..3bbfb4a 100644 --- a/src/main/java/com/epam/healenium/controller/HealingController.java +++ b/src/main/java/com/epam/healenium/controller/HealingController.java @@ -14,6 +14,7 @@ import com.epam.healenium.service.SelectorService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -21,9 +22,13 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.validation.FieldError; import org.springframework.web.servlet.ModelAndView; import javax.validation.Valid; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -181,4 +186,15 @@ public ModelAndView migrate() { return modelAndView; } + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { + Map errors = new HashMap<>(); + ex.getBindingResult().getAllErrors().forEach((error) -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.put(fieldName, errorMessage); + }); + return ResponseEntity.badRequest().body(errors); + } + } \ No newline at end of file diff --git a/src/main/java/com/epam/healenium/mapper/HealingMapper.java b/src/main/java/com/epam/healenium/mapper/HealingMapper.java index 5620344..081f9c9 100644 --- a/src/main/java/com/epam/healenium/mapper/HealingMapper.java +++ b/src/main/java/com/epam/healenium/mapper/HealingMapper.java @@ -1,5 +1,6 @@ package com.epam.healenium.mapper; +import com.epam.healenium.model.Locator; import com.epam.healenium.model.domain.Healing; import com.epam.healenium.model.domain.HealingResult; import com.epam.healenium.model.dto.HealingDto; @@ -25,12 +26,26 @@ public interface HealingMapper { default HealingResult resultDtoToModel(HealingResultDto dto) { HealingResult result = new HealingResult(); - result.setLocator(dto.getLocator()); - result.setScore(dto.getScore()); + result.setLocator(getLocator(dto)); + result.setScore(getScore(dto)); result.setCreateDate(LocalDateTime.now()); return result; } + default Double getScore(HealingResultDto dto) { + if (dto.getScore() == null || dto.getScore() < 0 || dto.getScore() > 1) { + throw new RuntimeException("Invalid Score value: " + dto.getScore()); + } + return dto.getScore(); + } + + default Locator getLocator(HealingResultDto dto) { + if (dto.getLocator() == null || !dto.getLocator().getType().contains("By")) { + throw new RuntimeException("Invalid Locator value: " + dto.getLocator()); + } + return dto.getLocator(); + } + @IterableMapping(elementTargetType = HealingResult.class) Set resultDtoToModel(Collection dto); diff --git a/src/main/java/com/epam/healenium/mapper/SelectorMapper.java b/src/main/java/com/epam/healenium/mapper/SelectorMapper.java index ebe4197..2f06e78 100644 --- a/src/main/java/com/epam/healenium/mapper/SelectorMapper.java +++ b/src/main/java/com/epam/healenium/mapper/SelectorMapper.java @@ -3,7 +3,6 @@ import com.epam.healenium.constants.Constants; import com.epam.healenium.model.Locator; import com.epam.healenium.model.domain.Selector; -import com.epam.healenium.model.dto.RequestDto; import com.epam.healenium.model.dto.SelectorDto; import com.epam.healenium.model.dto.SelectorRequestDto; import com.epam.healenium.model.wrapper.NodePathWrapper; @@ -35,8 +34,8 @@ default Selector toSelector(SelectorRequestDto dto, String id, Optional toRequestDto(List selector) { - List requestDtoResult = new ArrayList<>(); + default List toRequestDto(List selector) { + List requestDtoResult = new ArrayList<>(); for (Selector selectorEntity : selector) { SelectorRequestDto requestDto = new SelectorRequestDto(); requestDto.setId(selectorEntity.getUid()); diff --git a/src/main/java/com/epam/healenium/model/SessionContext.java b/src/main/java/com/epam/healenium/model/SessionContext.java deleted file mode 100644 index 9f7a490..0000000 --- a/src/main/java/com/epam/healenium/model/SessionContext.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.epam.healenium.model; - -import com.epam.healenium.node.NodeService; -import lombok.Data; -import lombok.experimental.Accessors; -import org.openqa.selenium.remote.RemoteWebDriver; - -@Data -@Accessors(chain = true) -public class SessionContext { - - private RemoteWebDriver remoteWebDriver; - private NodeService nodeService; -} diff --git a/src/main/java/com/epam/healenium/model/dto/SelectorRequestDto.java b/src/main/java/com/epam/healenium/model/dto/SelectorRequestDto.java index cc308e0..a38b6aa 100644 --- a/src/main/java/com/epam/healenium/model/dto/SelectorRequestDto.java +++ b/src/main/java/com/epam/healenium/model/dto/SelectorRequestDto.java @@ -1,20 +1,20 @@ package com.epam.healenium.model.dto; import com.epam.healenium.treecomparing.Node; -import lombok.AllArgsConstructor; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; import lombok.ToString; -import java.io.Serializable; import java.util.List; @Data -@EqualsAndHashCode(callSuper = true) -public class SelectorRequestDto extends RequestDto { +@EqualsAndHashCode +public class SelectorRequestDto { private String id; + @Pattern(regexp = "^By.*", message = "The field must start with 'By'") private String type; @ToString.Exclude private List> nodePath; @@ -22,5 +22,15 @@ public class SelectorRequestDto extends RequestDto { private String sessionId; private boolean enableHealing; private boolean urlKey; + @NotBlank + private String locator; + @NotBlank + private String className; + @NotBlank + private String methodName; + @Pattern(regexp = "^(findElement|findElements)$", message = "The command must be either 'findElement' or 'findElements'") + private String command; + @NotBlank + private String url; } diff --git a/src/main/java/com/epam/healenium/node/MobileNodeService.java b/src/main/java/com/epam/healenium/node/MobileNodeService.java deleted file mode 100644 index 612a421..0000000 --- a/src/main/java/com/epam/healenium/node/MobileNodeService.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.epam.healenium.node; - -import com.epam.healenium.treecomparing.Node; -import com.epam.healenium.treecomparing.NodeBuilder; -import lombok.extern.slf4j.Slf4j; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Attribute; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.parser.Parser; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.remote.RemoteWebDriver; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -@Slf4j(topic = "healenium") -@Service -public class MobileNodeService implements NodeService { - - private static final int ONE_ELEMENT = 1; - private static final int THE_ONLY_ELEMENT = 0; - - @Override - public List getNodePath(WebDriver driver, WebElement element) { - String xmlString = driver.getPageSource(); - Document doc = Jsoup.parse(xmlString, "", Parser.xmlParser()); - Element currentElementInDoc = getElementFromDoc(doc, element); - List list = new ArrayList<>(); - - while (currentElementInDoc.hasParent()) { - Node currentNode = toNode(currentElementInDoc); - list.add(currentNode); - currentElementInDoc = currentElementInDoc.parent(); - } - Collections.reverse(list); - return new LinkedList<>(list); - } - - private Element getElementFromDoc(Document doc, WebElement webElement) { - List paramsList = Arrays.asList("resource-id", "content-desc", "text", "class", "bounds", "checked", - "enabled", "selected", "focused", "displayed"); - - List tempElements = new ArrayList<>(doc.getAllElements()); - Iterator it = paramsList.iterator(); - - if (tempElements.size() == ONE_ELEMENT) { - return tempElements.get(THE_ONLY_ELEMENT); - } - - while (it.hasNext()) { - String nextParam = it.next(); - String tempValue = webElementParamValue(nextParam, webElement); - tempElements.removeIf(e -> !e.attributes().get(nextParam).equals(tempValue)); - - if (tempElements.size() == ONE_ELEMENT) { - return tempElements.get(THE_ONLY_ELEMENT); - } - } - return tempElements.get(THE_ONLY_ELEMENT); - } - - private String webElementParamValue(String currentAttribute, WebElement webElement) { - String temp = webElement.getAttribute(currentAttribute); - return temp != null ? temp : ""; - } - - private Node toNode(Element e) { - Map otherAttributes = new HashMap<>(); - List list = e.attributes().asList(); - list.forEach(attr -> otherAttributes.put(attr.getKey(), attr.getValue())); - - return new NodeBuilder() - .setTag(e.attributes().get("class")) - .setContent(Collections.singletonList(e.attributes().get("text"))) - .setOtherAttributes(otherAttributes) - .build(); - } - - public String getCurrentUrl(WebDriver driver) { - return (String) ((RemoteWebDriver) driver).getCapabilities().asMap() - .getOrDefault("appium:appActivity", ""); - } -} diff --git a/src/main/java/com/epam/healenium/node/NodeService.java b/src/main/java/com/epam/healenium/node/NodeService.java deleted file mode 100644 index 18c3b78..0000000 --- a/src/main/java/com/epam/healenium/node/NodeService.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.epam.healenium.node; - -import com.epam.healenium.treecomparing.Node; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface NodeService { - List getNodePath(WebDriver driver, WebElement element); - - String getCurrentUrl(WebDriver driver); -} diff --git a/src/main/java/com/epam/healenium/node/WebNodeService.java b/src/main/java/com/epam/healenium/node/WebNodeService.java deleted file mode 100644 index 850ffb1..0000000 --- a/src/main/java/com/epam/healenium/node/WebNodeService.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.epam.healenium.node; - -import com.epam.healenium.constants.FieldName; -import com.epam.healenium.treecomparing.Node; -import com.epam.healenium.treecomparing.NodeBuilder; -import com.epam.healenium.util.ResourceReader; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.ObjectCodec; -import com.fasterxml.jackson.core.TreeNode; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.extern.slf4j.Slf4j; -import org.openqa.selenium.JavascriptExecutor; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.springframework.stereotype.Service; - -import java.io.IOException; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -@Slf4j(topic = "healenium") -@Service -public class WebNodeService implements NodeService { - - /** - * A JavaScript source to extract an HTML item with its attributes - */ - private static final String SCRIPT = ResourceReader.readResource( - "itemsWithAttributes.js", s -> s.collect(Collectors.joining())); - - /** - * build list nodes by source webElement - * - * @param webElement - source element - * @return - list path nodes - */ - public List getNodePath(WebDriver driver, WebElement webElement) { - JavascriptExecutor executor = (JavascriptExecutor) driver; - String data = (String) executor.executeScript(SCRIPT, webElement); - List path = new LinkedList<>(); - - try { - ObjectMapper mapper = new ObjectMapper(); - JsonNode treeNode = mapper.readTree(data); - if (treeNode.isArray()) { - for (final JsonNode jsonNode : treeNode) { - Node node = toNode(mapper.treeAsTokens(jsonNode)); - path.add(node); - } - } - } catch (Exception ex) { - log.error("Failed to get element node path!", ex); - } - return path; - } - - /** - * Convert raw data to {@code Node} - * - * @param parser - JSON reader - * @return path node - * @throws IOException - */ - private Node toNode(JsonParser parser) throws IOException { - ObjectCodec codec = parser.getCodec(); - TreeNode tree = parser.readValueAsTree(); - String tag = codec.treeToValue(tree.path(FieldName.TAG), String.class); - Integer index = codec.treeToValue(tree.path(FieldName.INDEX), Integer.class); - String innerText = codec.treeToValue(tree.path(FieldName.INNER_TEXT), String.class); - String id = codec.treeToValue(tree.path(FieldName.ID), String.class); - //noinspection unchecked - Set classes = codec.treeToValue(tree.path(FieldName.CLASSES), Set.class); - //noinspection unchecked - Map attributes = codec.treeToValue(tree.path(FieldName.OTHER), Map.class); - return new NodeBuilder() - //TODO: fix attribute setting, because they override 'id' and 'classes' property - .setAttributes(attributes) - .setTag(tag) - .setIndex(index) - .setId(id) - .addContent(innerText) - .setClasses(classes) - .build(); - } - - @Override - public String getCurrentUrl(WebDriver driver) { - return driver.getCurrentUrl(); - } -} diff --git a/src/main/java/com/epam/healenium/service/SelectorService.java b/src/main/java/com/epam/healenium/service/SelectorService.java index 381e204..6f788a3 100644 --- a/src/main/java/com/epam/healenium/service/SelectorService.java +++ b/src/main/java/com/epam/healenium/service/SelectorService.java @@ -6,7 +6,6 @@ import com.epam.healenium.model.dto.RequestDto; import com.epam.healenium.model.dto.SelectorDto; import com.epam.healenium.model.dto.SelectorRequestDto; -import com.epam.healenium.model.dto.SessionDto; import java.util.List; @@ -15,7 +14,7 @@ public interface SelectorService { ReferenceElementsDto getReferenceElements(RequestDto dto); - List getAllSelectors(); + List getAllSelectors(); ConfigSelectorDto getConfigSelectors(); diff --git a/src/main/java/com/epam/healenium/service/impl/ReportServiceImpl.java b/src/main/java/com/epam/healenium/service/impl/ReportServiceImpl.java index 49632ef..439222e 100644 --- a/src/main/java/com/epam/healenium/service/impl/ReportServiceImpl.java +++ b/src/main/java/com/epam/healenium/service/impl/ReportServiceImpl.java @@ -31,6 +31,8 @@ @RequiredArgsConstructor public class ReportServiceImpl implements ReportService { + private static final String SESSION_ID_REGEX = "^[a-fA-F0-9]+$"; + private final ReportRepository reportRepository; private final HealingResultRepository resultRepository; @@ -143,7 +145,7 @@ private String transformPath(String sourcePath) { */ @Override public void createReportRecord(HealingResult result, Healing healing, String sessionId, byte[] screenshot) { - if (!StringUtils.isEmpty(sessionId)) { + if (!StringUtils.isEmpty(sessionId) && sessionId.matches(SESSION_ID_REGEX)) { String screenshotDir = "/screenshots/" + sessionId; String screenshotPath = persistScreenshot(screenshot, screenshotDir); log.debug("[Save Healing] Screenshot Path: {}", screenshotPath); diff --git a/src/main/java/com/epam/healenium/service/impl/SelectorServiceImpl.java b/src/main/java/com/epam/healenium/service/impl/SelectorServiceImpl.java index 065ac11..496d2d6 100644 --- a/src/main/java/com/epam/healenium/service/impl/SelectorServiceImpl.java +++ b/src/main/java/com/epam/healenium/service/impl/SelectorServiceImpl.java @@ -2,7 +2,6 @@ import com.epam.healenium.mapper.SelectorMapper; -import com.epam.healenium.model.SessionContext; import com.epam.healenium.model.domain.Healing; import com.epam.healenium.model.domain.Selector; import com.epam.healenium.model.dto.ConfigSelectorDto; @@ -10,7 +9,6 @@ import com.epam.healenium.model.dto.RequestDto; import com.epam.healenium.model.dto.SelectorDto; import com.epam.healenium.model.dto.SelectorRequestDto; -import com.epam.healenium.node.NodeService; import com.epam.healenium.repository.HealingRepository; import com.epam.healenium.repository.SelectorRepository; import com.epam.healenium.service.SelectorService; @@ -19,20 +17,14 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections4.map.PassiveExpiringMap; -import org.openqa.selenium.remote.RemoteWebDriver; -import org.openqa.selenium.remote.RemoteWebElement; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; @Slf4j(topic = "healenium") @Service @@ -46,18 +38,12 @@ public class SelectorServiceImpl implements SelectorService { @Value("${app.healing.elements}") private boolean findElementsAutoHealing; - private PassiveExpiringMap sessionContextCache = new PassiveExpiringMap<>(8, TimeUnit.HOURS); - private final SelectorRepository selectorRepository; private final SelectorMapper selectorMapper; private final HealingRepository healingRepository; @Override public void saveSelector(SelectorRequestDto request) { - if (CollectionUtils.isEmpty(request.getNodePath())) { - log.debug("[Save Elements] Parse Node Path"); - parseNodePath(request); - } String id = getSelectorId(request.getLocator(), request.getUrl(), request.getCommand(), urlForKey); Optional existSelector = selectorRepository.findById(id); final Selector selector = selectorMapper.toSelector(request, id, existSelector, findElementsAutoHealing); @@ -76,7 +62,7 @@ public ReferenceElementsDto getReferenceElements(RequestDto dto) { } @Override - public List getAllSelectors() { + public List getAllSelectors() { List selectors = selectorRepository.findAll(); return selectorMapper.toRequestDto(selectors); } @@ -119,35 +105,6 @@ public void migrate() { migrateSelectors(all); } - - private void parseNodePath(SelectorRequestDto request) { - try { - SessionContext sessionContext = sessionContextCache.get(request.getSessionId()); - if (sessionContext == null || sessionContext.getRemoteWebDriver() == null) { - log.warn("[Save Elements] SessionContext or RemoteWebDriver not found from cache."); - return; - } - NodeService nodeService = sessionContext.getNodeService(); - RemoteWebDriver remoteWebDriver = sessionContext.getRemoteWebDriver(); - String url = nodeService.getCurrentUrl(remoteWebDriver); - log.debug("[Save Elements] Parse Node Path to URL: {}", url); - request.setUrl(url); - List ids = request.getElementIds(); - List> nodes = ids.stream() - .map(id -> { - RemoteWebElement remoteWebElement = new RemoteWebElement(); - remoteWebElement.setId(id); - remoteWebElement.setParent(remoteWebDriver); - return nodeService.getNodePath(remoteWebDriver, remoteWebElement); - }) - .collect(Collectors.toList()); - request.setNodePath(nodes); - log.debug("[Save Elements] Parse Node size: {}", nodes.size()); - } catch (Exception e) { - log.error("[Save Elements] Error during parseNodePath. Message: {} Ex: {}", e.getMessage(), e); - } - } - @Override public void migrateSelectors(List sourceSelectors) { if (sourceSelectors.isEmpty()) { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c1f5ced..3ca5d04 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,9 +5,7 @@ server: spring: banner: - image: - location: - /images/banner.png + location: /images/banner.txt servlet.multipart: max-file-size: -1 max-request-size: -1 diff --git a/src/main/resources/images/banner.txt b/src/main/resources/images/banner.txt new file mode 100644 index 0000000..965bf5e --- /dev/null +++ b/src/main/resources/images/banner.txt @@ -0,0 +1,17 @@ + + .*%@@@@@#- =#@@@@@#= + :@@@@%**#@@@@@@%#**%@@@# + =@@@. +@@@- =@@@. + :@@# =@@@+ @@@ + *@@- -@@@* +@@: + +@@= .@@@# -=- *@@. + @@@@@@@ #@@@@@ -@@* + .@@@@. @@@@@@@ @@@% + %@@= *@@@@@@ #@@@@@= + =@@+ -@@*. *@@@- #@@ + *@@- =@@@+ +@@: + =@@+ -@@@* #@@ + #@@+ :@@@# %@@= + *@@@#-:.-+@@@@@=:.:=%@@@- + *@@@@@@@@#=%@@@@@@@%= + \ No newline at end of file