diff --git a/.java-version b/.java-version index 98d9bcb..aabe6ec 100644 --- a/.java-version +++ b/.java-version @@ -1 +1 @@ -17 +21 diff --git a/src/main/java/com/epam/healenium/constants/FieldName.java b/src/main/java/com/epam/healenium/constants/FieldName.java index 2f5d52c..3037bc8 100644 --- a/src/main/java/com/epam/healenium/constants/FieldName.java +++ b/src/main/java/com/epam/healenium/constants/FieldName.java @@ -9,5 +9,7 @@ public class FieldName { public static final String CLASSES = "classes"; public static final String CLASS = "class"; public static final String OTHER = "other"; + /** Parent node in the ancestor chain (serialized without children to avoid cycles). */ + public static final String PARENT = "parent"; } \ No newline at end of file diff --git a/src/main/java/com/epam/healenium/converter/NodeDeserializer.java b/src/main/java/com/epam/healenium/converter/NodeDeserializer.java index 693cc2e..2a88c26 100644 --- a/src/main/java/com/epam/healenium/converter/NodeDeserializer.java +++ b/src/main/java/com/epam/healenium/converter/NodeDeserializer.java @@ -9,8 +9,16 @@ import tools.jackson.databind.JsonNode; import tools.jackson.databind.deser.std.StdDeserializer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.Map; +/** + * Deserializes a {@link Node} and restores the ancestor chain from nested {@code parent} objects. + * Returns the leaf node with {@code parent} links set (via {@link NodeBuilder#addChild}). + */ @SuppressWarnings("unchecked") public class NodeDeserializer extends StdDeserializer { @@ -21,21 +29,57 @@ public NodeDeserializer() { @Override public Node deserialize(JsonParser parser, DeserializationContext ctxt) throws JacksonException { JsonNode tree = ctxt.readTree(parser); + return deserializeLeafWithAncestors(tree, ctxt); + } + + private Node deserializeLeafWithAncestors(JsonNode leafTree, DeserializationContext ctxt) throws JacksonException { + List rootToLeaf = new ArrayList<>(); + for (JsonNode current = leafTree; current != null && !current.isNull() && !current.isMissingNode(); + current = current.get(FieldName.PARENT)) { + rootToLeaf.add(current); + } + Collections.reverse(rootToLeaf); + + // Build root → leaf so NodeBuilder#build sets parent links on children. + Node child = null; + for (int i = rootToLeaf.size() - 1; i >= 0; i--) { + NodeBuilder builder = toBuilder(rootToLeaf.get(i), ctxt); + if (child != null) { + builder.addChild(child); + } + child = builder.build(); + } + + Node leaf = child; + while (leaf != null && leaf.getChildren() != null && !leaf.getChildren().isEmpty()) { + leaf = leaf.getChildren().getFirst(); + } + return leaf; + } + + private NodeBuilder toBuilder(JsonNode tree, DeserializationContext ctxt) throws JacksonException { String tag = ctxt.readTreeAsValue(tree.path(FieldName.TAG), String.class); Integer index = ctxt.readTreeAsValue(tree.path(FieldName.INDEX), Integer.class); String innerText = ctxt.readTreeAsValue(tree.path(FieldName.INNER_TEXT), String.class); String id = ctxt.readTreeAsValue(tree.path(FieldName.ID), String.class); String classes = ctxt.readTreeAsValue(tree.path(FieldName.CLASSES), String.class); Map attributes = ctxt.readTreeAsValue(tree.path(FieldName.OTHER), Map.class); - attributes.put(FieldName.ID, id); - attributes.put(FieldName.CLASS, classes); + if (attributes == null) { + attributes = new HashMap<>(); + } else { + attributes = new HashMap<>(attributes); + } + attributes.put(FieldName.ID, id != null ? id : ""); + attributes.put(FieldName.CLASS, classes != null ? classes : ""); - return new NodeBuilder() + NodeBuilder builder = new NodeBuilder() .setTag(tag) - .setIndex(index) - .addContent(innerText) - .setAttributes(attributes) - .build(); + .setIndex(index != null ? index : 0) + .setAttributes(attributes); + if (innerText != null) { + builder.addContent(innerText); + } + return builder; } } diff --git a/src/main/java/com/epam/healenium/converter/NodeSerializer.java b/src/main/java/com/epam/healenium/converter/NodeSerializer.java index 739a4d1..6cdcd6e 100644 --- a/src/main/java/com/epam/healenium/converter/NodeSerializer.java +++ b/src/main/java/com/epam/healenium/converter/NodeSerializer.java @@ -10,6 +10,14 @@ import tools.jackson.databind.jsontype.TypeSerializer; import tools.jackson.databind.ser.std.StdSerializer; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; + +/** + * Serializes a {@link Node} including its ancestor chain via nested {@code parent} objects. + * Children are intentionally omitted to avoid cycles and oversized payloads. + */ public class NodeSerializer extends StdSerializer { public NodeSerializer() { @@ -27,6 +35,12 @@ public void serializeWithType(Node value, JsonGenerator gen, SerializationContex @Override public void serialize(Node value, JsonGenerator gen, SerializationContext serializers) throws JacksonException { + Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + visited.add(value); + serializeNode(value, gen, visited); + } + + private void serializeNode(Node value, JsonGenerator gen, Set visited) throws JacksonException { gen.writeStartObject(); gen.writeStringProperty(FieldName.TAG, value.getTag()); Integer index = value.getIndex(); @@ -39,6 +53,13 @@ public void serialize(Node value, JsonGenerator gen, SerializationContext serial gen.writeStringProperty(FieldName.ID, value.getId()); gen.writeStringProperty(FieldName.CLASSES, String.join(" ", value.getClasses())); gen.writePOJOProperty(FieldName.OTHER, value.getOtherAttributes()); + + Node parent = value.getParent(); + if (parent != null && visited.add(parent)) { + gen.writeName(FieldName.PARENT); + serializeNode(parent, gen, visited); + } + gen.writeEndObject(); gen.flush(); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e9f1190..519439f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -98,6 +98,12 @@ logging: max-history: 10 total-size-cap: 100MB +# Healenium healing settings (env aliases used by docker-compose) +healenium: + selector-type: ${SELECTOR_TYPE:cssSelector} + score-cap: ${SCORE_CAP:.6} + recovery-tries: ${RECOVERY_TRIES:1} + # Proxy service configuration proxy: service: