diff --git a/pom.xml b/pom.xml
index 5b25ea1..504962a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,9 @@
bundle
UTF-8
- 4.4.6
+
+ 1.0.3
+ 4.4.8
5.10.0
1.9.10
5.7.2
@@ -39,6 +41,11 @@
smack-xmlparser-stax
${smack.version}
+
+ org.jxmpp
+ jxmpp-stringprep-rocksxmppprecis
+ ${jxmpp.version}
+
org.jitsi
jitsi-utils
diff --git a/src/main/java/org/jitsi/xmpp/extensions/DefaultPacketExtensionProvider.java b/src/main/java/org/jitsi/xmpp/extensions/DefaultPacketExtensionProvider.java
index 0e28581..6cbd895 100644
--- a/src/main/java/org/jitsi/xmpp/extensions/DefaultPacketExtensionProvider.java
+++ b/src/main/java/org/jitsi/xmpp/extensions/DefaultPacketExtensionProvider.java
@@ -109,10 +109,12 @@ public C parse(XmlPullParser parser, int depth, XmlEnvironment xmlEnvironment)
namespace = parser.getNamespace();
if (logger.isLoggable(Level.FINEST))
+ {
logger.finest("Will parse event " + eventType
- + " for " + elementName
- + " ns=" + namespace
- + " class=" + packetExtension.getClass().getSimpleName());
+ + " for " + elementName
+ + " ns=" + namespace
+ + " class=" + packetExtension.getClass().getSimpleName());
+ }
if (eventType == XmlPullParser.Event.START_ELEMENT)
{
@@ -122,7 +124,7 @@ public C parse(XmlPullParser parser, int depth, XmlEnvironment xmlEnvironment)
if (provider == null)
{
//we don't know how to handle this kind of extensions.
- logger.fine("Could not add a provider for element "
+ logger.fine("Could not find a provider for element "
+ elementName + " from namespace " + namespace);
}
else
diff --git a/src/main/kotlin/org/jitsi/xmpp/Smack.kt b/src/main/kotlin/org/jitsi/xmpp/Smack.kt
new file mode 100644
index 0000000..8b1d9ad
--- /dev/null
+++ b/src/main/kotlin/org/jitsi/xmpp/Smack.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright @ 2024 - present 8x8, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jitsi.xmpp
+
+import org.jitsi.utils.logging2.createLogger
+import org.jitsi.xmpp.stringprep.JitsiXmppStringprep
+import org.jivesoftware.smack.SmackConfiguration
+import org.jivesoftware.smack.parsing.ExceptionLoggingCallback
+import org.jivesoftware.smackx.bytestreams.socks5.Socks5Proxy
+import org.jxmpp.JxmppContext
+import org.jxmpp.jid.impl.JidCreate
+
+object Smack {
+ val logger = createLogger()
+
+ fun initialize(useJitsiXmppStringprep: Boolean = true) {
+ logger.info("Setting XML parsing limits.")
+ System.setProperty("jdk.xml.entityExpansionLimit", "0")
+ System.setProperty("jdk.xml.maxOccurLimit", "0")
+ System.setProperty("jdk.xml.elementAttributeLimit", "524288")
+ System.setProperty("jdk.xml.totalEntitySizeLimit", "0")
+ System.setProperty("jdk.xml.maxXMLNameLimit", "524288")
+ System.setProperty("jdk.xml.entityReplacementLimit", "0")
+
+ if (useJitsiXmppStringprep) {
+ // Force XmppStringPrepUtil to load before we override the context, otherwise it gets reverted.
+ // https://github.com/igniterealtime/jxmpp/pull/44
+ JidCreate.from("example")
+ logger.info("Using JitsiXmppStringprep.")
+ JxmppContext.setDefaultXmppStringprep(JitsiXmppStringprep.INSTANCE)
+ }
+
+ // if there is a parsing error, do not break the connection to the server(the default behaviour) as we need
+ // it for the other conferences.
+ SmackConfiguration.setDefaultParsingExceptionCallback(ExceptionLoggingCallback())
+ Socks5Proxy.setLocalSocks5ProxyEnabled(false)
+ }
+}
diff --git a/src/main/kotlin/org/jitsi/xmpp/stringprep/JitsiXmppStringprep.kt b/src/main/kotlin/org/jitsi/xmpp/stringprep/JitsiXmppStringprep.kt
new file mode 100644
index 0000000..9ca7be1
--- /dev/null
+++ b/src/main/kotlin/org/jitsi/xmpp/stringprep/JitsiXmppStringprep.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright @ 2024 - present 8x8, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jitsi.xmpp.stringprep
+
+import org.jxmpp.stringprep.XmppStringprep
+import org.jxmpp.stringprep.XmppStringprepException
+import org.jxmpp.stringprep.rocksxmppprecis.RocksXmppPrecisStringprep
+import rocks.xmpp.precis.PrecisProfile
+import java.net.IDN
+import java.text.Normalizer
+import java.util.regex.Pattern
+
+/**
+ * Extends [RocksXmppPrecisStringprep] to allow underscores (_) in the domain part.
+ *
+ * This is needed because jitsi-meet URLs of the form https://domain/tenant/room get translated into a JID of the
+ * form room@tenant.conference.domain, and the tenant field has been allowed to use _ and % for a long time (in
+ * fact '.' in the tenant is translated into '_', while unicode characters get url encoded into e.g. %c3%9f).
+ */
+class JitsiXmppStringprep : XmppStringprep by RocksXmppPrecisStringprep.INSTANCE {
+ override fun domainprep(string: String?): String {
+ try {
+ return idnWithUnderscoreProfile.enforce(string)
+ } catch (e: IllegalArgumentException) {
+ throw XmppStringprepException(string, e)
+ }
+ }
+
+ companion object {
+ val INSTANCE = JitsiXmppStringprep()
+ private val idnWithUnderscoreProfile = IDNWithUnderscoreProfile()
+ }
+}
+
+/**
+ * Based on [PrecisProfiles.IDN], but allows underscores.
+ */
+class IDNWithUnderscoreProfile : PrecisProfile(false) {
+ override fun prepare(input: CharSequence): String {
+ // We're calling toASCII and toUnicode without the [IDN.USE_STD3_ASCII_RULES] flag, so we have to do the
+ // (relaxed) verification.
+ val ascii = verifyLDHUP(IDN.toASCII(input.toString()))
+ return verifyLDHUP(IDN.toUnicode(ascii))
+ }
+
+ /**
+ * Assert that, after splitting [s] into labels separated, each label:
+ * -- Is not empty.
+ * -- All ASCII characters are Letters/Digits/Hyphen/Underscore/Percent.
+ * -- Does not begin or end with a hyphen.
+ *
+ * Based on the implementation in java's IDN, but relaxed to accept _ and % as part of a label.
+ *
+ * @throws IllegalStateException if any of the assertions fail.
+ */
+ private fun verifyLDHUP(s: String) = s.also {
+ val dest = StringBuffer(s)
+ require(dest.isNotEmpty()) { "Empty label is not a legal name" }
+
+ for (i in s.indices) {
+ require(!dest[i].code.isNonLDHUPAsciiCodePoint()) { "Contains non-LDHUP ASCII characters: ${dest[i]}" }
+ if (dest[i].isLabelSeparator()) {
+ require(i != 0) { "Empty label is not a legal name" }
+ require(dest[i - 1] != '-') { "Label has trailing hyphen" }
+ require(!dest[i - 1].isLabelSeparator()) { "Empty label is not a legal name" }
+ require(i == dest.length - 1 || dest[i + 1] != '-') { "Label has leading hyphen" }
+ require(i == dest.length - 1 || !dest[i + 1].isLabelSeparator()) { "Empty label" }
+ }
+ }
+ require(dest[0] != '-' && dest[dest.length - 1] != '-') { "Has leading or trailing hyphen" }
+ }
+
+ override fun applyWidthMappingRule(charSequence: CharSequence) = widthMap(charSequence)
+ override fun applyAdditionalMappingRule(charSequence: CharSequence) =
+ LABEL_SEPARATOR.matcher(charSequence).replaceAll(".")
+ override fun applyCaseMappingRule(charSequence: CharSequence) = charSequence.toString().lowercase()
+
+ override fun applyNormalizationRule(charSequence: CharSequence) =
+ Normalizer.normalize(charSequence, Normalizer.Form.NFC)
+
+ override fun applyDirectionalityRule(charSequence: CharSequence) = charSequence
+
+ companion object {
+ private val dots = listOf('.', '\u3002', '\uFF0E', '\uFF61').toCharArray()
+ private val LABEL_SEPARATOR = Pattern.compile("[${dots.joinToString(separator = "")}]")
+
+ private fun Char.isLabelSeparator() = dots.contains(this)
+
+ /**
+ * Return true if [this] is a code for an ASCII character that is not a Letter/Digit/Hyphen/Underscore/Percent.
+ */
+ private fun Int.isNonLDHUPAsciiCodePoint(): Boolean {
+ return (this in 0x0000..0x0024) ||
+ (this in 0x0026..0x002C) ||
+ (this == 0x002F) ||
+ (this in 0x003A..0x0040) ||
+ (this in 0x005B..0x005e) ||
+ (this == 0x0060) ||
+ (this in 0x007B..0x007F)
+ }
+ }
+}
diff --git a/src/test/kotlin/org/jitsi/xmpp/JidTest.kt b/src/test/kotlin/org/jitsi/xmpp/JidTest.kt
new file mode 100644
index 0000000..527efe0
--- /dev/null
+++ b/src/test/kotlin/org/jitsi/xmpp/JidTest.kt
@@ -0,0 +1,256 @@
+/*
+ * Jicofo, the Jitsi Conference Focus.
+ *
+ * Copyright @ 2024-Present 8x8, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jitsi.xmpp
+
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.assertions.withClue
+import io.kotest.core.spec.IsolationMode
+import io.kotest.core.spec.style.ShouldSpec
+import io.kotest.core.test.TestCase
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.shouldNotBe
+import io.kotest.matchers.types.shouldBeInstanceOf
+import org.jitsi.xmpp.stringprep.IDNWithUnderscoreProfile
+import org.jitsi.xmpp.stringprep.JitsiXmppStringprep
+import org.jxmpp.JxmppContext
+import org.jxmpp.jid.impl.JidCreate
+import org.jxmpp.stringprep.XmppStringprepException
+
+/**
+ * Test JID parsing. The lists below are based on the jxmpp corpora here, plus a couple additional ones:
+ * https://github.com/igniterealtime/jxmpp/tree/master/jxmpp-strings-testframework/src/main/resources/xmpp-strings/jids/valid/main
+ * https://github.com/igniterealtime/jxmpp/blob/master/jxmpp-strings-testframework/src/main/resources/xmpp-strings/jids/invalid/main
+ */
+class JidTest : ShouldSpec() {
+ override fun isolationMode(): IsolationMode {
+ return IsolationMode.SingleInstance
+ }
+ override suspend fun beforeAny(testCase: TestCase) {
+ super.beforeAny(testCase)
+ Smack.initialize()
+ }
+
+ init {
+ context("Parsing valid JIDs") {
+ JxmppContext.getDefaultContext().xmppStringprep.shouldBeInstanceOf()
+ validJids.forEach {
+ withClue(it) {
+ JidCreate.from(it) shouldNotBe null
+ }
+ }
+ }
+ context("Parsing invalid JIDs") {
+ JxmppContext.getDefaultContext().xmppStringprep.shouldBeInstanceOf()
+ invalidJids.forEach {
+ withClue(it) {
+ shouldThrow {
+ JidCreate.from((it))
+ }
+ }
+ }
+ }
+ context("Parsing internationalized domains") {
+ val idnWithUnderscoreProfile = IDNWithUnderscoreProfile()
+
+ idns.forEach { idnGroup ->
+ idnGroup.forEach { idn ->
+ withClue(idn) {
+ // prepare() doesn't always normalize, but it shouldn't throw an exception.
+ idnWithUnderscoreProfile.prepare(idn)
+
+ idnWithUnderscoreProfile.enforce(idn) shouldBe idnGroup[0]
+ JidCreate.from(idn).toString() shouldBe idnGroup[0]
+ }
+ }
+ }
+ }
+ context("Parsing invalid domains") {
+ val idnWithUnderscoreProfile = IDNWithUnderscoreProfile()
+
+ invalidIdns.forEach { idn ->
+ withClue(idn) {
+ shouldThrow {
+ idnWithUnderscoreProfile.prepare(idn)
+ }
+ shouldThrow {
+ idnWithUnderscoreProfile.enforce(idn)
+ }
+ }
+ }
+ }
+ context("JIDs with IDNs") {
+ listOf("user", "юзер", "π", "测试").forEach { username ->
+ listOf("resource", "ресурс", "🍺").forEach { resource ->
+ idns.forEach { idnGroup ->
+ idnGroup.forEach { idn ->
+ val str = "$username@$idn/$resource"
+ withClue(str) {
+ val jid = JidCreate.from(str)
+ jid.resourceOrNull.toString() shouldBe resource
+ jid.localpartOrNull.toString() shouldBe username
+ jid.domain.toString() shouldBe idnGroup[0]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Valid internationalized domain names. The first entry in each group is the normalized form that we expect, the other
+ * entries are other encodings of the same domain.
+ *
+ * https://www.iana.org/domains/reserved
+ */
+val idns = listOf(
+ listOf("إختبار", "XN--KGBECHTV", "xn--KGBEchtv"),
+ listOf("آزمایشی", "XN--HGBK6AJ7F53BBA", "xn--hgbK6AJ7F53BBA"),
+ listOf("测试", "XN--0ZWM56D", "Xn--0Zwm56D"),
+ listOf("測試", "XN--G6W251D", "Xn--G6W251d"),
+ listOf("испытание", "XN--80AKHBYKNJ4F", "XN--80AKHBYKNJ4f", "испытаНИЕ"),
+ listOf("испыта-ние", "испыта-НИЕ", "xn----7sbqjc3alpk3g"),
+ listOf("abc-испытание-def", "ABc-испытаНИЕ-DeF", "xn--abc--def-46g4c5ab8d0a3ar6m"),
+ listOf("испытание.com", "XN--80AKHBYKNJ4F\u3002com", "XN--80AKHBYKNJ4f\uFF0Ecom", "испытаНИЕ\uFF61com"),
+ listOf("испыта-ние.com", "испыта-НИЕ\u3002com", "xn----7sbqjc3alpk3g\uFF0Ecom", "XN----7SBQJC3ALPK3G\uFF61com"),
+ listOf(
+ "abc-испытание-def.com",
+ "ABc-испытаНИЕ-DeF\u3002com",
+ "xn--abc--def-46g4c5ab8d0a3ar6m\uFF0Ecom",
+ "ABc-испытаНИЕ-DeF\uFF61com"
+ ),
+ listOf("abc.испытание-def.com", "ABc.испытаНИЕ-DeF\u3002com", "ABc.испытаНИЕ-DeF\uFF61com"),
+ listOf("परीक्षा", "XN--11B5BS3A9AJ6G", "xn--11B5bs3A9AJ6G"),
+ listOf("δοκιμή", "XN--JXALPDLP"),
+ listOf("테스트", "XN--9T4B11YI5A"),
+ listOf("טעסט", "XN--DEBA0AD"),
+ listOf("テスト", "XN--ZCKZAH"),
+ listOf("பரிட்சை", "XN--HLCJ6AYA9ESC7A"),
+ listOf("bücher.de", "xn--bcher-kva.de"),
+ listOf("büchxr.de", "xn--bchxr-kva.de"),
+ listOf("büch_r.de", "xn--bch_r-kva.de"),
+ listOf("buch_ü"),
+ listOf("__б__"),
+ listOf("büch%r.de", "xn--bch%r-kva.de"),
+ listOf("buch%ü"),
+ listOf("_%б%_"),
+ // IDNA2003 converts this to "fussball", while IDNA2008 leaves the "ß" as is. OpenJDK 11, 17, 21 implement the older
+ // standard. This is here to document the behavior and alert if it changes.
+ listOf("fussball", "fußball")
+)
+
+val invalidIdns = listOf(
+ // Invalid ascii characters
+ "buch?r",
+ "büch?r",
+ "büch[r",
+ // Leading hyphens
+ "-bücher",
+ "-bücher.com",
+ "sub.-bücher.com",
+ "sub\u3002-bücher\uFF61com",
+ "sub\uFF0E-bücher.com",
+ "sub\uFF61-bücher\uFF61com",
+ // Trailing hyphens
+ "bücher-",
+ "bücher-.com",
+ "sub-.bücher.com",
+ "bücher-\u3002com",
+ "bücher-\uFF0Ecom",
+ "bücher-\uFF61com",
+ // Empty labels
+ "example..com",
+ "example\uFF0E\u3002com",
+ "example.com..",
+ "example.com...",
+ "example\uFF61com\uFF0E\u3002",
+ "\u3002\uFF61example.com",
+ ".example.com",
+)
+
+val validJids = listOf(
+ "juliet@example.com",
+ "juliet@example.com/foo",
+ "juliet@example.com/foo bar",
+ "juliet@example.com/foo@bar",
+ "foo\\20bar@example.com",
+ "foo%bar@example.com/f%b",
+ "fussball@example.com",
+ "fußball@example.com",
+ "π@example.com",
+ "Σ@example.com",
+ "ς@example.com",
+ "king@example.com/♚",
+ "example.com",
+ "example.com/foobar",
+ "a.example.com/b@example.net",
+ "server/resource@foo",
+ "server/resource@foo/bar",
+ "user@CaSe-InSeNsItIvE",
+ "user@192.168.1.1",
+ "long-conference-name-1245c711a15e466687b6333577d83e0b@" +
+ "conference.vpaas-magic-cookie-a32a0c3311ee432eab711fa1fdf34793.8x8.vc",
+ "user@example.org/🍺",
+ // These are not valid according to the XMPP spec, but we accept them intentionally.
+ "do_main.com",
+ "u_s_e_r@_do_main_.com",
+ "user@do_ma-in.com",
+ "do%main.com",
+ "u%s%e%r@%do%main%.com",
+ "user@do%ma-in.com"
+)
+
+val invalidJids = listOf(
+ "jul\u0001iet@example.c",
+ "\"juliet\"@example.com",
+ "foo bar@example.com",
+ // This fails due to a corner case in JidCreate when "example.com" is already cached as a DomainpartJid
+ // "@example.com/",
+ "henryⅣ@example.com",
+ "♚@example.com",
+ "juliet@",
+ "/foobar",
+ "node@/server",
+ "@server",
+ "@server/resource",
+ "@/resource",
+ "@/",
+ "/",
+ "@",
+ "user@",
+ "user@@",
+ "user@@host",
+ "user@@host/resource",
+ "user@@host/",
+ "xsf@muc.xmpp.org/x",
+ "username@example.org@example.org",
+ "foo\u0000bar@example.org",
+ "foobar@ex\u0000ample.org",
+ // Leading - in domain part.
+ "user@-do-main.com",
+ // Trailing - in domain part.
+ "user@do-main-.com",
+ "user@conference..example.org",
+ // These are VALID according to the XMPP spec (see the valid corpus), but we currently do not accept them.
+ // [ is an ASSCI symbol that's not allowed in domain names.
+ "user@[2001:638:a000:4134::ffff:40]",
+ "user@[2001:638:a000:4134::ffff:40%eno1]",
+ // A single label in the domain part is limited to 63
+ "user@averylongdomainpartisstillvalideventhoughitexceedsthesixtyfourbytelimitofdnslabels",
+)
diff --git a/src/test/kotlin/org/jitsi/xmpp/extensions/XmlStringBuilderPerfTest.kt b/src/test/kotlin/org/jitsi/xmpp/extensions/XmlStringBuilderPerfTest.kt
new file mode 100644
index 0000000..ab863ad
--- /dev/null
+++ b/src/test/kotlin/org/jitsi/xmpp/extensions/XmlStringBuilderPerfTest.kt
@@ -0,0 +1,114 @@
+/*
+ * Jicofo, the Jitsi Conference Focus.
+ *
+ * Copyright @ 2024-Present 8x8, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jitsi.xmpp.extensions
+
+import io.kotest.core.spec.style.ShouldSpec
+import org.jitsi.utils.logging2.createLogger
+import org.jivesoftware.smack.util.Supplier
+import org.jivesoftware.smack.util.XmlStringBuilder
+
+/**
+ * A test for the performance of XmlStringBuilder serialization. See https://github.com/igniterealtime/Smack/pull/569
+ */
+class XmlStringBuilderPerfTest : ShouldSpec() {
+ val countOuter: Int = 500
+ val countInner: Int = 50
+ val logger = createLogger()
+
+ init {
+ xcontext("XmlStringBuilder.toString() performance") {
+ test1()
+ test2()
+ test3()
+ }
+ }
+
+ private fun test1() {
+ logger.info("Test 1")
+ val parent = XmlStringBuilder()
+ val child = XmlStringBuilder()
+ val child2 = XmlStringBuilder()
+
+ for (i in 1 until countOuter) {
+ val cs = XmlStringBuilder()
+ for (j in 0 until countInner) {
+ cs.append("abc")
+ }
+ child2.append(cs as CharSequence)
+ }
+
+ child.append(child2 as CharSequence)
+ parent.append(child as CharSequence)
+
+ time("test1: parent") { "len=" + parent.toString().length }
+ time("test1: child") { "len=" + child.toString().length }
+ time("test1: child2") { "len=" + child2.toString().length }
+ }
+
+ private fun test2() {
+ logger.info("Test 2: evaluate children first")
+ val parent = XmlStringBuilder()
+ val child = XmlStringBuilder()
+ val child2 = XmlStringBuilder()
+
+ for (i in 1 until countOuter) {
+ val cs = XmlStringBuilder()
+ for (j in 0 until countInner) {
+ cs.append("abc")
+ }
+ child2.append(cs as CharSequence)
+ }
+
+ child.append(child2 as CharSequence)
+ parent.append(child as CharSequence)
+
+ time("test2: child2") { "len=" + child2.toString().length }
+ time("test2: child") { "len=" + child.toString().length }
+ time("test2: parent") { "len=" + parent.toString().length }
+ }
+
+ private fun test3() {
+ logger.info("Test 3: use append(XmlStringBuilder)")
+ val parent = XmlStringBuilder()
+ val child = XmlStringBuilder()
+ val child2 = XmlStringBuilder()
+
+ for (i in 1 until countOuter) {
+ val cs = XmlStringBuilder()
+ for (j in 0 until countInner) {
+ cs.append("abc")
+ }
+ child2.append(cs)
+ }
+
+ child.append(child2)
+ parent.append(child)
+
+ time("test3: parent") { "len=" + parent.toString().length }
+ time("test3: child") { "len=" + child.toString().length }
+ time("test3: child2") { "len=" + child2.toString().length }
+ }
+
+ fun time(name: String, block: Supplier) {
+ val start = System.currentTimeMillis()
+ val result = block.get()
+ val end = System.currentTimeMillis()
+
+ logger.info(name + " took " + (end - start) + "ms: " + result)
+ }
+}