|
| 1 | +/* |
| 2 | + * Elemental |
| 3 | + * Copyright (C) 2024, Evolved Binary Ltd |
| 4 | + * |
| 5 | + |
| 6 | + * https://www.evolvedbinary.com | https://www.elemental.xyz |
| 7 | + * |
| 8 | + * This library is free software; you can redistribute it and/or |
| 9 | + * modify it under the terms of the GNU Lesser General Public |
| 10 | + * License as published by the Free Software Foundation; version 2.1. |
| 11 | + * |
| 12 | + * This library is distributed in the hope that it will be useful, |
| 13 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 15 | + * Lesser General Public License for more details. |
| 16 | + * |
| 17 | + * You should have received a copy of the GNU Lesser General Public |
| 18 | + * License along with this library; if not, write to the Free Software |
| 19 | + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
| 20 | + */ |
| 21 | +package org.exist.storage.serializers; |
| 22 | + |
| 23 | +import org.easymock.Capture; |
| 24 | +import org.exist.dom.memtree.MemTreeBuilder; |
| 25 | +import org.exist.util.Configuration; |
| 26 | +import org.exist.util.serializer.SAXSerializer; |
| 27 | +import org.exist.xquery.XPathException; |
| 28 | +import org.exist.xquery.value.*; |
| 29 | +import org.junit.jupiter.params.ParameterizedTest; |
| 30 | +import org.junit.jupiter.params.provider.CsvSource; |
| 31 | +import org.xml.sax.SAXException; |
| 32 | +import org.xmlunit.matchers.CompareMatcher; |
| 33 | + |
| 34 | +import java.io.IOException; |
| 35 | +import java.io.StringWriter; |
| 36 | +import java.io.Writer; |
| 37 | +import java.util.ArrayList; |
| 38 | +import java.util.List; |
| 39 | + |
| 40 | +import static org.easymock.EasyMock.*; |
| 41 | +import static org.exist.Namespaces.EXIST_NS; |
| 42 | +import static org.exist.Namespaces.EXIST_NS_PREFIX; |
| 43 | +import static org.hamcrest.MatcherAssert.assertThat; |
| 44 | +import static org.junit.jupiter.api.Assertions.assertEquals; |
| 45 | + |
| 46 | +public class NativeSerializerTest { |
| 47 | + |
| 48 | + private static final int DEFAULT_START = 1; |
| 49 | + private static final long DEFAULT_COMPILATION_TIME = 0; |
| 50 | + private static final long DEFAULT_EXECUTION_TIME = 0; |
| 51 | + |
| 52 | + public enum Wrapped { |
| 53 | + NOT_WRAPPED, |
| 54 | + WRAPPED; |
| 55 | + } |
| 56 | + |
| 57 | + public enum Typed { |
| 58 | + NOT_TYPED, |
| 59 | + TYPED; |
| 60 | + } |
| 61 | + |
| 62 | + @ParameterizedTest |
| 63 | + @CsvSource({"NOT_WRAPPED,NOT_TYPED", "WRAPPED,NOT_TYPED", "NOT_WRAPPED,TYPED", "WRAPPED,TYPED"}) |
| 64 | + public void serializeInteger(final Wrapped wrapped, final Typed typed) throws SAXException, XPathException, IOException { |
| 65 | + final Sequence sequence = new ValueSequence(); |
| 66 | + sequence.add(new IntegerValue(123)); |
| 67 | + sequence.add(new IntegerValue(456)); |
| 68 | + |
| 69 | + assertSerialize(sequence, wrapped == Wrapped.WRAPPED, typed == Typed.TYPED); |
| 70 | + } |
| 71 | + |
| 72 | + @ParameterizedTest |
| 73 | + @CsvSource({"NOT_WRAPPED,NOT_TYPED", "WRAPPED,NOT_TYPED", "NOT_WRAPPED,TYPED", "WRAPPED,TYPED"}) |
| 74 | + public void serializeText(final Wrapped wrapped, final Typed typed) throws SAXException, XPathException, IOException { |
| 75 | + final MemTreeBuilder builder = new MemTreeBuilder(); |
| 76 | + builder.startDocument(); |
| 77 | + final int text1Id = builder.characters("hello"); |
| 78 | + builder.comment("separator"); |
| 79 | + final int text2Id = builder.characters("world"); |
| 80 | + builder.endDocument(); |
| 81 | + |
| 82 | + final Sequence sequence = new ValueSequence(); |
| 83 | + sequence.add(builder.getDocument().getNode(text1Id)); |
| 84 | + sequence.add(builder.getDocument().getNode(text2Id)); |
| 85 | + |
| 86 | + assertSerialize(sequence, wrapped == Wrapped.WRAPPED, typed == Typed.TYPED); |
| 87 | + } |
| 88 | + |
| 89 | + @ParameterizedTest |
| 90 | + @CsvSource({"NOT_WRAPPED,NOT_TYPED", "WRAPPED,NOT_TYPED", "NOT_WRAPPED,TYPED", "WRAPPED,TYPED"}) |
| 91 | + public void serializeMixed(final Wrapped wrapped, final Typed typed) throws SAXException, XPathException, IOException { |
| 92 | + final MemTreeBuilder builder = new MemTreeBuilder(); |
| 93 | + builder.startDocument(); |
| 94 | + final int text1Id = builder.characters("hello"); |
| 95 | + builder.comment("separator"); |
| 96 | + final int text2Id = builder.characters("world"); |
| 97 | + builder.endDocument(); |
| 98 | + |
| 99 | + final Sequence sequence = new ValueSequence(); |
| 100 | + sequence.add(new IntegerValue(123)); |
| 101 | + sequence.add(builder.getDocument().getNode(text1Id)); |
| 102 | + sequence.add(new StringValue("Some string")); |
| 103 | + sequence.add(builder.getDocument().getNode(text2Id)); |
| 104 | + sequence.add(new StringValue("Another string")); |
| 105 | + |
| 106 | + assertSerialize(sequence, wrapped == Wrapped.WRAPPED, typed == Typed.TYPED); |
| 107 | + } |
| 108 | + |
| 109 | + private void assertSerialize(final Sequence sequence, final boolean wrapped, final boolean typed) throws IOException, SAXException, XPathException { |
| 110 | + final String expected; |
| 111 | + if (wrapped && typed) { |
| 112 | + final List<String> items = type(sequence, false); |
| 113 | + expected = wrap(items); |
| 114 | + } else if (wrapped) { |
| 115 | + final List<String> items = toStrings(sequence); |
| 116 | + expected = wrap(items); |
| 117 | + } else if (typed) { |
| 118 | + final List<String> items = type(sequence, true); |
| 119 | + expected = join(items, ""); |
| 120 | + } else { |
| 121 | + expected = join(sequence, ""); |
| 122 | + } |
| 123 | + |
| 124 | + final String serialized = serialize(sequence, wrapped, typed); |
| 125 | + |
| 126 | + if (wrapped) { |
| 127 | + assertThat(serialized, CompareMatcher.isIdenticalTo(expected)); |
| 128 | + } else if (typed) { |
| 129 | + assertThat(wrapInElement(serialized), CompareMatcher.isIdenticalTo(wrapInElement(expected))); |
| 130 | + } else { |
| 131 | + assertEquals(expected, serialized); |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + private static String join(final List<String> strings, final String separator) { |
| 136 | + return String.join(separator, strings); |
| 137 | + } |
| 138 | + |
| 139 | + private static String join(final Sequence sequence, final String separator) throws XPathException { |
| 140 | + return join(toStrings(sequence), separator); |
| 141 | + } |
| 142 | + |
| 143 | + private static List<String> toStrings(final Sequence sequence) throws XPathException { |
| 144 | + final List<String> strings = new ArrayList<>(sequence.getItemCount()); |
| 145 | + for (final SequenceIterator it = sequence.iterate(); it.hasNext();) { |
| 146 | + final Item item = it.nextItem(); |
| 147 | + strings.add(item.getStringValue()); |
| 148 | + } |
| 149 | + return strings; |
| 150 | + } |
| 151 | + |
| 152 | + private static String wrapInElement(final String string) { |
| 153 | + return "<stub-element>" + string + "</stub-element>"; |
| 154 | + } |
| 155 | + |
| 156 | + private static List<String> type(final Sequence sequence, final boolean explicitNamespace) throws XPathException { |
| 157 | + final String namespace; |
| 158 | + if (explicitNamespace) { |
| 159 | + namespace = " xmlns:" + EXIST_NS_PREFIX + "=\"" + EXIST_NS + "\""; |
| 160 | + } else { |
| 161 | + namespace = ""; |
| 162 | + } |
| 163 | + |
| 164 | + final List<String> typed = new ArrayList<>(sequence.getItemCount()); |
| 165 | + for (final SequenceIterator it = sequence.iterate(); it.hasNext();) { |
| 166 | + final Item item = it.nextItem(); |
| 167 | + if (item.getType() == Type.TEXT) { |
| 168 | + typed.add("<exist:text" + namespace + ">" + item.getStringValue() + "</exist:text>"); |
| 169 | + } else { |
| 170 | + typed.add("<exist:value" + namespace + " exist:type=\"" + Type.getTypeName(item.getType()) + "\">" + item.getStringValue() + "</exist:value>"); |
| 171 | + } |
| 172 | + } |
| 173 | + return typed; |
| 174 | + } |
| 175 | + |
| 176 | + private static String wrap(final List<String> items) { |
| 177 | + final StringBuilder builder = new StringBuilder() |
| 178 | + .append("<exist:result xmlns:exist=\"http://exist.sourceforge.net/NS/exist\" exist:hits=\"") |
| 179 | + .append(items.size()) |
| 180 | + .append("\" exist:start=\"") |
| 181 | + .append(DEFAULT_START) |
| 182 | + .append("\" exist:count=\"") |
| 183 | + .append(items.size()) |
| 184 | + .append("\" exist:compilation-time=\"") |
| 185 | + .append(DEFAULT_COMPILATION_TIME) |
| 186 | + .append("\" exist:execution-time=\"") |
| 187 | + .append(DEFAULT_EXECUTION_TIME) |
| 188 | + .append("\">"); |
| 189 | + |
| 190 | + for (final String item : items) { |
| 191 | + builder.append(item); |
| 192 | + } |
| 193 | + |
| 194 | + return builder.append("</exist:result>").toString(); |
| 195 | + } |
| 196 | + |
| 197 | + private String serialize(final Sequence sequence, final boolean wrapped, final boolean typed) throws IOException, SAXException { |
| 198 | + final int count = sequence.getItemCount(); |
| 199 | + |
| 200 | + final Configuration mockConfiguration = mock(Configuration.class); |
| 201 | + expect(mockConfiguration.getProperty(anyString())).andReturn(null).anyTimes(); |
| 202 | + final Capture<Object> capturePropertyDefault = newCapture(); |
| 203 | + expect(mockConfiguration.getProperty(anyString(), capture(capturePropertyDefault))).andAnswer(() -> capturePropertyDefault.getValue()); |
| 204 | + |
| 205 | + replay(mockConfiguration); |
| 206 | + |
| 207 | + final Serializer serializer = new NativeSerializer(null, mockConfiguration); |
| 208 | + try (final Writer writer = new StringWriter()) { |
| 209 | + final SAXSerializer saxSerializer = new SAXSerializer(writer, null); |
| 210 | + serializer.setSAXHandlers(saxSerializer, saxSerializer); |
| 211 | + serializer.toSAX(sequence, DEFAULT_START, count, wrapped, typed, DEFAULT_COMPILATION_TIME, DEFAULT_EXECUTION_TIME); |
| 212 | + return writer.toString(); |
| 213 | + |
| 214 | + } finally { |
| 215 | + verify(mockConfiguration); |
| 216 | + } |
| 217 | + } |
| 218 | +} |
0 commit comments