Skip to content

Commit 06f390e

Browse files
authoredDec 5, 2024··
Merge pull request #278 from volodya-lombrozo/277_perfomance
feat(#277): Add `#deepCopy` And `#inner()` Methods Instead of `Node`
2 parents 672d8c6 + 5709e1a commit 06f390e

File tree

7 files changed

+344
-79
lines changed

7 files changed

+344
-79
lines changed
 

‎src/main/java/com/jcabi/xml/DomParser.java

+125-11
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import com.jcabi.log.Logger;
3333
import java.io.ByteArrayInputStream;
34+
import java.io.File;
3435
import java.io.IOException;
3536
import java.nio.charset.StandardCharsets;
3637
import javax.xml.parsers.DocumentBuilder;
@@ -49,18 +50,18 @@
4950
* @since 0.1
5051
*/
5152
@ToString
52-
@EqualsAndHashCode(of = "xml")
53+
@EqualsAndHashCode
5354
final class DomParser {
5455

5556
/**
56-
* The XML as a text.
57+
* Document builder factory to use for parsing.
5758
*/
58-
private final transient byte[] xml;
59+
private final transient DocumentBuilderFactory factory;
5960

6061
/**
61-
* Document builder factory to use for parsing.
62+
* Source of XML.
6263
*/
63-
private final transient DocumentBuilderFactory factory;
64+
private final DocSource source;
6465

6566
/**
6667
* Public ctor.
@@ -76,7 +77,7 @@ final class DomParser {
7677
* @param txt The XML in text (in UTF-8)
7778
*/
7879
DomParser(final DocumentBuilderFactory fct, final String txt) {
79-
this(fct, txt.getBytes(StandardCharsets.UTF_8));
80+
this(fct, new BytesSource(txt));
8081
}
8182

8283
/**
@@ -92,12 +93,36 @@ final class DomParser {
9293
*/
9394
@SuppressWarnings("PMD.ArrayIsStoredDirectly")
9495
DomParser(final DocumentBuilderFactory fct, final byte[] bytes) {
95-
this.xml = bytes;
96-
this.factory = fct;
96+
this(fct, new BytesSource(bytes));
9797
}
9898

9999
/**
100-
* Get document of body.
100+
* Public ctor.
101+
*
102+
* <p>An {@link IllegalArgumentException} may be thrown if the parameter
103+
* passed is not in XML format. It doesn't perform a strict validation
104+
* and is not guaranteed that an exception will be thrown whenever
105+
* the parameter is not XML.
106+
*
107+
* @param fct Document builder factory to use
108+
* @param file The XML as a file
109+
*/
110+
DomParser(final DocumentBuilderFactory fct, final File file) {
111+
this(fct, new FileSource(file));
112+
}
113+
114+
/**
115+
* Private ctor.
116+
* @param factory Document builder factory to use
117+
* @param source Source of XML
118+
*/
119+
private DomParser(final DocumentBuilderFactory factory, final DocSource source) {
120+
this.factory = factory;
121+
this.source = source;
122+
}
123+
124+
/**
125+
* Get the document body.
101126
* @return The document
102127
*/
103128
public Document document() {
@@ -116,7 +141,7 @@ public Document document() {
116141
final long start = System.nanoTime();
117142
final Document doc;
118143
try {
119-
doc = builder.parse(new ByteArrayInputStream(this.xml));
144+
doc = this.source.apply(builder);
120145
} catch (final IOException | SAXException ex) {
121146
throw new IllegalArgumentException(
122147
String.format(
@@ -131,11 +156,100 @@ public Document document() {
131156
this,
132157
"%s parsed %d bytes of XML in %[nano]s",
133158
builder.getClass().getName(),
134-
this.xml.length,
159+
this.source.length(),
135160
System.nanoTime() - start
136161
);
137162
}
138163
return doc;
139164
}
140165

166+
/**
167+
* Source of XML.
168+
* @since 0.32
169+
*/
170+
private interface DocSource {
171+
172+
/**
173+
* Parse XML by the builder.
174+
* @param builder The builder to use during parsing.
175+
* @return The document.
176+
* @throws IOException If fails.
177+
* @throws SAXException If fails.
178+
*/
179+
Document apply(DocumentBuilder builder) throws IOException, SAXException;
180+
181+
/**
182+
* The length of the source.
183+
* @return The length.
184+
*/
185+
long length();
186+
}
187+
188+
/**
189+
* File source of XML from a file.
190+
* @since 0.32
191+
*/
192+
private static class FileSource implements DocSource {
193+
194+
/**
195+
* The file.
196+
*/
197+
private final File file;
198+
199+
/**
200+
* Public ctor.
201+
* @param file The file.
202+
*/
203+
FileSource(final File file) {
204+
this.file = file;
205+
}
206+
207+
@Override
208+
public Document apply(final DocumentBuilder builder) throws IOException, SAXException {
209+
return builder.parse(this.file);
210+
}
211+
212+
@Override
213+
public long length() {
214+
return this.file.length();
215+
}
216+
}
217+
218+
/**
219+
* Bytes source of XML.
220+
* @since 0.32
221+
*/
222+
private static class BytesSource implements DocSource {
223+
224+
/**
225+
* Bytes of the XML.
226+
*/
227+
private final byte[] xml;
228+
229+
/**
230+
* Public ctor.
231+
* @param xml Bytes of the XML.
232+
*/
233+
BytesSource(final String xml) {
234+
this(xml.getBytes(StandardCharsets.UTF_8));
235+
}
236+
237+
/**
238+
* Public ctor.
239+
* @param xml Bytes of the XML.
240+
*/
241+
BytesSource(final byte[] xml) {
242+
this.xml = xml;
243+
}
244+
245+
@Override
246+
public Document apply(final DocumentBuilder builder) throws IOException, SAXException {
247+
return builder.parse(new ByteArrayInputStream(this.xml));
248+
}
249+
250+
@Override
251+
public long length() {
252+
return this.xml.length;
253+
}
254+
}
141255
}

‎src/main/java/com/jcabi/xml/SaxonDocument.java

+22-1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
*
6060
* @since 0.28
6161
*/
62+
@SuppressWarnings("PMD.TooManyMethods")
6263
public final class SaxonDocument implements XML {
6364

6465
/**
@@ -199,13 +200,33 @@ public XML merge(final NamespaceContext context) {
199200
);
200201
}
201202

202-
@Override
203+
/**
204+
* Retrieve DOM node, represented by this wrapper.
205+
* This method works exactly the same as {@link #deepCopy()}.
206+
* @deprecated Use {@link #inner()} or {@link #deepCopy()} instead.
207+
* @return Deep copy of the inner DOM node.
208+
*/
209+
@Deprecated
203210
public Node node() {
204211
throw new UnsupportedOperationException(
205212
String.format(SaxonDocument.UNSUPPORTED, "node")
206213
);
207214
}
208215

216+
@Override
217+
public Node inner() {
218+
throw new UnsupportedOperationException(
219+
String.format(SaxonDocument.UNSUPPORTED, "inner")
220+
);
221+
}
222+
223+
@Override
224+
public Node deepCopy() {
225+
throw new UnsupportedOperationException(
226+
String.format(SaxonDocument.UNSUPPORTED, "deepCopy")
227+
);
228+
}
229+
209230
@Override
210231
public Collection<SAXParseException> validate() {
211232
throw new UnsupportedOperationException(

‎src/main/java/com/jcabi/xml/StrictXML.java

+27-7
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,10 @@ public StrictXML(final XML xml, final XML schema) {
108108
* @param errors XML Document errors
109109
*/
110110
@SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors")
111-
private StrictXML(final XML xml,
112-
final Collection<SAXParseException> errors) {
111+
private StrictXML(
112+
final XML xml,
113+
final Collection<SAXParseException> errors
114+
) {
113115
if (!errors.isEmpty()) {
114116
Logger.warn(
115117
StrictXML.class,
@@ -154,9 +156,25 @@ public XML merge(final NamespaceContext context) {
154156
return this.origin.merge(context);
155157
}
156158

157-
@Override
159+
/**
160+
* Retrieve DOM node, represented by this wrapper.
161+
* This method works exactly the same as {@link #deepCopy()}.
162+
* @deprecated Use {@link #inner()} or {@link #deepCopy()} instead.
163+
* @return Deep copy of the inner DOM node.
164+
*/
165+
@Deprecated
158166
public Node node() {
159-
return this.origin.node();
167+
return this.origin.deepCopy();
168+
}
169+
170+
@Override
171+
public Node inner() {
172+
return this.origin.inner();
173+
}
174+
175+
@Override
176+
public Node deepCopy() {
177+
return this.origin.deepCopy();
160178
}
161179

162180
@Override
@@ -185,7 +203,8 @@ private static Collection<SAXParseException> check(final XML xml, final XML xsd)
185203
* @return List of messages to print
186204
*/
187205
private static Iterable<String> print(
188-
final Collection<SAXParseException> errors) {
206+
final Collection<SAXParseException> errors
207+
) {
189208
final Collection<String> lines = new ArrayList<>(errors.size());
190209
for (final SAXParseException error : errors) {
191210
lines.add(StrictXML.asMessage(error));
@@ -248,15 +267,16 @@ private static String join(final Iterable<?> iterable, final String sep) {
248267
*/
249268
private static Collection<SAXParseException> validate(
250269
final XML xml,
251-
final Validator validator) {
270+
final Validator validator
271+
) {
252272
final Collection<SAXParseException> errors =
253273
new CopyOnWriteArrayList<>();
254274
final int max = 3;
255275
try {
256276
validator.setErrorHandler(
257277
new XMLDocument.ValidationHandler(errors)
258278
);
259-
final DOMSource dom = new DOMSource(xml.node());
279+
final DOMSource dom = new DOMSource(xml.inner());
260280
for (int retry = 1; retry <= max; ++retry) {
261281
try {
262282
validator.validate(dom);

‎src/main/java/com/jcabi/xml/XML.java

+21-3
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@
4646
* // ...
4747
* }</pre>
4848
*
49-
* <p>You can always get DOM node out of this abstraction using {@link #node()}
50-
* method.
49+
* <p>You can always get DOM node out of this abstraction using {@link #inner()}
50+
* or {@link #deepCopy()} methods.
5151
*
5252
* <p>{@code toString()} must produce a full XML.
5353
*
@@ -161,10 +161,28 @@ public interface XML {
161161

162162
/**
163163
* Retrieve DOM node, represented by this wrapper.
164-
* @return DOM node
164+
* This method works exactly the same as {@link #deepCopy()}.
165+
* @deprecated Use {@link #inner()} or {@link #deepCopy()} instead.
166+
* @return Deep copy of the inner DOM node.
165167
*/
168+
@Deprecated
166169
Node node();
167170

171+
/**
172+
* Retrieve DOM node, represented by this wrapper.
173+
* Pay attention that this method returns inner node, not a deep copy.
174+
* It means that any changes to the returned node will affect the original XML.
175+
* @return Inner node.
176+
*/
177+
Node inner();
178+
179+
/**
180+
* Retrieve a deep copy of the DOM node, represented by this wrapper.
181+
* Might be expensive in terms of performance.
182+
* @return Deep copy of the node.
183+
*/
184+
Node deepCopy();
185+
168186
/**
169187
* Validate this XML against the XSD schema inside it.
170188
* @return List of errors found

‎src/main/java/com/jcabi/xml/XMLDocument.java

+57-49
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,22 @@ public final class XMLDocument implements XML {
106106
*/
107107
private final transient Node cache;
108108

109+
/**
110+
* Public ctor, from a source.
111+
*
112+
* <p>The object is created with a default implementation of
113+
* {@link NamespaceContext}, which already defines a
114+
* number of namespaces, see {@link XMLDocument#XMLDocument(String)}.
115+
*
116+
* <p>An {@link IllegalArgumentException} is thrown if the parameter
117+
* passed is not in XML format.
118+
*
119+
* @param source Source of XML document
120+
*/
121+
public XMLDocument(final Source source) {
122+
this(XMLDocument.transform(source));
123+
}
124+
109125
/**
110126
* Public ctor, from XML as a text.
111127
*
@@ -125,11 +141,7 @@ public final class XMLDocument implements XML {
125141
* @param text XML document body
126142
*/
127143
public XMLDocument(final String text) {
128-
this(
129-
new DomParser(XMLDocument.configuredDFactory(), text).document(),
130-
new XPathContext(),
131-
false
132-
);
144+
this(new DomParser(XMLDocument.configuredDFactory(), text).document());
133145
}
134146

135147
/**
@@ -151,29 +163,11 @@ public XMLDocument(final String text) {
151163
* @param data The XML body
152164
*/
153165
public XMLDocument(final byte[] data) {
154-
this(
155-
new DomParser(XMLDocument.configuredDFactory(), data).document(),
156-
new XPathContext(),
157-
false
158-
);
166+
this(new DomParser(XMLDocument.configuredDFactory(), data).document());
159167
}
160168

161169
/**
162-
* Public ctor, from a DOM node.
163-
*
164-
* <p>The object is created with a default implementation of
165-
* {@link NamespaceContext}, which already defines a
166-
* number of namespaces, see {@link XMLDocument#XMLDocument(String)}.
167-
*
168-
* @param node DOM source
169-
* @since 0.2
170-
*/
171-
public XMLDocument(final Node node) {
172-
this(node, new XPathContext(), !(node instanceof Document));
173-
}
174-
175-
/**
176-
* Public ctor, from a source.
170+
* Public ctor, from XML in a file.
177171
*
178172
* <p>The object is created with a default implementation of
179173
* {@link NamespaceContext}, which already defines a
@@ -182,10 +176,11 @@ public XMLDocument(final Node node) {
182176
* <p>An {@link IllegalArgumentException} is thrown if the parameter
183177
* passed is not in XML format.
184178
*
185-
* @param source Source of XML document
179+
* @param file XML file
180+
* @throws FileNotFoundException In case of I/O problems
186181
*/
187-
public XMLDocument(final Source source) {
188-
this(XMLDocument.transform(source), new XPathContext(), false);
182+
public XMLDocument(final File file) throws FileNotFoundException {
183+
this(new DomParser(XMLDocument.configuredDFactory(), file).document());
189184
}
190185

191186
/**
@@ -201,12 +196,12 @@ public XMLDocument(final Source source) {
201196
* @param file XML file
202197
* @throws FileNotFoundException In case of I/O problems
203198
*/
204-
public XMLDocument(final File file) throws FileNotFoundException {
205-
this(new TextResource(file).toString());
199+
public XMLDocument(final Path file) throws FileNotFoundException {
200+
this(new DomParser(XMLDocument.configuredDFactory(), file.toFile()).document());
206201
}
207202

208203
/**
209-
* Public ctor, from XML in a file.
204+
* Public ctor, from input stream.
210205
*
211206
* <p>The object is created with a default implementation of
212207
* {@link NamespaceContext}, which already defines a
@@ -215,11 +210,16 @@ public XMLDocument(final File file) throws FileNotFoundException {
215210
* <p>An {@link IllegalArgumentException} is thrown if the parameter
216211
* passed is not in XML format.
217212
*
218-
* @param file XML file
219-
* @throws FileNotFoundException In case of I/O problems
213+
* <p>The provided input stream will be closed automatically after
214+
* getting data from it.
215+
*
216+
* @param stream The input stream, which will be closed automatically
217+
* @throws IOException In case of I/O problem
220218
*/
221-
public XMLDocument(final Path file) throws FileNotFoundException {
222-
this(file.toFile());
219+
@SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors")
220+
public XMLDocument(final InputStream stream) throws IOException {
221+
this(new TextResource(stream).toString());
222+
stream.close();
223223
}
224224

225225
/**
@@ -257,25 +257,17 @@ public XMLDocument(final URI uri) throws IOException {
257257
}
258258

259259
/**
260-
* Public ctor, from input stream.
260+
* Public ctor, from a DOM node.
261261
*
262262
* <p>The object is created with a default implementation of
263263
* {@link NamespaceContext}, which already defines a
264264
* number of namespaces, see {@link XMLDocument#XMLDocument(String)}.
265265
*
266-
* <p>An {@link IllegalArgumentException} is thrown if the parameter
267-
* passed is not in XML format.
268-
*
269-
* <p>The provided input stream will be closed automatically after
270-
* getting data from it.
271-
*
272-
* @param stream The input stream, which will be closed automatically
273-
* @throws IOException In case of I/O problem
266+
* @param node DOM source
267+
* @since 0.2
274268
*/
275-
@SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors")
276-
public XMLDocument(final InputStream stream) throws IOException {
277-
this(new TextResource(stream).toString());
278-
stream.close();
269+
public XMLDocument(final Node node) {
270+
this(node, new XPathContext(), !(node instanceof Document));
279271
}
280272

281273
/**
@@ -316,8 +308,24 @@ public int hashCode() {
316308
return this.cache.hashCode();
317309
}
318310

319-
@Override
311+
/**
312+
* Retrieve DOM node, represented by this wrapper.
313+
* This method works exactly the same as {@link #deepCopy()}.
314+
* @deprecated Use {@link #inner()} or {@link #deepCopy()} instead.
315+
* @return Deep copy of the inner DOM node.
316+
*/
317+
@Deprecated
320318
public Node node() {
319+
return this.deepCopy();
320+
}
321+
322+
@Override
323+
public Node inner() {
324+
return this.cache;
325+
}
326+
327+
@Override
328+
public Node deepCopy() {
321329
final Node casted = this.cache;
322330
final Node answer;
323331
if (casted instanceof Document) {

‎src/main/java/com/jcabi/xml/XSLDocument.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ private void transformInto(final XML xml, final Result result) {
434434
trans.setErrorListener(errors);
435435
final long start = System.nanoTime();
436436
try {
437-
trans.transform(new DOMSource(xml.node()), result);
437+
trans.transform(new DOMSource(xml.inner()), result);
438438
} catch (final TransformerException ex) {
439439
final StringBuilder summary = new StringBuilder(
440440
String.join("; ", errors.summary())

‎src/test/java/com/jcabi/xml/XMLDocumentTest.java

+91-7
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@
3434
import java.io.ByteArrayInputStream;
3535
import java.io.File;
3636
import java.io.IOException;
37+
import java.nio.charset.StandardCharsets;
3738
import java.nio.file.Files;
39+
import java.nio.file.Path;
3840
import java.security.SecureRandom;
3941
import java.util.Arrays;
4042
import java.util.Collection;
@@ -47,6 +49,9 @@
4749
import java.util.concurrent.Executors;
4850
import java.util.concurrent.TimeUnit;
4951
import java.util.concurrent.atomic.AtomicInteger;
52+
import java.util.stream.Collectors;
53+
import java.util.stream.IntStream;
54+
import javax.xml.parsers.DocumentBuilderFactory;
5055
import org.apache.commons.lang3.RandomStringUtils;
5156
import org.apache.commons.lang3.StringUtils;
5257
import org.cactoos.io.ResourceOf;
@@ -57,7 +62,9 @@
5762
import org.hamcrest.Matchers;
5863
import org.hamcrest.core.IsEqual;
5964
import org.junit.jupiter.api.Disabled;
65+
import org.junit.jupiter.api.RepeatedTest;
6066
import org.junit.jupiter.api.Test;
67+
import org.junit.jupiter.api.io.TempDir;
6168
import org.w3c.dom.Document;
6269
import org.w3c.dom.Element;
6370
import org.w3c.dom.Node;
@@ -183,7 +190,7 @@ void printsWithNamespace() {
183190
new XMLDocument(
184191
new XMLDocument(
185192
"<z xmlns:a='hey'><f a:boom='test'/></z>"
186-
).node()
193+
).inner()
187194
).toString(),
188195
Matchers.containsString("a:boom")
189196
);
@@ -195,11 +202,11 @@ void retrievesDomNode() throws Exception {
195202
this.getClass().getResource("simple.xml")
196203
);
197204
MatcherAssert.assertThat(
198-
doc.nodes("/root/simple").get(0).node().getNodeName(),
205+
doc.nodes("/root/simple").get(0).inner().getNodeName(),
199206
Matchers.equalTo("simple")
200207
);
201208
MatcherAssert.assertThat(
202-
doc.nodes("//simple").get(0).node().getNodeType(),
209+
doc.nodes("//simple").get(0).inner().getNodeType(),
203210
Matchers.equalTo(Node.ELEMENT_NODE)
204211
);
205212
}
@@ -385,7 +392,7 @@ void takesNodeInMultipleThreads() throws Exception {
385392
final Runnable runnable = () -> {
386393
try {
387394
MatcherAssert.assertThat(
388-
new XMLDocument(xml.node()).toString(),
395+
new XMLDocument(xml.inner()).toString(),
389396
Matchers.containsString(">5555<")
390397
);
391398
done.incrementAndGet();
@@ -427,11 +434,11 @@ void performsXpathCalculations() {
427434
void buildsDomNode() {
428435
final XML doc = new XMLDocument("<?xml version='1.0'?><f/>");
429436
MatcherAssert.assertThat(
430-
doc.node(),
437+
doc.inner(),
431438
Matchers.instanceOf(Document.class)
432439
);
433440
MatcherAssert.assertThat(
434-
doc.nodes("/f").get(0).node(),
441+
doc.nodes("/f").get(0).inner(),
435442
Matchers.instanceOf(Element.class)
436443
);
437444
}
@@ -479,7 +486,7 @@ void preservesXmlNamespaces() {
479486
@Test
480487
void preservesImmutability() {
481488
final XML xml = new XMLDocument("<r1><a/></r1>");
482-
final Node node = xml.nodes("/r1/a").get(0).node();
489+
final Node node = xml.nodes("/r1/a").get(0).deepCopy();
483490
node.appendChild(node.getOwnerDocument().createElement("h9"));
484491
MatcherAssert.assertThat(
485492
xml,
@@ -670,4 +677,81 @@ void validatesMultipleXmlsInThreads() throws Exception {
670677
service.shutdownNow();
671678
}
672679

680+
/**
681+
* This test is disabled because it is a performance test that might be flaky.
682+
* @param temp Temporary directory.
683+
* @throws IOException If something goes wrong.
684+
*/
685+
@RepeatedTest(10)
686+
@Disabled
687+
void createsXmlFromFile(@TempDir final Path temp) throws IOException {
688+
final Path xml = temp.resolve("test.xml");
689+
Files.write(xml, XMLDocumentTest.large().getBytes(StandardCharsets.UTF_8));
690+
final long clear = XMLDocumentTest.measure(
691+
() -> DocumentBuilderFactory.newInstance()
692+
.newDocumentBuilder()
693+
.parse(xml.toFile())
694+
.getFirstChild()
695+
.getNodeName()
696+
);
697+
final long wrapped = XMLDocumentTest.measure(
698+
() -> new XMLDocument(xml.toFile()).inner().getFirstChild().getNodeName()
699+
);
700+
MatcherAssert.assertThat(
701+
String.format(
702+
"We expect that jcabi-xml is at max 2 times slower than default approach, time spend on jcabi-xml: %d ms, time spend on default approach: %d ms",
703+
wrapped,
704+
clear
705+
),
706+
wrapped / clear,
707+
Matchers.lessThan(2L)
708+
);
709+
}
710+
711+
/**
712+
* Measure the time of execution.
713+
* @param run The callable to run.
714+
* @return Time in milliseconds.
715+
* @checkstyle IllegalCatchCheck (20 lines)
716+
*/
717+
@SuppressWarnings({"PMD.AvoidCatchingGenericException", "PMD.PrematureDeclaration"})
718+
private static long measure(final Callable<String> run) {
719+
final long start = System.nanoTime();
720+
if (!IntStream.range(0, 1000).mapToObj(
721+
each -> {
722+
try {
723+
return run.call();
724+
} catch (final Exception exception) {
725+
throw new IllegalStateException(
726+
String.format("Failed to run %s", run), exception
727+
);
728+
}
729+
}
730+
).allMatch("root"::equals)) {
731+
throw new IllegalStateException("Invalid result");
732+
}
733+
return System.nanoTime() - start / 1_000_000;
734+
}
735+
736+
/**
737+
* Generate large XML for tests.
738+
* @return Large XML string.
739+
*/
740+
private static String large() {
741+
return IntStream.range(0, 100)
742+
.mapToObj(
743+
i -> StringUtils.join(
744+
"<payment><id>333</id>",
745+
"<date>1-Jan-2013</date>",
746+
"<debit>test-1</debit>",
747+
"<credit>test-2</credit>",
748+
"</payment>"
749+
)
750+
).collect(
751+
Collectors.joining(
752+
"", "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>", "</root>"
753+
)
754+
);
755+
}
756+
673757
}

0 commit comments

Comments
 (0)
Please sign in to comment.