diff --git a/src/main/java/com/github/olivergondza/saxeed/Bookmark.java b/src/main/java/com/github/olivergondza/saxeed/Bookmark.java new file mode 100644 index 0000000..d471dbb --- /dev/null +++ b/src/main/java/com/github/olivergondza/saxeed/Bookmark.java @@ -0,0 +1,19 @@ +package com.github.olivergondza.saxeed; + +/** + * A reference to a tag acquired in a previous Saxeed run, to be queried in the next run. + * + * The reference is valid only between two consecutive runs. Provided the document was modified between the executions, + * Saxeed provides no guarantee it will match anything, fail predictably, or match the intended tag. + * + * To bookmark an element, call {@link Tag#bookmark()}. The object returned is valid outside the Saxeed transformation. + * In the next processing, use {@link Tag#isBookmarked(Bookmark)} or {@link Tag#isBookmarked(java.util.List)} to + * identify the bookmarked element. + */ +public interface Bookmark { + + /** + * Determine if the tag was removed from the output. + */ + boolean isOmitted(); +} diff --git a/src/main/java/com/github/olivergondza/saxeed/Tag.java b/src/main/java/com/github/olivergondza/saxeed/Tag.java index 257bc20..04f9240 100644 --- a/src/main/java/com/github/olivergondza/saxeed/Tag.java +++ b/src/main/java/com/github/olivergondza/saxeed/Tag.java @@ -1,6 +1,7 @@ package com.github.olivergondza.saxeed; import java.util.Collection; +import java.util.List; import java.util.Map; /** @@ -65,6 +66,21 @@ public interface Tag { */ Map getAttributes(); + /** + * Create a bookmark for this element. + */ + Bookmark bookmark(); + + /** + * Determine if this tag matches the bookmark provided. + */ + boolean isBookmarked(Bookmark bookmark); + + /** + * Determine if this tag has been bookmarked by any of the bookmarks provided. + */ + boolean isBookmarked(List bookmarks); + /** * Determine if the current tag was added by a visitor. * @@ -76,7 +92,7 @@ public interface Tag { boolean isGenerated(); /** - * Determine if the current was removed. + * Determine if the current tag was removed. * * In other words, it will not be written to the Target. */ diff --git a/src/main/java/com/github/olivergondza/saxeed/TagName.java b/src/main/java/com/github/olivergondza/saxeed/TagName.java index 37958c1..70ab3fd 100644 --- a/src/main/java/com/github/olivergondza/saxeed/TagName.java +++ b/src/main/java/com/github/olivergondza/saxeed/TagName.java @@ -100,4 +100,18 @@ public String getQualifiedName() { public TagName inheritNamespace(String name) { return new TagName(uri, prefix, name); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TagName tagName = (TagName) o; + return Objects.equals(local, tagName.local) && Objects.equals(uri, tagName.uri); + } + + @Override + public int hashCode() { + return Objects.hash(local, uri); + } } diff --git a/src/main/java/com/github/olivergondza/saxeed/Transformation.java b/src/main/java/com/github/olivergondza/saxeed/Transformation.java deleted file mode 100644 index 5564d0e..0000000 --- a/src/main/java/com/github/olivergondza/saxeed/Transformation.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.github.olivergondza.saxeed; - -public interface Transformation { - static TransformationBuilder build() { - return new TransformationBuilder(); - } - - static Transformation fromAnnotations() { - throw new UnsupportedOperationException(); - } -} diff --git a/src/main/java/com/github/olivergondza/saxeed/internal/BookmarkImpl.java b/src/main/java/com/github/olivergondza/saxeed/internal/BookmarkImpl.java new file mode 100644 index 0000000..f062ca3 --- /dev/null +++ b/src/main/java/com/github/olivergondza/saxeed/internal/BookmarkImpl.java @@ -0,0 +1,74 @@ +package com.github.olivergondza.saxeed.internal; + +import com.github.olivergondza.saxeed.Bookmark; +import com.github.olivergondza.saxeed.TagName; + +import java.util.Objects; + +public class BookmarkImpl implements Bookmark { + private String value; + private boolean omitted = false; + + static BookmarkImpl from(BookmarkImpl parent, TagName name, int count) { + return new BookmarkImpl(pathFrom(parent, name, count)); + } + + static String pathFrom(BookmarkImpl parent, TagName name, int count) { + StringBuilder sb = new StringBuilder(); + if (parent != null) { + assert parent.value != null: "Parent tag " + parent + " must not be written by the time its children " + name + " are still being created."; + sb.append(parent.value); + } + + sb.append('/').append(name.getLocal()); + String nsUri = name.getNsUri(); + if (!nsUri.isEmpty()) { + sb.append('<').append(nsUri).append('>'); + } + sb.append('[').append(count).append(']'); + return sb.toString(); + } + + private BookmarkImpl(String bookmark) { + this.value = bookmark; + } + + /*package*/ void update(String value) { + this.value = value; + } + + /*package*/ void omit() { + omitted = true; + } + + @Override + public boolean isOmitted() { + return omitted; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + BookmarkImpl bookmark = (BookmarkImpl) o; + + // If either is omitted, they are not equal. This is to prevent that omitted bookmark would match real tag based + // on value clash. + if (omitted || bookmark.omitted) { + return false; + } + + return Objects.equals(value, bookmark.value); + } + + @Override + public int hashCode() { + return Objects.hash(value, omitted); + } + + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/com/github/olivergondza/saxeed/internal/TagImpl.java b/src/main/java/com/github/olivergondza/saxeed/internal/TagImpl.java index 8fb7865..37a5d0b 100644 --- a/src/main/java/com/github/olivergondza/saxeed/internal/TagImpl.java +++ b/src/main/java/com/github/olivergondza/saxeed/internal/TagImpl.java @@ -1,10 +1,12 @@ package com.github.olivergondza.saxeed.internal; +import com.github.olivergondza.saxeed.Bookmark; import com.github.olivergondza.saxeed.Tag; import com.github.olivergondza.saxeed.TagName; import org.xml.sax.Attributes; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -57,15 +59,21 @@ */ private TagImpl wrapWith; + private final BookmarkImpl bookmark; + private final Map childCounts = new HashMap<>(); + /** * Create generated Tag. */ private TagImpl(TagImpl parent, TagName name) { + this.parent = parent; this.name = Objects.requireNonNull(name); - this.attrs = null; // No SAX attrs, setting attributes right away + // No SAX attrs, setting attributes right away + this.attrs = null; this.attributes = new LinkedHashMap<>(); this.namespaces = null; this.generated = true; + this.bookmark = initBookmark(); init(parent); } @@ -73,19 +81,19 @@ private TagImpl(TagImpl parent, TagName name) { * Create Tag from input. */ public TagImpl(TagImpl parent, TagName name, Attributes attrs, Map namespaces) { + this.parent = parent; this.name = Objects.requireNonNull(name); this.attrs = Objects.requireNonNull(attrs); // Create defensive copy in either case this.namespaces = namespaces.isEmpty() ? null : new LinkedHashMap<>(namespaces); this.generated = false; + this.bookmark = initBookmark(); init(parent); } private void init(TagImpl parent) { - this.parent = parent; - - // Inherit the write mode based on the parent's one. if (parent != null) { + // Inherit the write mode based on the parent's one. writeMode = parent.writeMode.children; } @@ -93,6 +101,16 @@ private void init(TagImpl parent) { traverseParentChain(null); } + private BookmarkImpl initBookmark() { + if (parent != null) { + assert parent.bookmark != null; + AtomicInteger parentChildCount = parent.childCounts.computeIfAbsent(name, k -> new AtomicInteger()); + return BookmarkImpl.from(parent.bookmark, name, parentChildCount.getAndIncrement()); + } else { + return BookmarkImpl.from(null, name, 0); + } + } + @Override public Map getAttributes() { if (attributes == null) { @@ -122,9 +140,7 @@ public boolean isNamed(String name) { */ @Override public boolean isNamed(TagName name) { - return Objects.equals(name.getLocal(), this.name.getLocal()) - && Objects.equals(name.getNsUri(), this.name.getNsUri()) - ; + return Objects.equals(name, this.name); } @Override @@ -132,6 +148,22 @@ public boolean isGenerated() { return generated; } + @Override + public boolean isBookmarked(Bookmark bookmark) { + Objects.requireNonNull(bookmark, "null bookmark provided"); + return this.bookmark.equals(bookmark); + } + + @Override + public boolean isBookmarked(List bookmarks) { + for (Bookmark bookmark : bookmarks) { + if (isBookmarked(bookmark)) { + return true; + } + } + return false; + } + @Override public TagName getName() { return name; @@ -169,6 +201,19 @@ public Tag getAncestor(TagName name) { return null; } + @Override + public Bookmark bookmark() { + return bookmark; + } + + /*package*/ BookmarkImpl getBookmark() { + return bookmark; + } + + /*package*/ void bookmarkWrittenAs(String newPath) { + bookmark.update(newPath); + } + @Override public TagImpl addChild(String name) { return addChild(this.name.inheritNamespace(name)); diff --git a/src/main/java/com/github/olivergondza/saxeed/internal/TransformationHandler.java b/src/main/java/com/github/olivergondza/saxeed/internal/TransformationHandler.java index e07dd85..b034454 100644 --- a/src/main/java/com/github/olivergondza/saxeed/internal/TransformationHandler.java +++ b/src/main/java/com/github/olivergondza/saxeed/internal/TransformationHandler.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -40,9 +41,12 @@ public class TransformationHandler extends DefaultHandler implements AutoCloseab private TagImpl currentTag; private final CharChunk currentChars = new CharChunk(); - private LinkedHashMap currentNsMapping = new LinkedHashMap<>(); - private Map documentNamespaces = new HashMap<>(); + private final LinkedHashMap currentNsMapping = new LinkedHashMap<>(); + + private final Map documentNamespaces = new HashMap<>(); + + private final Map writtenBookmarks = new HashMap<>(); public TransformationHandler( LinkedHashMap visitors, @@ -61,22 +65,30 @@ public void startPrefixMapping(String prefix, String uri) { } @Override - public void startElement(String uri, String localName, String tagName, Attributes attributes) { - TagName tagname = TagName.fromSaxArgs(uri, localName, tagName); - currentTag = new TagImpl(currentTag, tagname, attributes, currentNsMapping); + public void startElement(String uri, String localName, String qName, Attributes attributes) { + TagName tagName = TagName.fromSaxArgs(uri, localName, qName); + TagImpl parent = currentTag; + + currentTag = new TagImpl(parent, tagName, attributes, currentNsMapping); currentNsMapping.clear(); _startElement(currentTag); } private void _startElement(TagImpl tag) { - if (tag.isOmitted()) return; + if (tag.isOmitted()) { + tag.getBookmark().omit(); + return; + } TagName name = tag.getName(); for (UpdatingVisitor v : getVisitors(name)) { v.startTag(tag); - if (tag.isOmitted()) return; + if (tag.isOmitted()) { + tag.getBookmark().omit(); + return; + } } TagImpl wrapper = tag.startWrapWith(); @@ -132,12 +144,15 @@ private void _startElement(TagImpl tag) { } LOGGER.fine(">"); + tag.bookmarkWrittenAs(getWriteBookmarkPath(tag)); + writeChildren(tag); } catch (XMLStreamException e) { throw new FailedWriting(ERROR_WRITING_TO_OUTPUT_FILE, e); } } + /** * Write namespace declarations ("xmlns" pseudo-attributes), existing or added */ @@ -147,6 +162,17 @@ private void writeNamespaceDeclarations(TagImpl tag) throws XMLStreamException { } } + private String getWriteBookmarkPath(TagImpl tag) { + TagImpl parent = (TagImpl) tag.getParent(); + + BookmarkImpl parentBookmark = parent == null ? null : parent.getBookmark(); + + String key = BookmarkImpl.pathFrom(parentBookmark, tag.getName(), -1); + AtomicInteger counter = writtenBookmarks.computeIfAbsent(key, k -> new AtomicInteger(0)); + + return BookmarkImpl.pathFrom(parentBookmark, tag.getName(), counter.getAndIncrement()); + } + /** * Get visitors subscribed to given tag name. * diff --git a/src/test/java/com/github/olivergondza/saxeed/BookmarkTest.java b/src/test/java/com/github/olivergondza/saxeed/BookmarkTest.java new file mode 100644 index 0000000..c0f610f --- /dev/null +++ b/src/test/java/com/github/olivergondza/saxeed/BookmarkTest.java @@ -0,0 +1,136 @@ +package com.github.olivergondza.saxeed; + +import com.github.olivergondza.saxeed.ex.FailedTransforming; +import com.github.olivergondza.saxeed.internal.CharChunk; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BookmarkTest { + + @Test + void annotateChars() { + final List bookmarks = new ArrayList<>(); + class Screener implements UpdatingVisitor { + + @Override + public void chars(Tag.Chars tag, CharChunk chars) { + bookmarks.add(tag.bookmark()); + } + } + Screener screener = new Screener(); + + class Updater implements UpdatingVisitor { + @Override + public void startTag(Tag.Start tag) throws FailedTransforming { + if (tag.isBookmarked(bookmarks)) { + tag.getAttributes().put("chars", "true"); + } + } + } + Updater updater = new Updater(); + + String input = "!!!"; + Util.transform(input, screener); + String actual = Util.transform(input, updater); + + String expected = "!!!"; + + assertEquals(expected, actual); + } + + @Test + void generate() { + final List bookmarks = new ArrayList<>(); + class Generator implements UpdatingVisitor { + @Override + public void startTag(Tag.Start tag) throws FailedTransforming { + if (tag.isNamed("e")) { + bookmarks.add(tag.wrapWith("w").bookmark()); + bookmarks.add(tag.addChild("ch").bookmark()); + } + } + } + Generator generator = new Generator(); + + class Reverter implements UpdatingVisitor { + @Override + public void startTag(Tag.Start tag) throws FailedTransforming { + if (tag.isBookmarked(bookmarks)) { + tag.unwrap(); + } + } + } + Reverter reverter = new Reverter(); + + String input = ""; + String actual = Util.transform(input, generator); + + assertEquals("", actual); + assertEquals(6, bookmarks.size()); + + actual = Util.transform(actual, reverter); + + assertEquals(input, actual); + } + + @Test + void invalidateRemoved() { + Map t2b = new HashMap<>(); + class Screener implements UpdatingVisitor { + + @Override + public void startTag(Tag.Start tag) throws FailedTransforming { + t2b.put(tag.getName(), tag.bookmark()); + if (tag.isNamed("del")) { + tag.skip(); + } + } + + @Override + public void endDocument() throws FailedTransforming { + verifyBookmarksInvalidated(t2b); + } + } + Screener screener = new Screener(); + + String first = ""; + String second = Util.transform(first, screener); + assertEquals("", second); + + verifyBookmarksInvalidated(t2b); + + class User implements UpdatingVisitor { + @Override + public void startTag(Tag.Start tag) throws FailedTransforming { + Bookmark bookmarkForSameName = t2b.remove(tag.getName()); + assertFalse(bookmarkForSameName.isOmitted()); + assertTrue(tag.isBookmarked(bookmarkForSameName), bookmarkForSameName.toString()); + } + } + User user = new User(); + Util.transform(second, user); + + assertEquals(1, t2b.size()); + assertTrue(t2b.values().iterator().next().isOmitted()); + } + + private static void verifyBookmarksInvalidated(Map t2b) { + assertEquals(3, t2b.size()); + Map bookmarkValidity = t2b.entrySet().stream() + .collect(Collectors.toMap( + e -> e.getKey().getLocal(), + e -> e.getValue().isOmitted() + ) + ); + assertEquals(Map.of("r", false, "del", true, "keep", false), bookmarkValidity, t2b.toString()); + } +}