├── .editorconfig ├── .travis.yml ├── kotlin-xml-builder ├── test.dtd ├── src │ ├── test │ │ ├── resources │ │ │ └── test-results │ │ │ │ ├── XmlBuilderTest │ │ │ │ ├── emptyRoot.xml │ │ │ │ ├── emptyString.xml │ │ │ │ ├── doctypeSimple.xml │ │ │ │ ├── emptyElement.xml │ │ │ │ ├── removeElement.xml │ │ │ │ ├── updateAttribute.xml │ │ │ │ ├── whitespace.xml │ │ │ │ ├── addElement.xml │ │ │ │ ├── selfClosingTag.xml │ │ │ │ ├── xmlEncode.xml │ │ │ │ ├── cdata.xml │ │ │ │ ├── parseXmlEncode.xml │ │ │ │ ├── replaceElement.xml │ │ │ │ ├── doctypeSystem.xml │ │ │ │ ├── addElementAfter.xml │ │ │ │ ├── noSelfClosingTag.xml │ │ │ │ ├── quoteInAttribute.xml │ │ │ │ ├── addElementBefore.xml │ │ │ │ ├── elementValue.xml │ │ │ │ ├── parseCData.xml │ │ │ │ ├── specialCharInAttribute.xml │ │ │ │ ├── unsafeAttributeValue.xml │ │ │ │ ├── addElementAfterLastChild.xml │ │ │ │ ├── elementAsString.xml │ │ │ │ ├── processingInstruction.xml │ │ │ │ ├── cdataNesting.xml │ │ │ │ ├── elementAsStringWithAttributes.xml │ │ │ │ ├── singleLineCDATAElement.xml │ │ │ │ ├── doctypePublic.xml │ │ │ │ ├── notPrettyFormatting.xml │ │ │ │ ├── zeroSpaceIndentNoPrettyFormatting.xml │ │ │ │ ├── comment.xml │ │ │ │ ├── singleLineTextElement.xml │ │ │ │ ├── elementAsStringWithAttributesAndContent.xml │ │ │ │ ├── singleLineProcessingInstructionElement.xml │ │ │ │ ├── characterReference.xml │ │ │ │ ├── globalProcessingInstructionElement.xml │ │ │ │ ├── zeroSpaceIndent.xml │ │ │ │ ├── multipleAttributes.xml │ │ │ │ ├── parseMultipleAttributes.xml │ │ │ │ ├── singleLineProcessingInstructionElementWithAttributes.xml │ │ │ │ ├── parseCDataWhitespace.xml │ │ │ │ ├── customNamespaces.xml │ │ │ │ ├── parseCustomNamespaces.xml │ │ │ │ ├── basicTest.xml │ │ │ │ ├── parseBasicTest.xml │ │ │ │ └── advancedNamespaces.xml │ │ │ │ ├── OrderedNodesTest │ │ │ │ └── correctOrder.xml │ │ │ │ ├── NodeTest │ │ │ │ ├── addElementsAfter.xml │ │ │ │ └── addElementsBefore.xml │ │ │ │ └── sitemapTest │ │ │ │ ├── allElements.xml │ │ │ │ ├── basicTest.xml │ │ │ │ └── sitemapIndex.xml │ │ └── kotlin │ │ │ └── org │ │ │ └── redundent │ │ │ └── kotlin │ │ │ └── xml │ │ │ ├── OrderedNodesTest.kt │ │ │ ├── UtilsKtTest.kt │ │ │ ├── sitemapTest.kt │ │ │ ├── TextElementTest.kt │ │ │ ├── CommentTest.kt │ │ │ ├── CDATAElementTest.kt │ │ │ ├── ProcessingInstructionElementTest.kt │ │ │ ├── TestBase.kt │ │ │ ├── NodeTest.kt │ │ │ └── XmlBuilderTest.kt │ └── main │ │ └── kotlin │ │ └── org │ │ └── redundent │ │ └── kotlin │ │ └── xml │ │ ├── Unsafe.kt │ │ ├── XmlType.kt │ │ ├── XmlVersion.kt │ │ ├── Attribute.kt │ │ ├── Element.kt │ │ ├── Namespace.kt │ │ ├── Comment.kt │ │ ├── Doctype.kt │ │ ├── ProcessingInstructionElement.kt │ │ ├── CDATAElement.kt │ │ ├── TextElement.kt │ │ ├── Utils.kt │ │ ├── PrintOptions.kt │ │ ├── Sitemap.kt │ │ ├── XmlBuilder.kt │ │ └── Node.kt └── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle.kts ├── kotlin-xml-dsl-generator ├── src │ ├── main │ │ └── kotlin │ │ │ └── org │ │ │ └── redundent │ │ │ └── kotlin │ │ │ └── xml │ │ │ └── gen │ │ │ ├── writer │ │ │ ├── Code.kt │ │ │ ├── XmlAttribute.kt │ │ │ ├── XmlEnum.kt │ │ │ ├── XmlElement.kt │ │ │ ├── XmlClass.kt │ │ │ └── CodeWriter.kt │ │ │ ├── SchemaOutline.kt │ │ │ └── DslGenerator.kt │ └── test │ │ ├── resources │ │ ├── schema │ │ │ ├── string-to-bytearray.xsd │ │ │ ├── referencedType.xsd │ │ │ ├── target-namespace.xsd │ │ │ ├── child-prop-order.xsd │ │ │ ├── group.xsd │ │ │ ├── string-to-bytearray.jxb │ │ │ ├── choice.xsd │ │ │ ├── member-functions.xsd │ │ │ ├── nested.xsd │ │ │ ├── duplicate-names.xsd │ │ │ ├── extension.xsd │ │ │ ├── attributes.xsd │ │ │ ├── attributes-required.xsd │ │ │ └── sitemap.xsd │ │ └── code │ │ │ ├── referencedType.kt │ │ │ ├── group.kt │ │ │ ├── target-namespace.kt │ │ │ ├── string-to-bytearray.kt │ │ │ ├── child-prop-order.kt │ │ │ ├── member-functions.kt │ │ │ ├── duplicate-names.kt │ │ │ ├── choice.kt │ │ │ ├── nested.kt │ │ │ ├── extension.kt │ │ │ ├── sitemap.kt │ │ │ ├── attributes-required.kt │ │ │ └── attributes.kt │ │ └── kotlin │ │ └── org │ │ └── redundent │ │ └── kotlin │ │ └── xml │ │ └── gen │ │ ├── SimpleSchemaGen.kt │ │ └── AbstractGenTest.kt ├── README.md └── build.gradle.kts ├── .github └── workflows │ ├── ci.yml │ └── deploy.yml ├── gradlew.bat ├── gradlew ├── LICENSE └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | indent_style = tab -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 -------------------------------------------------------------------------------- /kotlin-xml-builder/test.dtd: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/emptyRoot.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/emptyString.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/doctypeSimple.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/emptyElement.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/removeElement.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/updateAttribute.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/whitespace.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/addElement.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/selfClosingTag.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/xmlEncode.xml: -------------------------------------------------------------------------------- 1 | 2 | &<> 3 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/cdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/parseXmlEncode.xml: -------------------------------------------------------------------------------- 1 | 2 | &<> 3 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/replaceElement.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/doctypeSystem.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redundent/kotlin-xml-builder/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/OrderedNodesTest/correctOrder.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/addElementAfter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/noSelfClosingTag.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/quoteInAttribute.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/addElementBefore.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/elementValue.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | value 4 | 5 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/parseCData.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/specialCharInAttribute.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/unsafeAttributeValue.xml: -------------------------------------------------------------------------------- 1 | 2 | { 3 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/addElementAfterLastChild.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/elementAsString.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | value 4 | 5 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/main/kotlin/org/redundent/kotlin/xml/Unsafe.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | class Unsafe(val value: Any?) 4 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/processingInstruction.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "kotlin-xml-builder" 2 | 3 | include(":kotlin-xml-dsl-generator") 4 | include(":kotlin-xml-builder") 5 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/cdataNesting.xml: -------------------------------------------------------------------------------- 1 | 2 | ]]> 3 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/elementAsStringWithAttributes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/singleLineCDATAElement.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/doctypePublic.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/notPrettyFormatting.xml: -------------------------------------------------------------------------------- 1 | HelloTest -------------------------------------------------------------------------------- /kotlin-xml-builder/src/main/kotlin/org/redundent/kotlin/xml/XmlType.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | annotation class XmlType(val childOrder: Array) 4 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/zeroSpaceIndentNoPrettyFormatting.xml: -------------------------------------------------------------------------------- 1 | HelloTest -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/comment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | value 5 | 6 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/singleLineTextElement.xml: -------------------------------------------------------------------------------- 1 | 2 | Hello 3 | Test 4 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/elementAsStringWithAttributesAndContent.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Content 4 | 5 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/singleLineProcessingInstructionElement.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/characterReference.xml: -------------------------------------------------------------------------------- 1 | 2 | Hello & Goodbye 3 | Test 4 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/globalProcessingInstructionElement.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/zeroSpaceIndent.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello 4 | 5 | 6 | Test 7 | 8 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/NodeTest/addElementsAfter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/NodeTest/addElementsBefore.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/multipleAttributes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/main/kotlin/org/redundent/kotlin/xml/XmlVersion.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | enum class XmlVersion(val value: String) { 4 | V10("1.0"), 5 | V11("1.1") 6 | } 7 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/parseMultipleAttributes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/main/kotlin/org/redundent/kotlin/xml/gen/writer/Code.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml.gen.writer 2 | 3 | interface Code { 4 | fun write(codeWriter: CodeWriter) 5 | } 6 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/singleLineProcessingInstructionElementWithAttributes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/main/kotlin/org/redundent/kotlin/xml/Attribute.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | data class Attribute @JvmOverloads constructor( 4 | val name: String, 5 | val value: Any, 6 | val namespace: Namespace? = null 7 | ) 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/parseCDataWhitespace.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/customNamespaces.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Test 4 | 5 |

6 | 7 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/parseCustomNamespaces.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Test 4 | 5 |

6 | 7 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/basicTest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://google.com/0 5 | 6 | 7 | 8 | 9 | https://google.com/1 10 | 11 | 12 | 13 | 14 | https://google.com/2 15 | 16 | 17 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/parseBasicTest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | http://google.com/0 5 | 6 | 7 | 8 | 9 | http://google.com/1 10 | 11 | 12 | 13 | 14 | http://google.com/2 15 | 16 | 17 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/sitemapTest/allElements.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | http://blog.redundent.org 5 | 6 | 7 | 2017-10-24 8 | 9 | 10 | hourly 11 | 12 | 13 | 14.0 14 | 15 | 16 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/schema/string-to-bytearray.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/XmlBuilderTest/advancedNamespaces.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/sitemapTest/basicTest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | http://blog.redundent.org/post/1 5 | 6 | 7 | 8 | 9 | http://blog.redundent.org/post/2 10 | 11 | 12 | 13 | 14 | http://blog.redundent.org/post/3 15 | 16 | 17 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/main/kotlin/org/redundent/kotlin/xml/Element.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | /** 4 | * Base interface for all elements. You shouldn't have to interact with this interface directly. 5 | * @author Jason Blackwell 6 | */ 7 | interface Element { 8 | /** 9 | * This method handles creating the XML. Used internally. 10 | */ 11 | fun render(builder: Appendable, indent: String, printOptions: PrintOptions) 12 | } 13 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/schema/referencedType.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/schema/target-namespace.xsd: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/code/referencedType.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName", "ReplaceArrayOfWithLiteral", "LocalVariableName", "FunctionName", "RemoveRedundantBackticks") 2 | 3 | package org.redundent.generated 4 | 5 | import org.redundent.kotlin.xml.* 6 | 7 | open class `RootType`(nodeName: String) : Node(nodeName) { 8 | var `id`: kotlin.String? 9 | get() = get("id") 10 | set(value) { set("id", value) } 11 | } 12 | 13 | fun `RootType`.`comments`(value: kotlin.String) { 14 | "comments"(value) 15 | } -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/schema/child-prop-order.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/code/group.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName", "ReplaceArrayOfWithLiteral", "LocalVariableName", "FunctionName", "RemoveRedundantBackticks") 2 | 3 | package org.redundent.generated 4 | 5 | import org.redundent.kotlin.xml.* 6 | 7 | open class `Root`(nodeName: String) : Node(nodeName) 8 | 9 | fun `Root`.`name`(value: kotlin.String) { 10 | "name"(value) 11 | } 12 | 13 | fun `root`(__block__: `Root`.() -> Unit): `Root` { 14 | val `root` = `Root`("root") 15 | `root`.apply(__block__) 16 | return `root` 17 | } -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/schema/group.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/main/kotlin/org/redundent/kotlin/xml/gen/writer/XmlAttribute.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml.gen.writer 2 | 3 | class XmlAttribute( 4 | val name: String, 5 | val type: String, 6 | private val isRequired: Boolean 7 | ) : Code { 8 | override fun write(codeWriter: CodeWriter) { 9 | codeWriter.writeln("var `$name`: $type") 10 | codeWriter.indent() 11 | codeWriter.writeln("get() = get(\"$name\")${if (isRequired) "!!" else ""}") 12 | codeWriter.writeln("set(value) { set(\"$name\", value) }") 13 | codeWriter.dedent() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/resources/test-results/sitemapTest/sitemapIndex.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | http://blog.redundent.org/sitemap1.xml 5 | 6 | 7 | 2017-10-24 8 | 9 | 10 | 11 | 12 | http://blog.redundent.org/sitemap2.xml 13 | 14 | 15 | 2016-01-01 16 | 17 | 18 | 19 | 20 | http://blog.redundent.org/sitemap3.xml 21 | 22 | 23 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/main/kotlin/org/redundent/kotlin/xml/gen/writer/XmlEnum.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml.gen.writer 2 | 3 | import com.sun.tools.xjc.outline.EnumOutline 4 | 5 | class XmlEnum(private val enum: EnumOutline) : Code { 6 | override fun write(codeWriter: CodeWriter) { 7 | with(codeWriter) { 8 | writeKotlinDoc(enum.target.documentation) 9 | writeln("enum class `${enum.target.shortName}` {") 10 | indent() 11 | 12 | writeln(enum.constants.joinToString(",\n\t") { "`${it.target.lexicalValue}`" }) 13 | dedent() 14 | writeln("}\n") 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/schema/string-to-bytearray.jxb: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/schema/choice.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/main/kotlin/org/redundent/kotlin/xml/Namespace.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | /** 4 | * Represents an xml namespace (`xmlns`). 5 | */ 6 | data class Namespace( 7 | /** 8 | * The name or prefix of the namespace. 9 | */ 10 | val name: String, 11 | /** 12 | * The value/uri/url of the namespace. 13 | */ 14 | val value: String 15 | ) { 16 | 17 | constructor(value: String) : this("", value) 18 | 19 | val isDefault: Boolean = name.isEmpty() 20 | 21 | val fqName: String = if (isDefault) "xmlns" else "xmlns:$name" 22 | 23 | override fun toString(): String { 24 | return "$fqName=\"$value\"" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/kotlin/org/redundent/kotlin/xml/OrderedNodesTest.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | import org.junit.Test 4 | 5 | class OrderedNodesTest : TestBase() { 6 | @Test 7 | fun correctOrder() { 8 | val xml = structured { 9 | second() 10 | first() 11 | } 12 | 13 | validate(xml) 14 | } 15 | 16 | @XmlType(childOrder = ["first", "second"]) 17 | inner class Structured internal constructor() : Node("xml") { 18 | fun first() = "first"() 19 | fun second() = "second"() 20 | } 21 | 22 | private fun structured(block: Structured.() -> Unit): Structured { 23 | return Structured().apply(block) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/schema/member-functions.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/kotlin/org/redundent/kotlin/xml/UtilsKtTest.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | import org.junit.Test 4 | import kotlin.test.assertEquals 5 | 6 | class UtilsKtTest { 7 | @Test 8 | fun escapeValue10() { 9 | val unescapedValue = "\u000b\u000c" 10 | 11 | val escapedValue = escapeValue(unescapedValue, XmlVersion.V10) 12 | 13 | assertEquals("", escapedValue, "1.0 escapes \\u000b and \\u000c to empty string") 14 | } 15 | 16 | @Test 17 | fun escapeValue11() { 18 | val unescapedValue = "\u000b\u000c" 19 | 20 | val escapedValue = escapeValue(unescapedValue, XmlVersion.V11) 21 | 22 | assertEquals(" ", escapedValue, "1.1 escapes \\u000b and \\u000c to and ") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/main/kotlin/org/redundent/kotlin/xml/Comment.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | /** 4 | * Similar to a [TextElement] except that the inner text is wrapped inside a comment tag ``. 5 | * 6 | * Note that `--` will be replaced with `--`. 7 | */ 8 | class Comment internal constructor(val text: String) : Element { 9 | override fun render(builder: Appendable, indent: String, printOptions: PrintOptions) { 10 | val lineEnding = getLineEnding(printOptions) 11 | 12 | builder.append("$indent$lineEnding") 13 | } 14 | 15 | override fun equals(other: Any?): Boolean = other is Comment && other.text == text 16 | 17 | override fun hashCode(): Int = text.hashCode() 18 | } 19 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/schema/nested.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/main/kotlin/org/redundent/kotlin/xml/Doctype.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | class Doctype @JvmOverloads constructor( 4 | private val name: String, 5 | private val systemId: String? = null, 6 | private val publicId: String? = null 7 | ) : Element { 8 | 9 | override fun render(builder: Appendable, indent: String, printOptions: PrintOptions) { 10 | builder.append("") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/code/target-namespace.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName", "ReplaceArrayOfWithLiteral", "LocalVariableName", "FunctionName", "RemoveRedundantBackticks") 2 | 3 | package org.redundent.generated 4 | 5 | import org.redundent.kotlin.xml.* 6 | 7 | open class `Code`(nodeName: String) : Node(nodeName) { 8 | init { 9 | xmlns = "http://code.redundent.org/schemas" 10 | } 11 | 12 | var `id`: kotlin.Long? 13 | get() = get("id") 14 | set(value) { set("id", value) } 15 | } 16 | 17 | @JvmOverloads 18 | fun `code`(`id`: kotlin.Long? = null, 19 | __block__: `Code`.() -> Unit): `Code` { 20 | val `code` = `Code`("code") 21 | `code`.apply { 22 | if (`id` != null) { 23 | this.`id` = `id` 24 | } 25 | } 26 | `code`.apply(__block__) 27 | return `code` 28 | } -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/schema/duplicate-names.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/README.md: -------------------------------------------------------------------------------- 1 | [ ![Download](https://api.bintray.com/packages/redundent/maven/kotlin-xml-dsl-generator/images/download.svg) ](https://bintray.com/redundent/maven/kotlin-xml-dsl-generator/_latestVersion) 2 | 3 | Kotlin XML Builder DSL Generator 4 | = 5 | 6 | This jar can be used to generate a typesafe DSL based on an XSD file. 7 | 8 | **NOTE**: This is experimental and may not generate the exact DSL you expect. Please report 9 | any issues. 10 | 11 | License 12 | - 13 | Apache 2.0 14 | 15 | Usage 16 | = 17 | Usage is similar to generating java code with XJC. Simply run: 18 | 19 | ```bash 20 | java -jar -d -p 21 | ``` 22 | 23 | You can also use JAXB bindings files to customize some of the generator code. Note that this is 24 | very experimental and not guaranteed to work with all customizations. 25 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/code/string-to-bytearray.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName", "ReplaceArrayOfWithLiteral", "LocalVariableName", "FunctionName", "RemoveRedundantBackticks") 2 | 3 | package org.redundent.generated 4 | 5 | import org.redundent.kotlin.xml.* 6 | 7 | open class `StringToByteArray`(nodeName: String) : Node(nodeName) { 8 | var `value`: kotlin.ByteArray? 9 | get() = get("value") 10 | set(value) { set("value", value) } 11 | } 12 | 13 | @JvmOverloads 14 | fun `String-To-ByteArray`(`value`: kotlin.ByteArray? = null, 15 | __block__: `StringToByteArray`.() -> Unit): `StringToByteArray` { 16 | val `String-To-ByteArray` = `StringToByteArray`("String-To-ByteArray") 17 | `String-To-ByteArray`.apply { 18 | if (`value` != null) { 19 | this.`value` = `value` 20 | } 21 | } 22 | `String-To-ByteArray`.apply(__block__) 23 | return `String-To-ByteArray` 24 | } -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/schema/extension.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/code/child-prop-order.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName", "ReplaceArrayOfWithLiteral", "LocalVariableName", "FunctionName", "RemoveRedundantBackticks") 2 | 3 | package org.redundent.generated 4 | 5 | import org.redundent.kotlin.xml.* 6 | 7 | @XmlType(childOrder = arrayOf("Child1", 8 | "child2", 9 | "CHILD3")) 10 | open class `ChildPropOrder`(nodeName: String) : Node(nodeName) 11 | 12 | fun `ChildPropOrder`.`Child1`(value: kotlin.String) { 13 | "Child1"(value) 14 | } 15 | 16 | fun `ChildPropOrder`.`child2`(value: kotlin.String) { 17 | "child2"(value) 18 | } 19 | 20 | fun `ChildPropOrder`.`CHILD3`(value: kotlin.String) { 21 | "CHILD3"(value) 22 | } 23 | 24 | fun `ChildPropOrder`(__block__: `ChildPropOrder`.() -> Unit): `ChildPropOrder` { 25 | val `ChildPropOrder` = `ChildPropOrder`("ChildPropOrder") 26 | `ChildPropOrder`.apply(__block__) 27 | return `ChildPropOrder` 28 | } -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/code/member-functions.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName", "ReplaceArrayOfWithLiteral", "LocalVariableName", "FunctionName", "RemoveRedundantBackticks") 2 | 3 | package org.redundent.generated 4 | 5 | import org.redundent.kotlin.xml.* 6 | 7 | @XmlType(childOrder = arrayOf("comments", 8 | "sub-type")) 9 | open class `MemberType`(nodeName: String) : Node(nodeName) { 10 | var `id`: kotlin.String? 11 | get() = get("id") 12 | set(value) { set("id", value) } 13 | 14 | fun `comments`(value: kotlin.String) { 15 | "comments"(value) 16 | } 17 | 18 | fun `sub-type`(__block__: `MemberType`.`SubType`.() -> Unit): `SubType` { 19 | val `sub-type` = `SubType`() 20 | `sub-type`.apply(__block__) 21 | return `sub-type` 22 | } 23 | 24 | inner class `SubType` : Node("SubType") { 25 | fun `sub-type-element`(value: kotlin.String) { 26 | "sub-type-element"(value) 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/kotlin/org/redundent/kotlin/xml/gen/SimpleSchemaGen.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml.gen 2 | 3 | import org.junit.Test 4 | 5 | class SimpleSchemaGen : AbstractGenTest() { 6 | @Test 7 | fun sitemap() = run() 8 | 9 | @Test 10 | fun attributes() = run() 11 | 12 | @Test 13 | fun `attributes-required`() = run() 14 | 15 | @Test 16 | fun nested() = run() 17 | 18 | @Test 19 | fun referencedType() = run() 20 | 21 | @Test 22 | fun group() = run() 23 | 24 | @Test 25 | fun extension() = run() 26 | 27 | @Test 28 | fun `string-to-bytearray`() = run(withBindingFile = true) 29 | 30 | @Test 31 | fun `target-namespace`() = run() 32 | 33 | @Test 34 | fun choice() = run() 35 | 36 | @Test 37 | fun `duplicate-names`() = run() 38 | 39 | @Test 40 | fun `child-prop-order`() = run() 41 | 42 | @Test 43 | fun `member-functions`() = run(additionalArgs = arrayOf("--use-member-functions")) 44 | } 45 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/kotlin/org/redundent/kotlin/xml/sitemapTest.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | import org.junit.Test 4 | import java.text.SimpleDateFormat 5 | 6 | class sitemapTest : TestBase() { 7 | @Test 8 | fun basicTest() { 9 | val urlset = urlset { 10 | for (i in 1..3) { 11 | url("http://blog.redundent.org/post/$i") 12 | } 13 | } 14 | 15 | validate(urlset) 16 | } 17 | 18 | @Test 19 | fun allElements() { 20 | val urlset = urlset { 21 | url( 22 | "http://blog.redundent.org", 23 | SimpleDateFormat("yyyy-MM-dd").parse("2017-10-24"), 24 | ChangeFreq.hourly, 25 | 14.0 26 | ) 27 | } 28 | validate(urlset) 29 | } 30 | 31 | @Test 32 | fun sitemapIndex() { 33 | val format = SimpleDateFormat("yyyy-MM-dd") 34 | val sitemapIndex = sitemapindex { 35 | sitemap("http://blog.redundent.org/sitemap1.xml", format.parse("2017-10-24")) 36 | sitemap("http://blog.redundent.org/sitemap2.xml", format.parse("2016-01-01")) 37 | sitemap("http://blog.redundent.org/sitemap3.xml") 38 | } 39 | 40 | validate(sitemapIndex) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /kotlin-xml-builder/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | `maven-publish` 4 | signing 5 | id("org.jlleitschuh.gradle.ktlint") 6 | } 7 | 8 | val kotlinVersion: String by rootProject.extra 9 | 10 | tasks { 11 | val jar by getting(Jar::class) 12 | 13 | register("sourceJar") { 14 | from(sourceSets["main"].allSource) 15 | destinationDirectory.set(jar.destinationDirectory) 16 | archiveClassifier.set("sources") 17 | } 18 | } 19 | 20 | dependencies { 21 | compileOnly(kotlin("stdlib", kotlinVersion)) 22 | compileOnly(kotlin("reflect", kotlinVersion)) 23 | implementation("org.apache.commons:commons-lang3:3.5") 24 | 25 | testImplementation("junit:junit:4.13.1") 26 | testImplementation(kotlin("reflect", kotlinVersion)) 27 | testImplementation(kotlin("test-junit", kotlinVersion)) 28 | } 29 | 30 | artifacts { 31 | add("archives", tasks["sourceJar"]) 32 | } 33 | 34 | publishing { 35 | publications { 36 | register("maven") { 37 | from(components["java"]) 38 | 39 | artifact(tasks["sourceJar"]) { 40 | classifier = "sources" 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/code/duplicate-names.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName", "ReplaceArrayOfWithLiteral", "LocalVariableName", "FunctionName", "RemoveRedundantBackticks") 2 | 3 | package org.redundent.generated 4 | 5 | import org.redundent.kotlin.xml.* 6 | 7 | open class `DuplicateNameContainer`(nodeName: String) : Node(nodeName) { 8 | inner class `DuplicateName` : org.redundent.generated.`DuplicateName`("DuplicateName") 9 | } 10 | 11 | fun `DuplicateNameContainer`.`DuplicateName`(__block__: `DuplicateNameContainer`.`DuplicateName`.() -> Unit) { 12 | val `DuplicateName` = `DuplicateName`() 13 | `DuplicateName`.apply(__block__) 14 | this.addElement(`DuplicateName`) 15 | } 16 | 17 | open class `DuplicateNames`(nodeName: String) : org.redundent.generated.`DuplicateNameContainer`(nodeName) 18 | 19 | fun `DuplicateNames`(__block__: `DuplicateNames`.() -> Unit): `DuplicateNames` { 20 | val `DuplicateNames` = `DuplicateNames`("DuplicateNames") 21 | `DuplicateNames`.apply(__block__) 22 | return `DuplicateNames` 23 | } 24 | 25 | open class `DuplicateName`(nodeName: String) : Node(nodeName) -------------------------------------------------------------------------------- /kotlin-xml-builder/src/main/kotlin/org/redundent/kotlin/xml/ProcessingInstructionElement.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | import org.apache.commons.lang3.builder.HashCodeBuilder 4 | 5 | /** 6 | * Similar to a [TextElement] except that the inner text is wrapped inside `` tag. 7 | */ 8 | class ProcessingInstructionElement internal constructor(text: String, private val attributes: Map) : 9 | TextElement(text) { 10 | override fun renderedText(printOptions: PrintOptions): String { 11 | return "" 12 | } 13 | 14 | private fun renderAttributes(): String { 15 | if (attributes.isEmpty()) { 16 | return "" 17 | } 18 | 19 | return " " + attributes.entries.joinToString(" ") { 20 | "${it.key}=\"${it.value}\"" 21 | } 22 | } 23 | 24 | override fun equals(other: Any?): Boolean { 25 | if (!super.equals(other) || other !is ProcessingInstructionElement) { 26 | return false 27 | } 28 | 29 | return attributes == other.attributes 30 | } 31 | 32 | override fun hashCode(): Int = HashCodeBuilder() 33 | .appendSuper(super.hashCode()) 34 | .append(attributes) 35 | .toHashCode() 36 | } 37 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/kotlin/org/redundent/kotlin/xml/TextElementTest.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | import org.junit.Test 4 | import kotlin.test.assertEquals 5 | import kotlin.test.assertFalse 6 | import kotlin.test.assertNotEquals 7 | 8 | class TextElementTest { 9 | @Test 10 | fun testHashCode() { 11 | val text = TextElement("test") 12 | 13 | assertEquals(text.text.hashCode(), text.hashCode()) 14 | } 15 | 16 | @Test 17 | fun `equals null`() { 18 | val text = TextElement("test") 19 | 20 | assertFalse(text.equals(null)) 21 | } 22 | 23 | @Test 24 | fun `equals different type`() { 25 | val text = TextElement("test") 26 | val other = Comment("test") 27 | 28 | assertFalse(text.equals(other)) 29 | } 30 | 31 | @Test 32 | fun `equals different text`() { 33 | val text1 = TextElement("text1") 34 | val text2 = TextElement("text2") 35 | 36 | assertNotEquals(text1, text2) 37 | assertNotEquals(text2, text1) 38 | } 39 | 40 | @Test 41 | fun equals() { 42 | val text1 = TextElement("text1") 43 | val text2 = TextElement("text1") 44 | 45 | assertEquals(text1, text2) 46 | assertEquals(text2, text1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ main ] 10 | pull_request: 11 | branches: [ main ] 12 | workflow_call: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # This workflow contains a single job called "build" 17 | build: 18 | # The type of runner that the job will run on 19 | runs-on: ubuntu-latest 20 | 21 | # Steps represent a sequence of tasks that will be executed as part of the job 22 | steps: 23 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 24 | - uses: actions/checkout@v3 25 | 26 | - name: Setup Java JDK 27 | uses: actions/setup-java@v3 28 | with: 29 | distribution: temurin 30 | java-version: 8 31 | 32 | - name: Assemble 33 | run: ./gradlew assemble 34 | 35 | - name: Check 36 | run: ./gradlew check 37 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/main/kotlin/org/redundent/kotlin/xml/CDATAElement.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | import org.apache.commons.lang3.builder.HashCodeBuilder 4 | 5 | /** 6 | * Similar to a [TextElement] except that the inner text is wrapped inside a `` tag. 7 | */ 8 | class CDATAElement internal constructor(text: String) : TextElement(text) { 9 | override fun renderedText(printOptions: PrintOptions): String { 10 | fun String.escapeCData(): String { 11 | val cdataEnd = "]]>" 12 | val cdataStart = "") 16 | } 17 | 18 | return "" 19 | } 20 | 21 | override fun equals(other: Any?): Boolean = super.equals(other) && other is CDATAElement 22 | 23 | // Need to use javaClass here to avoid a normal TextElement and a CDATAElement having the same hashCode if they have 24 | // the same text 25 | override fun hashCode(): Int = HashCodeBuilder() 26 | .appendSuper(super.hashCode()) 27 | .append(javaClass.hashCode()) 28 | .toHashCode() 29 | } 30 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/kotlin/org/redundent/kotlin/xml/CommentTest.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | import org.junit.Test 4 | import kotlin.test.assertEquals 5 | import kotlin.test.assertFalse 6 | import kotlin.test.assertNotEquals 7 | 8 | class CommentTest { 9 | @Test 10 | fun testHashCode() { 11 | val comment = Comment("test") 12 | 13 | assertEquals(comment.text.hashCode(), comment.hashCode()) 14 | } 15 | 16 | @Test 17 | fun `equals null`() { 18 | val comment = Comment("test") 19 | 20 | assertFalse(comment.equals(null)) 21 | } 22 | 23 | @Test 24 | fun `equals different type`() { 25 | val comment = Comment("test") 26 | val other = CDATAElement("test") 27 | 28 | assertFalse(comment.equals(other)) 29 | } 30 | 31 | @Test 32 | fun `equals different text`() { 33 | val comment1 = Comment("comment1") 34 | val comment2 = Comment("comment2") 35 | 36 | assertNotEquals(comment1, comment2) 37 | assertNotEquals(comment2, comment1) 38 | } 39 | 40 | @Test 41 | fun equals() { 42 | val comment1 = Comment("comment") 43 | val comment2 = Comment("comment") 44 | 45 | assertEquals(comment1, comment2) 46 | assertEquals(comment2, comment1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/main/kotlin/org/redundent/kotlin/xml/TextElement.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | /** 4 | * An element type that has some text in it. 5 | * 6 | * For example: 7 | * ```xml 8 | * http://blog.redundent.org 9 | * ``` 10 | */ 11 | open class TextElement internal constructor(val text: String, private val unsafe: Boolean = false) : Element { 12 | override fun render(builder: Appendable, indent: String, printOptions: PrintOptions) { 13 | if (text.isEmpty()) { 14 | return 15 | } 16 | 17 | val lineEnding = getLineEnding(printOptions) 18 | 19 | builder.append("$indent${renderedText(printOptions)}$lineEnding") 20 | } 21 | 22 | internal fun renderSingleLine(builder: Appendable, printOptions: PrintOptions) { 23 | builder.append(renderedText(printOptions)) 24 | } 25 | 26 | internal open fun renderedText(printOptions: PrintOptions): String? { 27 | return if (unsafe) { 28 | text 29 | } else { 30 | escapeValue(text, printOptions.xmlVersion, printOptions.useCharacterReference) 31 | } 32 | } 33 | 34 | override fun equals(other: Any?): Boolean = other is TextElement && other.text == text 35 | 36 | override fun hashCode(): Int = text.hashCode() 37 | } 38 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/kotlin/org/redundent/kotlin/xml/CDATAElementTest.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | import org.junit.Test 4 | import kotlin.test.assertEquals 5 | import kotlin.test.assertFalse 6 | import kotlin.test.assertNotEquals 7 | 8 | class CDATAElementTest { 9 | @Test 10 | fun testHashCode() { 11 | val text = CDATAElement("test") 12 | 13 | assertNotEquals(text.text.hashCode(), text.hashCode(), "CDATA hashcode is not just text.hashCode()") 14 | } 15 | 16 | @Test 17 | fun `equals null`() { 18 | val text = CDATAElement("test") 19 | 20 | assertFalse(text.equals(null)) 21 | } 22 | 23 | @Test 24 | fun `equals different type`() { 25 | val text = CDATAElement("test") 26 | val other = TextElement("test") 27 | 28 | assertNotEquals(text, other) 29 | } 30 | 31 | @Test 32 | fun `equals different text`() { 33 | val text1 = CDATAElement("text1") 34 | val text2 = CDATAElement("text2") 35 | 36 | assertNotEquals(text1, text2) 37 | assertNotEquals(text2, text1) 38 | } 39 | 40 | @Test 41 | fun equals() { 42 | val text1 = CDATAElement("text1") 43 | val text2 = CDATAElement("text1") 44 | 45 | assertEquals(text1, text2) 46 | assertEquals(text2, text1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/code/choice.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName", "ReplaceArrayOfWithLiteral", "LocalVariableName", "FunctionName", "RemoveRedundantBackticks") 2 | 3 | package org.redundent.generated 4 | 5 | import org.redundent.kotlin.xml.* 6 | 7 | open class `MethodName`(nodeName: String) : Node(nodeName) 8 | 9 | fun `MethodName`.`type1`(__block__: `Type1`.() -> Unit) { 10 | val `type1` = `Type1`("type1") 11 | `type1`.apply(__block__) 12 | this.addElement(`type1`) 13 | } 14 | 15 | fun `MethodName`.`type2`(__block__: `Type2`.() -> Unit) { 16 | val `type2` = `Type2`("type2") 17 | `type2`.apply(__block__) 18 | this.addElement(`type2`) 19 | } 20 | 21 | fun `MethodName`.`type3`(__block__: `Type3`.() -> Unit) { 22 | val `type3` = `Type3`("type3") 23 | `type3`.apply(__block__) 24 | this.addElement(`type3`) 25 | } 26 | 27 | fun `Method-Name`(__block__: `MethodName`.() -> Unit): `MethodName` { 28 | val `Method-Name` = `MethodName`("Method-Name") 29 | `Method-Name`.apply(__block__) 30 | return `Method-Name` 31 | } 32 | 33 | open class `Type1`(nodeName: String) : Node(nodeName) 34 | 35 | open class `Type2`(nodeName: String) : Node(nodeName) 36 | 37 | open class `Type3`(nodeName: String) : Node(nodeName) -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Deploy to central 3 | 4 | on: workflow_dispatch 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | build: 11 | uses: ./.github/workflows/ci.yml 12 | deploy: 13 | runs-on: ubuntu-latest 14 | needs: build 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Setup Java JDK 19 | uses: actions/setup-java@v3 20 | with: 21 | distribution: temurin 22 | java-version: 8 23 | 24 | - name: Import GPG key 25 | uses: crazy-max/ghaction-import-gpg@v5 26 | with: 27 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 28 | passphrase: ${{ secrets.GPG_PRIVATE_PASSWORD }} 29 | 30 | - name: Gradle publish 31 | uses: gradle/gradle-build-action@ce999babab2de1c4b649dc15f0ee67e6246c994f 32 | with: 33 | arguments: | 34 | publish 35 | closeSonatypeStagingRepository 36 | -Psigning.gnupg.passphrase='${{secrets.GPG_PRIVATE_PASSWORD}}' 37 | -Psigning.gnupg.keyName='${{secrets.GPG_PRIVATE_KEY_ID}}' 38 | -PsonatypeUsername='${{secrets.SONATYPE_USERNAME}}' 39 | -PsonatypePassword='${{secrets.SONATYPE_PASSWORD}}' -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/main/kotlin/org/redundent/kotlin/xml/gen/SchemaOutline.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml.gen 2 | 3 | import com.sun.tools.xjc.model.CClassInfo 4 | import com.sun.tools.xjc.model.CClassInfoParent 5 | import com.sun.tools.xjc.outline.Outline 6 | import org.redundent.kotlin.xml.gen.writer.XmlClass 7 | import org.redundent.kotlin.xml.gen.writer.XmlEnum 8 | 9 | class SchemaOutline(outline: Outline, private val opts: ExOptions) { 10 | private val innerClasses = outline.classes.filter { it.target.parent() is CClassInfo }.groupBy { it.target.parent() as CClassInfo } 11 | private val rootClasses = outline.classes.filter { it.target.parent() is CClassInfoParent.Package } 12 | 13 | val classes = rootClasses.sortedBy { if (it.target.isAbstract) 0 else 1 }.map { XmlClass(it, opts) } 14 | val enums = outline.enums.map(::XmlEnum) 15 | 16 | init { 17 | classes.forEach(this::recurseInnerClasses) 18 | } 19 | 20 | private fun recurseInnerClasses(clazz: XmlClass) { 21 | val innerClasses = innerClasses[clazz.clazz.target] 22 | innerClasses?.forEach { 23 | val innerClass = XmlClass(it, opts, innerClass = true) 24 | clazz.addInnerClass(innerClass) 25 | 26 | recurseInnerClasses(innerClass) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/main/kotlin/org/redundent/kotlin/xml/Utils.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | import org.apache.commons.lang3.StringEscapeUtils 4 | import java.lang.StringBuilder 5 | 6 | internal fun escapeValue(value: Any?, xmlVersion: XmlVersion, useCharacterReference: Boolean = false): String? { 7 | val asString = value?.toString() ?: return null 8 | 9 | if (useCharacterReference) { 10 | return referenceCharacter(asString) 11 | } 12 | 13 | return when (xmlVersion) { 14 | XmlVersion.V10 -> StringEscapeUtils.escapeXml10(asString) 15 | XmlVersion.V11 -> StringEscapeUtils.escapeXml11(asString) 16 | } 17 | } 18 | 19 | internal fun referenceCharacter(asString: String): String { 20 | val builder = StringBuilder() 21 | 22 | asString.toCharArray().forEach { character -> 23 | when (character) { 24 | '\'' -> builder.append("'") 25 | '&' -> builder.append("&") 26 | '<' -> builder.append("<") 27 | '>' -> builder.append(">") 28 | '"' -> builder.append(""") 29 | else -> builder.append(character) 30 | } 31 | } 32 | 33 | return builder.toString() 34 | } 35 | 36 | internal fun buildName(name: String, namespace: Namespace?): String = 37 | if (namespace == null || namespace.isDefault) name else "${namespace.name}:$name" 38 | 39 | fun unsafe(value: Any?): Unsafe = Unsafe(value) 40 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/code/nested.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName", "ReplaceArrayOfWithLiteral", "LocalVariableName", "FunctionName", "RemoveRedundantBackticks") 2 | 3 | package org.redundent.generated 4 | 5 | import org.redundent.kotlin.xml.* 6 | 7 | open class `Top`(nodeName: String) : Node(nodeName) { 8 | inner class `First` : Node("First") { 9 | var `Id`: kotlin.Long 10 | get() = get("Id")!! 11 | set(value) { set("Id", value) } 12 | 13 | inner class `Second` : Node("Second") { 14 | var `Name`: kotlin.String? 15 | get() = get("Name") 16 | set(value) { set("Name", value) } 17 | } 18 | } 19 | 20 | @JvmOverloads 21 | fun `First`.`second`(`Name`: kotlin.String? = null, 22 | __block__: `First`.`Second`.() -> Unit) { 23 | val `second` = `Second`() 24 | `second`.apply { 25 | if (`Name` != null) { 26 | this.`Name` = `Name` 27 | } 28 | } 29 | `second`.apply(__block__) 30 | this.addElement(`second`) 31 | } 32 | } 33 | 34 | fun `Top`.`first`(`Id`: kotlin.Long, 35 | __block__: `Top`.`First`.() -> Unit) { 36 | val `first` = `First`() 37 | `first`.apply { 38 | this.`Id` = `Id` 39 | } 40 | `first`.apply(__block__) 41 | this.addElement(`first`) 42 | } 43 | 44 | fun `top`(__block__: `Top`.() -> Unit): `Top` { 45 | val `top` = `Top`("top") 46 | `top`.apply(__block__) 47 | return `top` 48 | } -------------------------------------------------------------------------------- /kotlin-xml-builder/src/main/kotlin/org/redundent/kotlin/xml/PrintOptions.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | class PrintOptions( 4 | /** 5 | * Whether to print newlines and tabs while rendering the document. 6 | */ 7 | val pretty: Boolean = true, 8 | 9 | /** 10 | * Whether to print a single text element on the same line. 11 | * 12 | * ``` 13 | * 14 | * text value 15 | * 16 | * ``` 17 | * 18 | * vs 19 | * 20 | * ``` 21 | * text value 22 | * ``` 23 | */ 24 | val singleLineTextElements: Boolean = false, 25 | 26 | /** 27 | * Whether to use "self closing" tags for empty elements. 28 | * 29 | * ``` 30 | * 31 | * ``` 32 | * 33 | * vs 34 | * 35 | * ``` 36 | * 37 | * ``` 38 | */ 39 | val useSelfClosingTags: Boolean = true, 40 | 41 | /** 42 | * Whether to use escaped character or character reference 43 | * 44 | * If false: `'` becomes `'` 45 | * 46 | * If `true`: `'` becomes `'` 47 | */ 48 | val useCharacterReference: Boolean = false, 49 | 50 | /** 51 | * Changes the indent for new lines when [pretty] is enabled. The option has no effect when 52 | * [pretty] is set to `false`. The default uses one tab `\t`. 53 | */ 54 | val indent: String = "\t" 55 | ) { 56 | internal var xmlVersion: XmlVersion = XmlVersion.V10 57 | } 58 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar 2 | 3 | plugins { 4 | kotlin("jvm") 5 | id("com.github.johnrengelman.shadow") version "5.2.0" 6 | `maven-publish` 7 | signing 8 | id("org.jlleitschuh.gradle.ktlint") 9 | } 10 | 11 | val kotlinVersion: String by rootProject.extra 12 | 13 | tasks { 14 | val jar by getting(Jar::class) { 15 | manifest { 16 | attributes(mapOf("Main-Class" to "org.redundent.kotlin.xml.gen.DslGeneratorKt")) 17 | } 18 | } 19 | 20 | withType { 21 | archiveClassifier.set(null as String?) 22 | } 23 | 24 | register("sourceJar") { 25 | from(sourceSets["main"].allSource) 26 | destinationDirectory.set(jar.destinationDirectory) 27 | archiveClassifier.set("sources") 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation(kotlin("stdlib", kotlinVersion)) 33 | implementation(kotlin("reflect", kotlinVersion)) 34 | implementation("org.glassfish.jaxb:jaxb-xjc:2.3.8") 35 | 36 | testImplementation(project(":kotlin-xml-builder")) 37 | testImplementation("junit:junit:4.13.1") 38 | testImplementation(kotlin("test-junit", kotlinVersion)) 39 | } 40 | 41 | publishing { 42 | publications { 43 | register("maven") { 44 | artifact(tasks["shadowJar"]) 45 | 46 | artifact(tasks["sourceJar"]) { 47 | classifier = "sources" 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/code/extension.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName", "ReplaceArrayOfWithLiteral", "LocalVariableName", "FunctionName", "RemoveRedundantBackticks") 2 | 3 | package org.redundent.generated 4 | 5 | import org.redundent.kotlin.xml.* 6 | 7 | abstract class `BaseType`(nodeName: String) : Node(nodeName) { 8 | var `Name`: kotlin.String? 9 | get() = get("Name") 10 | set(value) { set("Name", value) } 11 | } 12 | 13 | open class `A`(nodeName: String) : org.redundent.generated.`BaseType`(nodeName) { 14 | var `Top`: kotlin.String? 15 | get() = get("Top") 16 | set(value) { set("Top", value) } 17 | } 18 | 19 | @JvmOverloads 20 | fun `a`(`Top`: kotlin.String? = null, 21 | `Name`: kotlin.String? = null, 22 | __block__: `A`.() -> Unit): `A` { 23 | val `a` = `A`("a") 24 | `a`.apply { 25 | if (`Top` != null) { 26 | this.`Top` = `Top` 27 | } 28 | if (`Name` != null) { 29 | this.`Name` = `Name` 30 | } 31 | } 32 | `a`.apply(__block__) 33 | return `a` 34 | } 35 | 36 | open class `B`(nodeName: String) : org.redundent.generated.`BaseType`(nodeName) { 37 | var `Middle`: kotlin.String? 38 | get() = get("Middle") 39 | set(value) { set("Middle", value) } 40 | } 41 | 42 | @JvmOverloads 43 | fun `b`(`Middle`: kotlin.String? = null, 44 | `Name`: kotlin.String? = null, 45 | __block__: `B`.() -> Unit): `B` { 46 | val `b` = `B`("b") 47 | `b`.apply { 48 | if (`Middle` != null) { 49 | this.`Middle` = `Middle` 50 | } 51 | if (`Name` != null) { 52 | this.`Name` = `Name` 53 | } 54 | } 55 | `b`.apply(__block__) 56 | return `b` 57 | } -------------------------------------------------------------------------------- /kotlin-xml-builder/src/main/kotlin/org/redundent/kotlin/xml/Sitemap.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.* 5 | 6 | const val DEFAULT_URLSET_NAMESPACE = "http://www.sitemaps.org/schemas/sitemap/0.9" 7 | 8 | class UrlSet internal constructor() : Node("urlset") { 9 | init { 10 | xmlns = DEFAULT_URLSET_NAMESPACE 11 | } 12 | 13 | fun url( 14 | loc: String, 15 | lastmod: Date? = null, 16 | changefreq: ChangeFreq? = null, 17 | priority: Double? = null 18 | ) { 19 | "url" { 20 | "loc"(loc) 21 | 22 | lastmod?.let { 23 | "lastmod"(formatDate(it)) 24 | } 25 | 26 | changefreq?.let { 27 | "changefreq"(it.name) 28 | } 29 | 30 | priority?.let { 31 | "priority"(it.toString()) 32 | } 33 | } 34 | } 35 | } 36 | 37 | class Sitemapindex internal constructor() : Node("sitemapindex") { 38 | init { 39 | xmlns = DEFAULT_URLSET_NAMESPACE 40 | } 41 | 42 | fun sitemap( 43 | loc: String, 44 | lastmod: Date? = null 45 | ) { 46 | "sitemap" { 47 | "loc"(loc) 48 | 49 | lastmod?.let { 50 | "lastmod"(formatDate(it)) 51 | } 52 | } 53 | } 54 | } 55 | 56 | @Suppress("EnumEntryName", "ktlint:enum-entry-name-case") 57 | enum class ChangeFreq { 58 | always, 59 | hourly, 60 | daily, 61 | weekly, 62 | monthly, 63 | yearly, 64 | never 65 | } 66 | 67 | private fun formatDate(date: Date): String { 68 | return SimpleDateFormat("yyyy-MM-dd").format(date) 69 | } 70 | 71 | fun urlset(init: UrlSet.() -> Unit) = UrlSet().apply(init) 72 | 73 | fun sitemapindex(init: Sitemapindex.() -> Unit) = Sitemapindex().apply(init) 74 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/schema/attributes.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/kotlin/org/redundent/kotlin/xml/ProcessingInstructionElementTest.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | import org.junit.Test 4 | import kotlin.test.assertEquals 5 | import kotlin.test.assertFalse 6 | import kotlin.test.assertNotEquals 7 | 8 | class ProcessingInstructionElementTest { 9 | @Test 10 | fun testHashCode() { 11 | val text = ProcessingInstructionElement("test", emptyMap()) 12 | 13 | assertNotEquals(text.text.hashCode(), text.hashCode(), "ProcessingInstructionElement hashcode is not just text.hashCode()") 14 | } 15 | 16 | @Test 17 | fun `equals null`() { 18 | val text = ProcessingInstructionElement("test", emptyMap()) 19 | 20 | assertFalse(text.equals(null)) 21 | } 22 | 23 | @Test 24 | fun `equals different type`() { 25 | val text = ProcessingInstructionElement("test", emptyMap()) 26 | val other = TextElement("test") 27 | 28 | assertNotEquals(text, other) 29 | } 30 | 31 | @Test 32 | fun `equals different text`() { 33 | val text1 = ProcessingInstructionElement("text1", emptyMap()) 34 | val text2 = ProcessingInstructionElement("text2", emptyMap()) 35 | 36 | assertNotEquals(text1, text2) 37 | assertNotEquals(text2, text1) 38 | } 39 | 40 | @Test 41 | fun equals() { 42 | val text1 = ProcessingInstructionElement("text1", emptyMap()) 43 | val text2 = ProcessingInstructionElement("text1", emptyMap()) 44 | 45 | assertEquals(text1, text2) 46 | assertEquals(text2, text1) 47 | } 48 | 49 | @Test 50 | fun `equals attributes same order`() { 51 | val text1 = ProcessingInstructionElement("text1", linkedMapOf("attr1" to "value1", "attr2" to "value2")) 52 | val text2 = ProcessingInstructionElement("text1", linkedMapOf("attr1" to "value1", "attr2" to "value2")) 53 | 54 | assertEquals(text1, text2) 55 | assertEquals(text2, text1) 56 | } 57 | 58 | @Test 59 | fun `equals attributes different order`() { 60 | val text1 = ProcessingInstructionElement("text1", linkedMapOf("attr1" to "value1", "attr2" to "value2")) 61 | val text2 = ProcessingInstructionElement("text1", linkedMapOf("attr2" to "value2", "attr1" to "value1")) 62 | 63 | assertEquals(text1, text2) 64 | assertEquals(text2, text1) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/code/sitemap.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName", "ReplaceArrayOfWithLiteral", "LocalVariableName", "FunctionName", "RemoveRedundantBackticks", "EnumEntryName") 2 | 3 | package org.redundent.generated 4 | 5 | import org.redundent.kotlin.xml.* 6 | 7 | /** 8 | * OPTIONAL: Indicates how frequently the content at a particular URL is 9 | * likely to change. The value "always" should be used to describe 10 | * documents that change each time they are accessed. The value "never" 11 | * should be used to describe archived URLs. Please note that web 12 | * crawlers may not necessarily crawl pages marked "always" more often. 13 | * Consider this element as a friendly suggestion and not a command. 14 | */ 15 | enum class `TChangeFreq` { 16 | `always`, 17 | `hourly`, 18 | `daily`, 19 | `weekly`, 20 | `monthly`, 21 | `yearly`, 22 | `never` 23 | } 24 | 25 | /** 26 | * Container for a set of up to 50,000 document elements. 27 | * This is the root element of the XML file. 28 | */ 29 | open class `Urlset`(nodeName: String) : Node(nodeName) { 30 | init { 31 | xmlns = "http://www.sitemaps.org/schemas/sitemap/0.9" 32 | } 33 | } 34 | 35 | fun `Urlset`.`url`(__block__: `TUrl`.() -> Unit) { 36 | val `url` = `TUrl`("url") 37 | `url`.apply(__block__) 38 | this.addElement(`url`) 39 | } 40 | 41 | /** 42 | * Container for a set of up to 50,000 document elements. 43 | * This is the root element of the XML file. 44 | */ 45 | fun `urlset`(__block__: `Urlset`.() -> Unit): `Urlset` { 46 | val `urlset` = `Urlset`("urlset") 47 | `urlset`.apply(__block__) 48 | return `urlset` 49 | } 50 | 51 | /** 52 | * Container for the data needed to describe a document to crawl. 53 | */ 54 | @XmlType(childOrder = arrayOf("loc", 55 | "lastmod", 56 | "changefreq", 57 | "priority")) 58 | open class `TUrl`(nodeName: String) : Node(nodeName) 59 | 60 | fun `TUrl`.`loc`(value: kotlin.String) { 61 | "loc"(value) 62 | } 63 | 64 | fun `TUrl`.`lastmod`(value: kotlin.String) { 65 | "lastmod"(value) 66 | } 67 | 68 | fun `TUrl`.`changefreq`(value: org.redundent.generated.TChangeFreq) { 69 | "changefreq"(value.toString()) 70 | } 71 | 72 | fun `TUrl`.`priority`(value: java.math.BigDecimal) { 73 | "priority"(value.toString()) 74 | } -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/kotlin/org/redundent/kotlin/xml/gen/AbstractGenTest.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml.gen 2 | 3 | import org.junit.Rule 4 | import org.junit.rules.TestName 5 | import java.io.File 6 | import java.io.FileNotFoundException 7 | import java.io.InputStream 8 | import java.io.InputStreamReader 9 | import kotlin.test.assertEquals 10 | 11 | abstract class AbstractGenTest { 12 | @Suppress("MemberVisibilityCanBePrivate") 13 | @get:Rule 14 | val testName = TestName() 15 | 16 | protected fun run(withBindingFile: Boolean = false, vararg additionalArgs: String) { 17 | val schema = javaClass.getResourceAsStream("/schema/${testName.methodName}.xsd") 18 | ?: throw FileNotFoundException("/schema/${testName.methodName}.xsd as not found") 19 | val code = getExpectedClassText() 20 | 21 | val file = File.createTempFile("schema", ".xsd").apply { 22 | outputStream().use { 23 | schema.copyTo(it) 24 | } 25 | } 26 | 27 | val opts = ExOptions().apply { parseArguments(arrayOf("-p", "org.redundent.generated", file.absolutePath) + additionalArgs) } 28 | 29 | if (withBindingFile) { 30 | val binding = javaClass.getResourceAsStream("/schema/${testName.methodName}.jxb") 31 | ?: throw FileNotFoundException("/schema/${testName.methodName}.jxb as not found") 32 | val bindingFile = File.createTempFile("binding", ".jxb").apply { 33 | writer().use { writer -> 34 | binding.reader().forEachLine { line -> 35 | writer.appendLine(line.replace("@schema@", "file:/${file.absolutePath}")) 36 | } 37 | } 38 | } 39 | 40 | opts.addBindFile(bindingFile) 41 | } 42 | 43 | val text = cleanText(DslGenerator(opts).generate().replace(System.lineSeparator(), "\n")) 44 | 45 | assertEquals(code, text, "generated code is not the same") 46 | } 47 | 48 | private fun getExpectedClassText(): String { 49 | val inputStream = getInputStream() 50 | inputStream.use { 51 | return InputStreamReader(it).readText().replace(System.lineSeparator(), "\n") 52 | } 53 | } 54 | 55 | private fun getInputStream(): InputStream { 56 | val resName = "/code/${testName.methodName}.kt" 57 | return javaClass.getResourceAsStream(resName) 58 | ?: throw FileNotFoundException("$resName as not found") 59 | } 60 | 61 | private fun cleanText(text: String): String { 62 | return text.split("\n").joinToString("\n", transform = String::trimEnd) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/main/kotlin/org/redundent/kotlin/xml/gen/writer/XmlElement.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml.gen.writer 2 | 3 | import com.sun.tools.xjc.model.CClassInfo 4 | import com.sun.tools.xjc.model.CNonElement 5 | 6 | class XmlElement( 7 | val name: String, 8 | val type: CNonElement, 9 | private val tagName: String, 10 | private val documentation: String?, 11 | private val parent: CClassInfo? = null 12 | ) : Code { 13 | 14 | override fun write(codeWriter: CodeWriter) { 15 | val rootElement = parent == null 16 | with(codeWriter) { 17 | writeKotlinDoc(documentation) 18 | 19 | val funLine = "fun ${if (!rootElement) "`${parent!!.shortName}`." else ""}`$name`" 20 | 21 | if (type is CClassInfo && type.hasOptionalAttributes) { 22 | writeln("@${JvmOverloads::class.simpleName}") 23 | } 24 | 25 | write(funLine) 26 | 27 | if (type is CClassInfo) { 28 | val blockParamType = 29 | "${((type.parent() as? CClassInfo)?.let { "`${it.shortName}`." } ?: "")}`${type.shortName}`" 30 | 31 | writeln( 32 | "(${type.attributesAsParameters(funLine.length + (currentIndex * 4) + 1)}__block__: $blockParamType.() -> Unit)${if (rootElement) ": `${type.shortName}`" else ""} {", 33 | false 34 | ) 35 | indent() 36 | writeln("val `$tagName` = `${type.shortName}`(${if (type.parent() is CClassInfo) "" else "\"$tagName\""})") 37 | if (type.allAttributes.isNotEmpty()) { 38 | writeln("`$tagName`.apply {") 39 | indent() 40 | for (attr in type.allAttributes) { 41 | val attrName = attr.xmlName.localPart 42 | if (!attr.isRequired) { 43 | writeln("if (`$attrName` != null) {") 44 | writeln("\tthis.`$attrName` = `$attrName`") 45 | writeln("}") 46 | } else { 47 | writeln("this.`$attrName` = `$attrName`") 48 | } 49 | } 50 | dedent() 51 | writeln("}") 52 | } 53 | 54 | writeln("`$tagName`.apply(__block__)") 55 | if (rootElement) { 56 | writeln("return `$tagName`") 57 | } else { 58 | writeln("this.addElement(`$tagName`)") 59 | } 60 | 61 | dedent() 62 | writeln("}\n") 63 | } else { 64 | val t = CodeWriter.mapType(type.type.fullName()) 65 | writeln("(value: $t) {", false) 66 | indent() 67 | writeln("\"$tagName\"(value${if (t != String::class.qualifiedName) ".toString()" else ""})") 68 | dedent() 69 | writeln("}\n") 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/schema/attributes-required.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/kotlin/org/redundent/kotlin/xml/TestBase.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | import org.junit.Rule 4 | import org.junit.rules.TestName 5 | import org.w3c.dom.Document 6 | import java.io.InputStream 7 | import java.io.InputStreamReader 8 | import java.io.StringWriter 9 | import java.util.MissingResourceException 10 | import javax.xml.parsers.DocumentBuilderFactory 11 | import javax.xml.transform.OutputKeys 12 | import javax.xml.transform.TransformerFactory 13 | import javax.xml.transform.dom.DOMSource 14 | import javax.xml.transform.stream.StreamResult 15 | import kotlin.test.assertEquals 16 | 17 | open class TestBase { 18 | @get:Rule 19 | val testName = TestName() 20 | 21 | private fun getExpectedXml(): String { 22 | val inputStream = getInputStream() 23 | inputStream.use { 24 | return InputStreamReader(it).readText().replace(System.lineSeparator(), "\n") 25 | } 26 | } 27 | 28 | protected fun getInputStream(): InputStream { 29 | val resName = "/test-results/${javaClass.simpleName}/${testName.methodName}.xml" 30 | return javaClass.getResourceAsStream(resName) 31 | ?: throw MissingResourceException("Cannot find expected xml resource: $resName. Did you forget to create it?", javaClass.name, testName.methodName) 32 | } 33 | 34 | protected fun validate(xml: Node, prettyFormat: Boolean = true) { 35 | validate(xml, PrintOptions(pretty = prettyFormat)) 36 | } 37 | 38 | protected fun validate(xml: Node, printOptions: PrintOptions) { 39 | val actual = xml.toString(printOptions) 40 | 41 | // Doing a replace to cater for different line endings. 42 | assertEquals(getExpectedXml(), actual.replace(System.lineSeparator(), "\n"), "actual xml matches what is expected") 43 | 44 | validateXml(actual) 45 | } 46 | 47 | protected fun validateXml(actual: String): Document { 48 | return actual.byteInputStream().use { 49 | DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(it) 50 | } 51 | } 52 | 53 | protected fun validateTest(xml: Node) { 54 | val actual = validateXml(xml.toString()) 55 | val expected = getInputStream().use { 56 | DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(it) 57 | } 58 | 59 | val actualString = actual.transform() 60 | val expectedString = expected.transform() 61 | 62 | assertEquals(expectedString, actualString, "actual xml matches what is expected") 63 | } 64 | 65 | private fun Document.transform(): String { 66 | val sw = StringWriter() 67 | val tf = TransformerFactory.newInstance() 68 | val transformer = tf.newTransformer() 69 | transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no") 70 | transformer.setOutputProperty(OutputKeys.METHOD, "xml") 71 | transformer.setOutputProperty(OutputKeys.INDENT, "yes") 72 | transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8") 73 | 74 | transformer.transform(DOMSource(this), StreamResult(sw)) 75 | return sw.toString() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/main/kotlin/org/redundent/kotlin/xml/gen/DslGenerator.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml.gen 2 | 3 | import com.sun.codemodel.JCodeModel 4 | import com.sun.tools.xjc.BadCommandLineException 5 | import com.sun.tools.xjc.ErrorReceiver 6 | import com.sun.tools.xjc.ModelLoader 7 | import com.sun.tools.xjc.Options 8 | import com.sun.tools.xjc.generator.bean.BeanGenerator 9 | import org.redundent.kotlin.xml.gen.writer.CodeWriter 10 | import org.xml.sax.SAXParseException 11 | import java.io.File 12 | import kotlin.system.exitProcess 13 | 14 | fun main(args: Array) { 15 | try { 16 | val opts = ExOptions().apply { parseArguments(args) } 17 | val output = 18 | File(opts.targetDir, "${opts.defaultPackage.replace(".", File.separator)}${File.separator}schema.kt") 19 | 20 | println("\nGenerating schema to ${output.absolutePath}") 21 | 22 | val generated = DslGenerator(opts).generate() 23 | 24 | output.parentFile.mkdirs() 25 | 26 | output.writer().use { 27 | it.append(generated) 28 | } 29 | } catch (e: BadCommandLineException) { 30 | // there was an error in the command line. 31 | // print usage and abort. 32 | if (e.message != null) { 33 | println("\n${e.message}") 34 | println() 35 | } 36 | 37 | usage() 38 | exitProcess(-1) 39 | } 40 | } 41 | 42 | fun usage() { 43 | println( 44 | """Usage: java -jar kotlin-xml-dsl-generator.jar [-options ...] ... [-b ] ... 45 | 46 | Options: 47 | -b : specify external JAXB bindings files 48 | This is experimental 49 | -d

: generated files will go into this directory 50 | -p : specifies the target package""" 51 | ) 52 | } 53 | 54 | class DslGenerator(private val opts: ExOptions) { 55 | fun generate(): String { 56 | val model = ModelLoader.load(opts, JCodeModel(), ErrReceiver()) 57 | ?: throw BadCommandLineException("Something failed generating the code model") 58 | 59 | val outline = BeanGenerator.generate(model, ErrReceiver()) 60 | 61 | val codeWriter = CodeWriter(outline) 62 | val schemaOutline = SchemaOutline(outline, opts) 63 | 64 | codeWriter.writeSuppress { 65 | addAll( 66 | listOf( 67 | "PropertyName", 68 | "ReplaceArrayOfWithLiteral", 69 | "LocalVariableName", 70 | "FunctionName", 71 | "RemoveRedundantBackticks" 72 | ) 73 | ) 74 | 75 | if (schemaOutline.enums.isNotEmpty()) { 76 | add("EnumEntryName") 77 | } 78 | } 79 | 80 | codeWriter.writePackage(opts.defaultPackage) 81 | codeWriter.writeImport("org.redundent.kotlin.xml.*\n") 82 | 83 | schemaOutline.enums.forEach { it.write(codeWriter) } 84 | 85 | schemaOutline.classes.forEach { it.write(codeWriter) } 86 | 87 | return codeWriter.asText() 88 | } 89 | } 90 | 91 | class ErrReceiver : ErrorReceiver() { 92 | override fun warning(exception: SAXParseException) { 93 | println(exception) 94 | } 95 | 96 | override fun info(exception: SAXParseException) { 97 | println(exception) 98 | } 99 | 100 | override fun error(exception: SAXParseException) = throw exception 101 | override fun fatalError(exception: SAXParseException) = throw exception 102 | } 103 | 104 | class ExOptions : Options() { 105 | var useMemberFunctions = false 106 | 107 | override fun parseArgument(args: Array, i: Int): Int { 108 | if (args[i] == "--use-member-functions") { 109 | useMemberFunctions = true 110 | return 1 111 | } 112 | 113 | return super.parseArgument(args, i) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/main/kotlin/org/redundent/kotlin/xml/gen/writer/XmlClass.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml.gen.writer 2 | 3 | import com.sun.tools.xjc.outline.ClassOutline 4 | import org.redundent.kotlin.xml.gen.ExOptions 5 | 6 | class XmlClass( 7 | val clazz: ClassOutline, 8 | private val opts: ExOptions, 9 | innerClass: Boolean = false 10 | ) : Code { 11 | val name: String = clazz.target.shortName 12 | val xmlns: String? = 13 | if (clazz.target.isElement && clazz.target.elementName.namespaceURI.isNotBlank()) clazz.target.elementName.namespaceURI else null 14 | 15 | private val superClassName: String = 16 | clazz.superClass?.target?.let { "${it.ownerPackage.name()}.`${it.shortName}`" } ?: "Node" 17 | private val abstract: Boolean = clazz.target.isAbstract 18 | private val attributes: MutableList = ArrayList() 19 | private val memberElements: MutableList = ArrayList() 20 | private var rootElement: XmlElement? = null 21 | private val innerClasses: MutableList = ArrayList() 22 | 23 | private val modifier = when { 24 | abstract -> "abstract" 25 | innerClass -> "inner" 26 | else -> "open" 27 | } 28 | 29 | private val constructorArg = if (!innerClass) "(nodeName: String)" else "" 30 | private val superClassConstructorArg = if (innerClass) "\"$name\"" else "nodeName" 31 | 32 | private val hasBody: Boolean 33 | get() = attributes.isNotEmpty() || xmlns != null || innerClasses.isNotEmpty() || (opts.useMemberFunctions && memberElements.isNotEmpty()) 34 | 35 | init { 36 | clazz.target.attributes.map { 37 | val field = clazz.parent().getField(it) 38 | val name = it.xmlName.localPart 39 | val type = "${CodeWriter.mapType(field.rawType.fullName())}${if (!it.isRequired) "?" else ""}" 40 | 41 | XmlAttribute(name, type, it.isRequired) 42 | }.forEach { attributes.add(it) } 43 | 44 | clazz.target.elements.forEach { element -> 45 | element.types.forEach { type -> 46 | memberElements.add( 47 | XmlElement( 48 | type.tagName.localPart, 49 | type.target, 50 | type.tagName.localPart, 51 | element.documentation, 52 | if (opts.useMemberFunctions) null else clazz.target 53 | ) 54 | ) 55 | } 56 | } 57 | 58 | if (clazz.target.isElement) { 59 | val name = clazz.target.elementName.localPart 60 | rootElement = XmlElement(name, clazz.target, name, clazz.target.documentation) 61 | } 62 | } 63 | 64 | override fun write(codeWriter: CodeWriter) { 65 | with(codeWriter) { 66 | writeKotlinDoc(clazz.target.documentation) 67 | 68 | if (clazz.target.isOrdered && clazz.target.elements.size > 1) { 69 | writeln("@XmlType(childOrder = arrayOf(${clazz.target.elements.joinToString(",\n\t\t") { "\"${it.types.first().tagName.localPart}\"" }}))") 70 | } 71 | writeln("$modifier class `$name`$constructorArg : $superClassName($superClassConstructorArg)${if (hasBody) " {" else "\n"}") 72 | indent() 73 | 74 | if (xmlns != null) { 75 | writeBlock { 76 | """ 77 | init { 78 | xmlns = "$xmlns" 79 | } 80 | """ 81 | } 82 | } 83 | 84 | if (attributes.isNotEmpty()) { 85 | attributes.forEach { it.write(this) } 86 | writeln() 87 | } 88 | 89 | if (opts.useMemberFunctions) { 90 | if (memberElements.isNotEmpty()) { 91 | memberElements.forEach { it.write(this) } 92 | } 93 | } 94 | 95 | if (innerClasses.isNotEmpty()) { 96 | innerClasses.forEach { it.write(this) } 97 | } 98 | 99 | if (hasBody) { 100 | trimLastNewLine() 101 | } 102 | 103 | dedent() 104 | 105 | if (hasBody) { 106 | writeln("}\n") 107 | } 108 | 109 | if (!opts.useMemberFunctions) { 110 | memberElements.forEach { it.write(this) } 111 | } 112 | 113 | rootElement?.write(this) 114 | } 115 | } 116 | 117 | fun addInnerClass(xmlClass: XmlClass) { 118 | innerClasses.add(xmlClass) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/main/kotlin/org/redundent/kotlin/xml/XmlBuilder.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | import org.w3c.dom.Document 4 | import org.xml.sax.InputSource 5 | import java.io.File 6 | import java.io.InputStream 7 | import javax.xml.parsers.DocumentBuilderFactory 8 | import kotlin.math.min 9 | import org.w3c.dom.Node as W3CNode 10 | 11 | internal fun getLineEnding(printOptions: PrintOptions) = if (printOptions.pretty) System.lineSeparator() else "" 12 | 13 | /** 14 | * Creates a new XML document with the specified root element name. 15 | * 16 | * @param root The root element name 17 | * @param encoding The encoding to use for the XML prolog 18 | * @param version The XML specification version to use for the xml prolog and attribute encoding 19 | * @param namespace Optional namespace object to use to build the name of the attribute. This will also add an `xmlns` 20 | * attribute for this value 21 | * @param init The block that defines the content of the XML 22 | */ 23 | fun xml( 24 | root: String, 25 | encoding: String? = null, 26 | version: XmlVersion? = null, 27 | namespace: Namespace? = null, 28 | init: (Node.() -> Unit)? = null 29 | ): Node { 30 | val node = Node(buildName(root, namespace)) 31 | if (encoding != null) { 32 | node.encoding = encoding 33 | } 34 | 35 | if (version != null) { 36 | node.version = version 37 | } 38 | 39 | if (init != null) { 40 | node.init() 41 | } 42 | 43 | if (namespace != null) { 44 | node.namespace(namespace) 45 | } 46 | return node 47 | } 48 | 49 | /** 50 | * Creates a new XML document with the specified root element name. 51 | * 52 | * @param name The name of the element 53 | * @param init The block that defines the content of the XML 54 | */ 55 | fun node(name: String, namespace: Namespace? = null, init: (Node.() -> Unit)? = null): Node { 56 | val node = Node(buildName(name, namespace)) 57 | if (init != null) { 58 | node.init() 59 | } 60 | return node 61 | } 62 | 63 | fun parse(f: File): Node = parse(DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(f)) 64 | fun parse(uri: String): Node = parse(DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(uri)) 65 | fun parse(inputSource: InputSource): Node = 66 | parse(DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputSource)) 67 | 68 | fun parse(inputStream: InputStream): Node = 69 | parse(DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)) 70 | 71 | fun parse(inputStream: InputStream, systemId: String): Node = 72 | parse(DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream, systemId)) 73 | 74 | fun parse(document: Document): Node { 75 | val root = document.documentElement 76 | 77 | val result = xml(root.tagName) 78 | 79 | copyAttributes(root, result) 80 | 81 | val children = root.childNodes 82 | (0 until children.length) 83 | .map(children::item) 84 | .forEach { copy(it, result) } 85 | 86 | return result 87 | } 88 | 89 | private fun copy(source: W3CNode, dest: Node) { 90 | when (source.nodeType) { 91 | W3CNode.ELEMENT_NODE -> { 92 | val cur = dest.element(source.nodeName) 93 | 94 | copyAttributes(source, cur) 95 | 96 | val children = source.childNodes 97 | (0 until children.length) 98 | .map(children::item) 99 | .forEach { copy(it, cur) } 100 | } 101 | 102 | W3CNode.CDATA_SECTION_NODE -> { 103 | dest.cdata(source.nodeValue) 104 | } 105 | 106 | W3CNode.TEXT_NODE -> { 107 | dest.text(source.nodeValue.trim { it.isWhitespace() || it == '\r' || it == '\n' }) 108 | } 109 | } 110 | } 111 | 112 | private fun copyAttributes(source: W3CNode, dest: Node) { 113 | val attributes = source.attributes 114 | if (attributes == null || attributes.length == 0) { 115 | return 116 | } 117 | 118 | (0 until attributes.length) 119 | .map(attributes::item) 120 | .forEach { 121 | if (it.nodeName.startsWith("xmlns")) { 122 | dest.namespace(it.nodeName.substring(min(6, it.nodeName.length)), it.nodeValue) 123 | } else { 124 | dest.attribute(it.nodeName, it.nodeValue) 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/schema/sitemap.xsd: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | XML Schema for Sitemap files. 9 | Last Modifed 2008-03-26 10 | 11 | 12 | 13 | 14 | 15 | 16 | Container for a set of up to 50,000 document elements. 17 | This is the root element of the XML file. 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Container for the data needed to describe a document to crawl. 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | REQUIRED: The location URI of a document. 47 | The URI must conform to RFC 2396 (http://www.ietf.org/rfc/rfc2396.txt). 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | OPTIONAL: The date the document was last modified. The date must conform 60 | to the W3C DATETIME format (http://www.w3.org/TR/NOTE-datetime). 61 | Example: 2005-05-10 62 | Lastmod may also contain a timestamp. 63 | Example: 2005-05-10T17:33:30+08:00 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | OPTIONAL: Indicates how frequently the content at a particular URL is 80 | likely to change. The value "always" should be used to describe 81 | documents that change each time they are accessed. The value "never" 82 | should be used to describe archived URLs. Please note that web 83 | crawlers may not necessarily crawl pages marked "always" more often. 84 | Consider this element as a friendly suggestion and not a command. 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | OPTIONAL: The priority of a particular URL relative to other pages 102 | on the same site. The value for this element is a number between 103 | 0.0 and 1.0 where 0.0 identifies the lowest priority page(s). 104 | The default priority of a page is 0.5. Priority is used to select 105 | between pages on your site. Setting a priority of 1.0 for all URLs 106 | will not help you, as the relative priority of pages on your site 107 | is what will be considered. 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/main/kotlin/org/redundent/kotlin/xml/gen/writer/CodeWriter.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml.gen.writer 2 | 3 | import com.sun.tools.xjc.model.CAttributePropertyInfo 4 | import com.sun.tools.xjc.model.CClassInfo 5 | import com.sun.tools.xjc.model.CElementPropertyInfo 6 | import com.sun.tools.xjc.model.CPropertyInfo 7 | import com.sun.tools.xjc.model.CTypeInfo 8 | import com.sun.tools.xjc.outline.Outline 9 | import com.sun.tools.xjc.reader.xmlschema.bindinfo.BindInfo 10 | import com.sun.xml.xsom.XSComponent 11 | import java.util.Date 12 | import kotlin.math.floor 13 | 14 | class CodeWriter(private val outline: Outline) { 15 | private var output = StringBuilder() 16 | 17 | var currentIndex = 0 18 | private set 19 | 20 | fun writePackage(packageName: String) { 21 | writeln("package $packageName\n") 22 | } 23 | 24 | fun writeImport(importStatement: String) { 25 | writeln("import $importStatement") 26 | } 27 | 28 | fun writeKotlinDoc(doc: String?) { 29 | if (doc.isNullOrBlank()) { 30 | return 31 | } 32 | 33 | writeln("/**") 34 | doc.split("\n").map(String::trim).filter(String::isNotBlank).forEach { writeln(" * $it") } 35 | writeln(" */") 36 | } 37 | 38 | fun indent() = currentIndex++ 39 | 40 | fun dedent() = currentIndex-- 41 | 42 | fun write(text: String, writeIndex: Boolean = true) { 43 | if (writeIndex) { 44 | output.append((0 until currentIndex).joinToString("") { "\t" }) 45 | } 46 | output.append(text) 47 | } 48 | 49 | fun writeSuppress(block: MutableList.() -> Unit) { 50 | val list = ArrayList() 51 | list.block() 52 | 53 | writeln("@file:Suppress(${list.joinToString(", ") { "\"$it\"" }})") 54 | writeln() 55 | } 56 | 57 | fun writeln(text: String = "", writeIndex: Boolean = true) { 58 | if (text.isNotEmpty()) { 59 | write(text, writeIndex) 60 | } 61 | output.append("\n") 62 | } 63 | 64 | fun writeBlock(block: () -> String) { 65 | val text = block().trimStart() 66 | text.split("\n").forEach { writeln(it) } 67 | } 68 | 69 | fun asText(): String = output.toString().trim() 70 | 71 | fun trimLastNewLine() { 72 | if (output.endsWith('\n')) { 73 | output = StringBuilder(output.substring(0, output.lastIndex)) 74 | } 75 | } 76 | 77 | fun CClassInfo.attributesAsParameters(indentLength: Int): String { 78 | if (allAttributes.isEmpty()) { 79 | return "" 80 | } 81 | 82 | val sortedAttributes = allAttributes.sortedWith { o1, o2 -> (if (o1.isRequired) 0 else 1).compareTo(if (o2.isRequired) 0 else 1) } 83 | 84 | val numOfTabs = floor(indentLength.toDouble() / 4.0).toInt() 85 | val numOfSpaces = indentLength % 4 86 | 87 | val delimiter = ",\n${(0..numOfTabs).joinToString("\t") { "" }}${(0..numOfSpaces).joinToString(" ") { "" }}" 88 | 89 | return sortedAttributes.joinToString(delimiter) { 90 | val field = outline.getField(it) 91 | "`${it.xmlName.localPart}`: ${mapType(field.rawType.fullName())}${if (!it.isRequired) "? = null" else ""}" 92 | } + delimiter 93 | } 94 | 95 | companion object { 96 | fun mapType(type: String): String { 97 | return when (type) { 98 | "int", 99 | java.lang.Integer::class.java.name, 100 | java.math.BigInteger::class.java.name -> Int::class.qualifiedName 101 | "long", 102 | java.lang.Long::class.java.name -> Long::class.qualifiedName 103 | "boolean", 104 | java.lang.Boolean::class.java.name -> Boolean::class.qualifiedName 105 | "double", 106 | java.lang.Double::class.java.name -> Double::class.qualifiedName 107 | "float", 108 | java.lang.Float::class.java.name -> Float::class.qualifiedName 109 | java.lang.String::class.java.name, 110 | javax.xml.namespace.QName::class.java.name -> String::class.qualifiedName 111 | "byte", 112 | java.lang.Byte::class.java.name -> Byte::class.qualifiedName 113 | "short", 114 | java.lang.Short::class.java.name -> Short::class.qualifiedName 115 | "byte[]" -> ByteArray::class.qualifiedName 116 | javax.xml.datatype.XMLGregorianCalendar::class.java.name -> Date::class.qualifiedName 117 | else -> type 118 | }!! 119 | } 120 | } 121 | } 122 | 123 | val CClassInfo.attributes: List 124 | get() = properties.mapNotNull { it as? CAttributePropertyInfo } 125 | val CClassInfo.allAttributes: List 126 | get() = generateSequence(this) { it.baseClass }.toList().flatMap { it.properties.mapNotNull { p -> p as? CAttributePropertyInfo } } 127 | val CClassInfo.hasOptionalAttributes: Boolean 128 | get() = allAttributes.any { !it.isRequired } 129 | val CClassInfo.elements: List 130 | get() = properties.mapNotNull { it as? CElementPropertyInfo } 131 | val CPropertyInfo.documentation: String? 132 | get() = schemaComponent?.documentation 133 | val CTypeInfo.documentation: String? 134 | get() = schemaComponent?.documentation 135 | val XSComponent.documentation: String? 136 | get() = annotation?.annotation?.let { it as? BindInfo }?.documentation 137 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/kotlin/org/redundent/kotlin/xml/NodeTest.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | import org.junit.Test 4 | import kotlin.test.assertEquals 5 | import kotlin.test.assertFalse 6 | import kotlin.test.assertNotEquals 7 | import kotlin.test.assertSame 8 | 9 | class NodeTest : TestBase() { 10 | @Test 11 | fun `equals null`() { 12 | val xml = xml("test") 13 | 14 | assertFalse(xml.equals(null)) 15 | } 16 | 17 | @Test 18 | fun `equals different type`() { 19 | val xml = xml("test") 20 | val other = TextElement("test") 21 | 22 | assertFalse(xml == other) 23 | } 24 | 25 | @Test 26 | fun `equals different name`() { 27 | val xml1 = xml("test1") 28 | val xml2 = xml("test2") 29 | 30 | assertNotEquals(xml1, xml2) 31 | assertNotEquals(xml2, xml1) 32 | } 33 | 34 | @Test 35 | fun equals() { 36 | val xml1 = xml("complex_node", encoding = "utf-8", version = XmlVersion.V11) { 37 | xmlns = "https://test.com" 38 | namespace("t", "https://t.co") 39 | 40 | globalProcessingInstruction("global_pi", "global" to "top_level") 41 | 42 | attribute("attr", "value") 43 | attribute("other_attr", "some text & more") 44 | 45 | processingInstruction("blah", "pi_attr" to "value") 46 | 47 | "child1"("text") 48 | 49 | "child2" { 50 | comment("comment1") 51 | } 52 | } 53 | 54 | val xml2 = xml("complex_node", encoding = "utf-8", version = XmlVersion.V11) { 55 | xmlns = "https://test.com" 56 | namespace("t", "https://t.co") 57 | 58 | globalProcessingInstruction("global_pi", "global" to "top_level") 59 | 60 | attribute("attr", "value") 61 | attribute("other_attr", "some text & more") 62 | 63 | processingInstruction("blah", "pi_attr" to "value") 64 | 65 | "child1"("text") 66 | 67 | "child2" { 68 | comment("comment1") 69 | } 70 | } 71 | 72 | assertEquals(xml1, xml2) 73 | assertEquals(xml2, xml1) 74 | } 75 | 76 | @Test 77 | fun `equals slight difference`() { 78 | val xml1 = xml("complex_node", encoding = "utf-8", version = XmlVersion.V11) { 79 | xmlns = "https://test.com" 80 | namespace("t", "https://t.co") 81 | 82 | globalProcessingInstruction("global_pi", "global" to "top_level") 83 | 84 | attribute("attr", "value") 85 | attribute("other_attr", "some text & more") 86 | 87 | processingInstruction("blah", "pi_attr" to "value") 88 | 89 | "child1"("text") 90 | 91 | "child2" { 92 | comment("comment1") 93 | } 94 | } 95 | 96 | val xml2 = xml("complex_node", encoding = "utf-8", version = XmlVersion.V11) { 97 | xmlns = "https://test.com" 98 | namespace("t", "https://t.co") 99 | 100 | globalProcessingInstruction("global_pi", "global" to "top_level") 101 | 102 | attribute("attr", "value") 103 | attribute("other_attr", "some text & more") 104 | 105 | processingInstruction("blah", "pi_attr" to "value") 106 | 107 | "child1"("text") 108 | 109 | "child2" { 110 | comment("comment2") 111 | } 112 | } 113 | 114 | assertNotEquals(xml1, xml2) 115 | assertNotEquals(xml2, xml1) 116 | } 117 | 118 | @Suppress("ReplaceGetOrSet") 119 | @Test 120 | fun set() { 121 | val xml = xml("root") 122 | 123 | xml.set("myAttr", "myValue") 124 | 125 | assertEquals("myValue" as String?, xml.get("myAttr")) 126 | } 127 | 128 | @Suppress("ReplaceGetOrSet") 129 | @Test 130 | fun `set null`() { 131 | val xml = xml("root") 132 | 133 | xml.set("myAttr", "myValue") 134 | assertEquals("myValue" as String?, xml.get("myAttr")) 135 | 136 | xml.set("myAttr", null) 137 | assertFalse(xml.hasAttribute("myAttr")) 138 | } 139 | 140 | @Test 141 | fun `addElements varargs`() { 142 | val xml = xml("root") 143 | 144 | val text = TextElement("test") 145 | val cdata = CDATAElement("cdata") 146 | 147 | xml.addElements(text, cdata) 148 | 149 | assertSame(text, xml.children[0], "first child is text element") 150 | assertSame(cdata, xml.children[1], "second child is cdata element") 151 | } 152 | 153 | @Test 154 | fun `addElements iterable`() { 155 | val xml = xml("root") 156 | 157 | val text = TextElement("test") 158 | val cdata = CDATAElement("cdata") 159 | 160 | xml.addElements(listOf(text, cdata)) 161 | 162 | assertSame(text, xml.children[0], "first child is text element") 163 | assertSame(cdata, xml.children[1], "second child is cdata element") 164 | } 165 | 166 | @Test(expected = IllegalArgumentException::class) 167 | fun `addElementsAfter not found`() { 168 | val xml = xml("root") 169 | val text = TextElement("test") 170 | xml.addElements(text) 171 | 172 | val after = CDATAElement("cdata") 173 | 174 | xml.addElementsAfter(after, TextElement("new")) 175 | } 176 | 177 | @Test 178 | fun addElementsAfter() { 179 | val after = node("third") 180 | 181 | val xml = xml("root") { 182 | "first"("") 183 | "second"("") 184 | addElement(after) 185 | "fourth"("") 186 | "fifth"("") 187 | } 188 | 189 | xml.addElementsAfter( 190 | after, 191 | node("new1"), 192 | node("new2") 193 | ) 194 | 195 | validate( 196 | xml, 197 | PrintOptions( 198 | singleLineTextElements = true, 199 | useSelfClosingTags = true 200 | ) 201 | ) 202 | } 203 | 204 | @Test(expected = IllegalArgumentException::class) 205 | fun `addElementsBefore not found`() { 206 | val xml = xml("root") 207 | val text = TextElement("test") 208 | xml.addElements(text) 209 | 210 | val before = CDATAElement("cdata") 211 | 212 | xml.addElementsBefore(before, TextElement("new")) 213 | } 214 | 215 | @Test 216 | fun addElementsBefore() { 217 | val before = node("third") 218 | 219 | val xml = xml("root") { 220 | "first"("") 221 | "second"("") 222 | addElement(before) 223 | "fourth"("") 224 | "fifth"("") 225 | } 226 | 227 | xml.addElementsBefore( 228 | before, 229 | node("new1"), 230 | node("new2") 231 | ) 232 | 233 | validate( 234 | xml, 235 | PrintOptions( 236 | singleLineTextElements = true, 237 | useSelfClosingTags = true 238 | ) 239 | ) 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/code/attributes-required.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName", "ReplaceArrayOfWithLiteral", "LocalVariableName", "FunctionName", "RemoveRedundantBackticks") 2 | 3 | package org.redundent.generated 4 | 5 | import org.redundent.kotlin.xml.* 6 | 7 | open class `AttributesRequired`(nodeName: String) : Node(nodeName) { 8 | var `uri`: kotlin.String 9 | get() = get("uri")!! 10 | set(value) { set("uri", value) } 11 | var `base64Binary`: kotlin.ByteArray 12 | get() = get("base64Binary")!! 13 | set(value) { set("base64Binary", value) } 14 | var `boolean`: kotlin.Boolean 15 | get() = get("boolean")!! 16 | set(value) { set("boolean", value) } 17 | var `byte`: kotlin.Byte 18 | get() = get("byte")!! 19 | set(value) { set("byte", value) } 20 | var `date`: java.util.Date 21 | get() = get("date")!! 22 | set(value) { set("date", value) } 23 | var `dateTime`: java.util.Date 24 | get() = get("dateTime")!! 25 | set(value) { set("dateTime", value) } 26 | var `decimal`: java.math.BigDecimal 27 | get() = get("decimal")!! 28 | set(value) { set("decimal", value) } 29 | var `double`: kotlin.Double 30 | get() = get("double")!! 31 | set(value) { set("double", value) } 32 | var `duration`: javax.xml.datatype.Duration 33 | get() = get("duration")!! 34 | set(value) { set("duration", value) } 35 | var `float`: kotlin.Float 36 | get() = get("float")!! 37 | set(value) { set("float", value) } 38 | var `gDay`: java.util.Date 39 | get() = get("gDay")!! 40 | set(value) { set("gDay", value) } 41 | var `gMonth`: java.util.Date 42 | get() = get("gMonth")!! 43 | set(value) { set("gMonth", value) } 44 | var `gMonthDay`: java.util.Date 45 | get() = get("gMonthDay")!! 46 | set(value) { set("gMonthDay", value) } 47 | var `gYear`: java.util.Date 48 | get() = get("gYear")!! 49 | set(value) { set("gYear", value) } 50 | var `gYearMonth`: java.util.Date 51 | get() = get("gYearMonth")!! 52 | set(value) { set("gYearMonth", value) } 53 | var `hexBinary`: kotlin.ByteArray 54 | get() = get("hexBinary")!! 55 | set(value) { set("hexBinary", value) } 56 | var `int`: kotlin.Int 57 | get() = get("int")!! 58 | set(value) { set("int", value) } 59 | var `integer`: kotlin.Int 60 | get() = get("integer")!! 61 | set(value) { set("integer", value) } 62 | var `long`: kotlin.Long 63 | get() = get("long")!! 64 | set(value) { set("long", value) } 65 | var `negativeInteger`: kotlin.Int 66 | get() = get("negativeInteger")!! 67 | set(value) { set("negativeInteger", value) } 68 | var `nonNegativeInteger`: kotlin.Int 69 | get() = get("nonNegativeInteger")!! 70 | set(value) { set("nonNegativeInteger", value) } 71 | var `nonPositiveInteger`: kotlin.Int 72 | get() = get("nonPositiveInteger")!! 73 | set(value) { set("nonPositiveInteger", value) } 74 | var `positiveInteger`: kotlin.Int 75 | get() = get("positiveInteger")!! 76 | set(value) { set("positiveInteger", value) } 77 | var `string`: kotlin.String 78 | get() = get("string")!! 79 | set(value) { set("string", value) } 80 | var `short`: kotlin.Short 81 | get() = get("short")!! 82 | set(value) { set("short", value) } 83 | var `time`: java.util.Date 84 | get() = get("time")!! 85 | set(value) { set("time", value) } 86 | var `token`: kotlin.String 87 | get() = get("token")!! 88 | set(value) { set("token", value) } 89 | var `unsignedByte`: kotlin.Short 90 | get() = get("unsignedByte")!! 91 | set(value) { set("unsignedByte", value) } 92 | var `unsignedInt`: kotlin.Long 93 | get() = get("unsignedInt")!! 94 | set(value) { set("unsignedInt", value) } 95 | var `unsignedLong`: kotlin.Int 96 | get() = get("unsignedLong")!! 97 | set(value) { set("unsignedLong", value) } 98 | var `unsignedShort`: kotlin.Int 99 | get() = get("unsignedShort")!! 100 | set(value) { set("unsignedShort", value) } 101 | } 102 | 103 | fun `attributesRequired`(`uri`: kotlin.String, 104 | `base64Binary`: kotlin.ByteArray, 105 | `boolean`: kotlin.Boolean, 106 | `byte`: kotlin.Byte, 107 | `date`: java.util.Date, 108 | `dateTime`: java.util.Date, 109 | `decimal`: java.math.BigDecimal, 110 | `double`: kotlin.Double, 111 | `duration`: javax.xml.datatype.Duration, 112 | `float`: kotlin.Float, 113 | `gDay`: java.util.Date, 114 | `gMonth`: java.util.Date, 115 | `gMonthDay`: java.util.Date, 116 | `gYear`: java.util.Date, 117 | `gYearMonth`: java.util.Date, 118 | `hexBinary`: kotlin.ByteArray, 119 | `int`: kotlin.Int, 120 | `integer`: kotlin.Int, 121 | `long`: kotlin.Long, 122 | `negativeInteger`: kotlin.Int, 123 | `nonNegativeInteger`: kotlin.Int, 124 | `nonPositiveInteger`: kotlin.Int, 125 | `positiveInteger`: kotlin.Int, 126 | `string`: kotlin.String, 127 | `short`: kotlin.Short, 128 | `time`: java.util.Date, 129 | `token`: kotlin.String, 130 | `unsignedByte`: kotlin.Short, 131 | `unsignedInt`: kotlin.Long, 132 | `unsignedLong`: kotlin.Int, 133 | `unsignedShort`: kotlin.Int, 134 | __block__: `AttributesRequired`.() -> Unit): `AttributesRequired` { 135 | val `attributesRequired` = `AttributesRequired`("attributesRequired") 136 | `attributesRequired`.apply { 137 | this.`uri` = `uri` 138 | this.`base64Binary` = `base64Binary` 139 | this.`boolean` = `boolean` 140 | this.`byte` = `byte` 141 | this.`date` = `date` 142 | this.`dateTime` = `dateTime` 143 | this.`decimal` = `decimal` 144 | this.`double` = `double` 145 | this.`duration` = `duration` 146 | this.`float` = `float` 147 | this.`gDay` = `gDay` 148 | this.`gMonth` = `gMonth` 149 | this.`gMonthDay` = `gMonthDay` 150 | this.`gYear` = `gYear` 151 | this.`gYearMonth` = `gYearMonth` 152 | this.`hexBinary` = `hexBinary` 153 | this.`int` = `int` 154 | this.`integer` = `integer` 155 | this.`long` = `long` 156 | this.`negativeInteger` = `negativeInteger` 157 | this.`nonNegativeInteger` = `nonNegativeInteger` 158 | this.`nonPositiveInteger` = `nonPositiveInteger` 159 | this.`positiveInteger` = `positiveInteger` 160 | this.`string` = `string` 161 | this.`short` = `short` 162 | this.`time` = `time` 163 | this.`token` = `token` 164 | this.`unsignedByte` = `unsignedByte` 165 | this.`unsignedInt` = `unsignedInt` 166 | this.`unsignedLong` = `unsignedLong` 167 | this.`unsignedShort` = `unsignedShort` 168 | } 169 | `attributesRequired`.apply(__block__) 170 | return `attributesRequired` 171 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /kotlin-xml-dsl-generator/src/test/resources/code/attributes.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName", "ReplaceArrayOfWithLiteral", "LocalVariableName", "FunctionName", "RemoveRedundantBackticks") 2 | 3 | package org.redundent.generated 4 | 5 | import org.redundent.kotlin.xml.* 6 | 7 | open class `Attributes`(nodeName: String) : Node(nodeName) { 8 | var `uri`: kotlin.String? 9 | get() = get("uri") 10 | set(value) { set("uri", value) } 11 | var `base64Binary`: kotlin.ByteArray? 12 | get() = get("base64Binary") 13 | set(value) { set("base64Binary", value) } 14 | var `boolean`: kotlin.Boolean? 15 | get() = get("boolean") 16 | set(value) { set("boolean", value) } 17 | var `byte`: kotlin.Byte? 18 | get() = get("byte") 19 | set(value) { set("byte", value) } 20 | var `date`: java.util.Date? 21 | get() = get("date") 22 | set(value) { set("date", value) } 23 | var `dateTime`: java.util.Date? 24 | get() = get("dateTime") 25 | set(value) { set("dateTime", value) } 26 | var `decimal`: java.math.BigDecimal? 27 | get() = get("decimal") 28 | set(value) { set("decimal", value) } 29 | var `double`: kotlin.Double? 30 | get() = get("double") 31 | set(value) { set("double", value) } 32 | var `duration`: javax.xml.datatype.Duration? 33 | get() = get("duration") 34 | set(value) { set("duration", value) } 35 | var `float`: kotlin.Float? 36 | get() = get("float") 37 | set(value) { set("float", value) } 38 | var `gDay`: java.util.Date? 39 | get() = get("gDay") 40 | set(value) { set("gDay", value) } 41 | var `gMonth`: java.util.Date? 42 | get() = get("gMonth") 43 | set(value) { set("gMonth", value) } 44 | var `gMonthDay`: java.util.Date? 45 | get() = get("gMonthDay") 46 | set(value) { set("gMonthDay", value) } 47 | var `gYear`: java.util.Date? 48 | get() = get("gYear") 49 | set(value) { set("gYear", value) } 50 | var `gYearMonth`: java.util.Date? 51 | get() = get("gYearMonth") 52 | set(value) { set("gYearMonth", value) } 53 | var `hexBinary`: kotlin.ByteArray? 54 | get() = get("hexBinary") 55 | set(value) { set("hexBinary", value) } 56 | var `int`: kotlin.Int? 57 | get() = get("int") 58 | set(value) { set("int", value) } 59 | var `integer`: kotlin.Int? 60 | get() = get("integer") 61 | set(value) { set("integer", value) } 62 | var `long`: kotlin.Long? 63 | get() = get("long") 64 | set(value) { set("long", value) } 65 | var `negativeInteger`: kotlin.Int? 66 | get() = get("negativeInteger") 67 | set(value) { set("negativeInteger", value) } 68 | var `nonNegativeInteger`: kotlin.Int? 69 | get() = get("nonNegativeInteger") 70 | set(value) { set("nonNegativeInteger", value) } 71 | var `nonPositiveInteger`: kotlin.Int? 72 | get() = get("nonPositiveInteger") 73 | set(value) { set("nonPositiveInteger", value) } 74 | var `positiveInteger`: kotlin.Int? 75 | get() = get("positiveInteger") 76 | set(value) { set("positiveInteger", value) } 77 | var `string`: kotlin.String? 78 | get() = get("string") 79 | set(value) { set("string", value) } 80 | var `short`: kotlin.Short? 81 | get() = get("short") 82 | set(value) { set("short", value) } 83 | var `time`: java.util.Date? 84 | get() = get("time") 85 | set(value) { set("time", value) } 86 | var `token`: kotlin.String? 87 | get() = get("token") 88 | set(value) { set("token", value) } 89 | var `unsignedByte`: kotlin.Short? 90 | get() = get("unsignedByte") 91 | set(value) { set("unsignedByte", value) } 92 | var `unsignedInt`: kotlin.Long? 93 | get() = get("unsignedInt") 94 | set(value) { set("unsignedInt", value) } 95 | var `unsignedLong`: kotlin.Int? 96 | get() = get("unsignedLong") 97 | set(value) { set("unsignedLong", value) } 98 | var `unsignedShort`: kotlin.Int? 99 | get() = get("unsignedShort") 100 | set(value) { set("unsignedShort", value) } 101 | } 102 | 103 | @JvmOverloads 104 | fun `attributes`(`uri`: kotlin.String? = null, 105 | `base64Binary`: kotlin.ByteArray? = null, 106 | `boolean`: kotlin.Boolean? = null, 107 | `byte`: kotlin.Byte? = null, 108 | `date`: java.util.Date? = null, 109 | `dateTime`: java.util.Date? = null, 110 | `decimal`: java.math.BigDecimal? = null, 111 | `double`: kotlin.Double? = null, 112 | `duration`: javax.xml.datatype.Duration? = null, 113 | `float`: kotlin.Float? = null, 114 | `gDay`: java.util.Date? = null, 115 | `gMonth`: java.util.Date? = null, 116 | `gMonthDay`: java.util.Date? = null, 117 | `gYear`: java.util.Date? = null, 118 | `gYearMonth`: java.util.Date? = null, 119 | `hexBinary`: kotlin.ByteArray? = null, 120 | `int`: kotlin.Int? = null, 121 | `integer`: kotlin.Int? = null, 122 | `long`: kotlin.Long? = null, 123 | `negativeInteger`: kotlin.Int? = null, 124 | `nonNegativeInteger`: kotlin.Int? = null, 125 | `nonPositiveInteger`: kotlin.Int? = null, 126 | `positiveInteger`: kotlin.Int? = null, 127 | `string`: kotlin.String? = null, 128 | `short`: kotlin.Short? = null, 129 | `time`: java.util.Date? = null, 130 | `token`: kotlin.String? = null, 131 | `unsignedByte`: kotlin.Short? = null, 132 | `unsignedInt`: kotlin.Long? = null, 133 | `unsignedLong`: kotlin.Int? = null, 134 | `unsignedShort`: kotlin.Int? = null, 135 | __block__: `Attributes`.() -> Unit): `Attributes` { 136 | val `attributes` = `Attributes`("attributes") 137 | `attributes`.apply { 138 | if (`uri` != null) { 139 | this.`uri` = `uri` 140 | } 141 | if (`base64Binary` != null) { 142 | this.`base64Binary` = `base64Binary` 143 | } 144 | if (`boolean` != null) { 145 | this.`boolean` = `boolean` 146 | } 147 | if (`byte` != null) { 148 | this.`byte` = `byte` 149 | } 150 | if (`date` != null) { 151 | this.`date` = `date` 152 | } 153 | if (`dateTime` != null) { 154 | this.`dateTime` = `dateTime` 155 | } 156 | if (`decimal` != null) { 157 | this.`decimal` = `decimal` 158 | } 159 | if (`double` != null) { 160 | this.`double` = `double` 161 | } 162 | if (`duration` != null) { 163 | this.`duration` = `duration` 164 | } 165 | if (`float` != null) { 166 | this.`float` = `float` 167 | } 168 | if (`gDay` != null) { 169 | this.`gDay` = `gDay` 170 | } 171 | if (`gMonth` != null) { 172 | this.`gMonth` = `gMonth` 173 | } 174 | if (`gMonthDay` != null) { 175 | this.`gMonthDay` = `gMonthDay` 176 | } 177 | if (`gYear` != null) { 178 | this.`gYear` = `gYear` 179 | } 180 | if (`gYearMonth` != null) { 181 | this.`gYearMonth` = `gYearMonth` 182 | } 183 | if (`hexBinary` != null) { 184 | this.`hexBinary` = `hexBinary` 185 | } 186 | if (`int` != null) { 187 | this.`int` = `int` 188 | } 189 | if (`integer` != null) { 190 | this.`integer` = `integer` 191 | } 192 | if (`long` != null) { 193 | this.`long` = `long` 194 | } 195 | if (`negativeInteger` != null) { 196 | this.`negativeInteger` = `negativeInteger` 197 | } 198 | if (`nonNegativeInteger` != null) { 199 | this.`nonNegativeInteger` = `nonNegativeInteger` 200 | } 201 | if (`nonPositiveInteger` != null) { 202 | this.`nonPositiveInteger` = `nonPositiveInteger` 203 | } 204 | if (`positiveInteger` != null) { 205 | this.`positiveInteger` = `positiveInteger` 206 | } 207 | if (`string` != null) { 208 | this.`string` = `string` 209 | } 210 | if (`short` != null) { 211 | this.`short` = `short` 212 | } 213 | if (`time` != null) { 214 | this.`time` = `time` 215 | } 216 | if (`token` != null) { 217 | this.`token` = `token` 218 | } 219 | if (`unsignedByte` != null) { 220 | this.`unsignedByte` = `unsignedByte` 221 | } 222 | if (`unsignedInt` != null) { 223 | this.`unsignedInt` = `unsignedInt` 224 | } 225 | if (`unsignedLong` != null) { 226 | this.`unsignedLong` = `unsignedLong` 227 | } 228 | if (`unsignedShort` != null) { 229 | this.`unsignedShort` = `unsignedShort` 230 | } 231 | } 232 | `attributes`.apply(__block__) 233 | return `attributes` 234 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![CI](https://github.com/redundent/kotlin-xml-builder/workflows/CI/badge.svg) 2 | [![Download](https://maven-badges.herokuapp.com/maven-central/org.redundent/kotlin-xml-builder/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.redundent/kotlin-xml-builder) 3 | 4 | Kotlin XML Builder 5 | = 6 | 7 | This library can be used to build xml documents from Kotlin code. 8 | It is based on the HTML builder described in the [Kotlin docs](https://kotlinlang.org/docs/reference/type-safe-builders.html) 9 | 10 | It is designed to be lightweight and fast. There isn't any validation except to 11 | escape text to not violate xml standards. 12 | 13 | License 14 | - 15 | Apache 2.0 16 | 17 | Usage 18 | = 19 | To use in Gradle, simply add the Maven Central repository and then add the following dependency. 20 | ```gradle 21 | repositories { 22 | mavenCentral() 23 | } 24 | 25 | dependencies { 26 | compile("org.redundent:kotlin-xml-builder:[VERSION]") 27 | } 28 | ``` 29 | 30 | Similarly in Maven: 31 | ```xml 32 | 33 | 34 | org.redundent 35 | kotlin-xml-builder 36 | [VERSION] 37 | 38 | 39 | ``` 40 | 41 | Example 42 | = 43 | ```kotlin 44 | val people = xml("people") { 45 | xmlns = "http://example.com/people" 46 | "person" { 47 | attribute("id", 1) 48 | "firstName" { 49 | -"John" 50 | } 51 | "lastName" { 52 | -"Doe" 53 | } 54 | "phone" { 55 | -"555-555-5555" 56 | } 57 | } 58 | } 59 | 60 | val asString = people.toString() 61 | ``` 62 | produces 63 | ```xml 64 | 65 | 66 | 67 | John 68 | 69 | 70 | Doe 71 | 72 | 73 | 555-555-5555 74 | 75 | 76 | 77 | ``` 78 | 79 | ```kotlin 80 | class Person(val id: Long, val firstName: String, val lastName: String, val phone: String) 81 | 82 | val listOfPeople = listOf( 83 | Person(1, "John", "Doe", "555-555-5555"), 84 | Person(2, "Jane", "Doe", "555-555-6666") 85 | ) 86 | 87 | val people = xml("people") { 88 | xmlns = "http://example.com/people" 89 | for (person in listOfPeople) { 90 | "person" { 91 | attribute("id", person.id) 92 | "firstName" { 93 | -person.firstName 94 | } 95 | "lastName" { 96 | -person.lastName 97 | } 98 | "phone" { 99 | -person.phone 100 | } 101 | } 102 | } 103 | } 104 | 105 | val asString = people.toString() 106 | ``` 107 | produces 108 | ```xml 109 | 110 | 111 | 112 | John 113 | 114 | Doe 116 | 117 | 118 | 555-555-5555 119 | 120 | 121 | 122 | 123 | Jane 124 | 125 | 126 | Doe 127 | 128 | 129 | 555-555-6666 130 | 131 | 132 | 133 | ``` 134 | 135 | ### Namespaces 136 | Version 1.8.0 added more precise control over how namespaces are used. You can add a namespace to any element or attribute 137 | and the correct node/attribute name will be used automatically. When using the new namespace aware methods, you no longer 138 | need to manually add the namespace to the element. 139 | 140 | See examples of `< 1.8.0` and `>= 1.8.0` below to produce the following xml 141 | 142 | ```xml 143 | 144 | 145 | 146 | ``` 147 | 148 | #### < 1.8.0 149 | ```kotlin 150 | xml("t:root") { 151 | namespace("t", "https://ns.org") 152 | "t:element"("t:key" to "value") 153 | } 154 | ``` 155 | 156 | #### >= 1.8.0 157 | ```kotlin 158 | val ns = Namespace("t", "https://ns.org") 159 | xml("root", ns) { 160 | "element"(ns, Attribute("key", "value", ns)) 161 | } 162 | ``` 163 | 164 | You can also use the `Namespace("https://ns.org")` constructor to create a Namespace object that represents the default xmlns. 165 | 166 | #### Things to be aware of 167 | 168 | * Previously, all namespace declarations would get added to the attributes maps immediately. That no long happens. All 169 | namespaces get added at render time. To retrieve a list of the current namespaces of a node, use the `namespaces` property. 170 | * When a namespace is provided for a node or attribute, it will be declared on that element IF it is not already declared 171 | on the root element or one of the element's parents. 172 | 173 | ### Processing Instructions 174 | You can add processing instructions to any element by using the `processingInstruction` method. 175 | 176 | ```kotlin 177 | xml("root") { 178 | processingInstruction("instruction") 179 | } 180 | ``` 181 | 182 | ```xml 183 | 184 | 185 | 186 | ``` 187 | 188 | #### Global Instructions 189 | Similarly you can add a global (top-level) instruction by call `globalProcessingInstruction` on the 190 | root node. This method only applies to the root. If it is called on any other element, it will be ignored. 191 | 192 | 193 | ```kotlin 194 | xml("root") { 195 | globalProcessingInstruction("xml-stylesheet", "type" to "text/xsl", "href" to "style.xsl") 196 | } 197 | ``` 198 | 199 | ```xml 200 | 201 | 202 | ``` 203 | 204 | ## DOCTYPE 205 | 206 | As of version 1.7.4, you can specify a DTD (Document Type Declaration). 207 | 208 | ```kotlin 209 | xml("root") { 210 | doctype(systemId = "mydtd.dtd") 211 | } 212 | ``` 213 | 214 | ```xml 215 | 216 | 217 | ``` 218 | 219 | ### Limitations with DTD 220 | 221 | Complex DTDs are not supported. 222 | 223 | ## Unsafe 224 | You can now use unsafe text for element and attribute values. 225 | ```kotlin 226 | xml("root") { 227 | unsafeText("") 228 | } 229 | ``` 230 | produces 231 | ```xml 232 | 233 | 234 | 235 | ``` 236 | 237 | ```kotlin 238 | xml("root") { 239 | attribute("attr", unsafe("{")) 240 | } 241 | ``` 242 | produces 243 | ```xml 244 | 245 | ``` 246 | 247 | ## Print Options 248 | You can now control how your xml will look when rendered by passing the new PrintOptions class as an argument to `toString`. 249 | 250 | `pretty` - This is the default and will produce the xml you see above. 251 | 252 | `singleLineTextElements` - This will render single text element nodes on a single line if `pretty` is true 253 | ```xml 254 | 255 | value 256 | 257 | ``` 258 | as opposed to: 259 | ```xml 260 | 261 | 262 | value 263 | 264 | 265 | ``` 266 | `useSelfClosingTags` - Use `` instead of `` for empty tags 267 | 268 | `useCharacterReference` - Use character references instead of escaped characters. i.e. `'` instead of `'` 269 | 270 | ## Reading XML 271 | You can also read xml documents using the `parse` methods. They provide basic 272 | xml parsing and will build a `Node` element to build upon. 273 | 274 | For more advanced consuming, check out [konsume-xml](https://gitlab.com/mvysny/konsume-xml). 275 | It includes many more features for consuming documents. 276 | 277 | Release Notes 278 | ============= 279 | Version 1.9.3 280 | - 281 | * Re-fixing issue where text elements are ignored when they are just whitespace 282 | 283 | Version 1.9.2 284 | - 285 | * Fixing issue where text elements are ignored when they are just whitespace\ 286 | Thanks to [@fiddlededee](https://github.com/fiddlededee) for finding this! 287 | 288 | Version 1.9.1 289 | - 290 | * Adding `addElement`, `addElements`, `addElementsBefore`, `addElementsAfter`, `removeElement`, 291 | `removeElements`, and `replaceElement` to Node.\ 292 | Thanks to [@csmile2](https://github.com/csmile2) for requesting this! 293 | * Deprecating `addNode`, `addNodeBefore`, `addNodeAfter`, `removeNode`, and `replaceNode` in favor of Element methods. 294 | * Adding ktlint checks 295 | 296 | Version 1.9.0 297 | - 298 | * Adding `unsafe` and `unsafeText` methods to allow for unescaped values in elements and attributes.\ 299 | Thanks to [@krzysztofsroga](https://github.com/krzysztofsroga) for requesting this! 300 | 301 | **BREAKING CHANGES** 302 | * The `attributes` property on a node will now return an immutable map (`Map` instead of `LinkedHashMap`). This property can no longer be used to\ 303 | manipulate the attributes. Use `set`/`removeAttribute` for that. 304 | 305 | Version 1.8.0 306 | - 307 | * Adding more precise functionality for xml namespaces, allowing typesafe building of elements and attributes with namespaces.\ 308 | Thanks to [@asm0dey](https://github.com/asm0dey) for requesting this! 309 | 310 | Version 1.7.4 311 | - 312 | * Adding ability to add a DTD.\ 313 | Thanks to [@anskotid](https://github.com/anskotid) for raising this! 314 | 315 | Version 1.7.3 316 | - 317 | * Making `private fun parse(Document)` public.\ 318 | Thanks to [@rkklai](https://github.com/rkklai) for requesting this! 319 | 320 | Version 1.7.2 321 | - 322 | * Fixing issue where a text element that contains an empty string doesn't respect `useSelfClosingTags`.\ 323 | Thanks to [@d-wojciechowski](https://github.com/d-wojciechowski) for finding this! 324 | 325 | Version 1.7.1 326 | - 327 | * Added new PrintOptions to control the indent character(s) used. `indent` default is `\t`.\ 328 | Thanks to [@vRallev](https://github.com/vRallev) for adding this! 329 | 330 | Version 1.7 331 | - 332 | **POTENTIAL BREAKING CHANGES** 333 | * All node types override `equals` and `hashCode`. This could change the behavior of putting nodes in a Set or Map.\ 334 | Thanks to [@cbartolome](https://github.com/cbartolome) for requesting this! 335 | 336 | Version 1.6.1 337 | - 338 | * Added new PrintOptions to use character references instead of HTML names.\ 339 | Thanks to [@senecal-jjs](https://github.com/senecal-jjs) and [@sleddog](https://github.com/sleddor) for 340 | adding this! 341 | 342 | Version 1.6.0 343 | - 344 | * Updated README 345 | 346 | **BREAKING CHANGES** 347 | * The `kotlin-reflect` dependency has been removed from the transitive dependnecies. 348 | This module is only used for controlling element order using `@XmlType`. 349 | If your project depends on that feature, you will need to have `kotlin-reflect` on the 350 | runtime classpath.\ 351 | Thanks to [@mvysny](https://github.com/mvysny) for requesting this! 352 | 353 | Version 1.5.4 354 | - 355 | * Adding new global processing instructions.\ 356 | Thanks to [@rjaros](https://github.com/rjaros) for requsting this! 357 | 358 | Version 1.5.3 359 | - 360 | * Fixing single line text element rendering for processing instructures and CData elements.\ 361 | Thanks to [@jonathan-yan](https://github.com/jonathan-yan) for fixing this! 362 | 363 | Version 1.5.2 364 | - 365 | * Added ability to specify the xml version. This affects both the prolog and text escaping.\ 366 | Thanks to [@ZR8C](https://github.com/ZR8C) for finding this! 367 | 368 | Version 1.5.1 369 | - 370 | * Added ability to add processing instructions. Use the new `processingInstruction` method to do this.\ 371 | Thanks to [@endofhome](https://github.com/endofhome) for adding this! 372 | * Added ability to add comments. Use the new `comment` method to do this.\ 373 | Thanks to [@ejektaflex](https://github.com/ejektaflex) for requesting this! 374 | 375 | Version 1.5.0 376 | - 377 | * Added more robust PrintOptions class to allow for more control over how xml is structured.\ 378 | Fixes issue #16 379 | * Attribute values are now fully escaped properly.\ 380 | Thanks to [@pkulak](https://github.com/pkulak) for finding and fixing this! 381 | 382 | **BREAKING CHANGES** 383 | * Changed Element.render method signature to use kotlin.text.Appenable instead of kotlin.text.StringBuilder. 384 | Any custom element types created will need to be updated 385 | 386 | Version 1.4.5 387 | - 388 | * Fixed incorrect handling of CDATA elements. `prettyFormat` should not alter the CDATA content. 389 | * Fixed nested CData elements\ 390 | Big thanks to [@TWiStErRob](https://github.com/TWiStErRob) for finding and fixing both of these! 391 | 392 | Version 1.4.4 393 | - 394 | * Switched to Kotlin's `Charsets` instead of using `java.nio.charset.StandardCharsets`. `StandardCharsets` is not available in some versions of Android.\ 395 | Thanks to [@little-fish](https://github.com/little-fish) for submitting and fixing this! 396 | 397 | Version 1.4.3 398 | - 399 | **BREAKING CHANGES** 400 | * Moved `prettyFormat` to a parameter of `toString()`. `prettyFormat` in the constructor or util method is no longer available. Fixes issue #7 401 | 402 | Version 1.4.2 403 | - 404 | * Fixes issue #6 405 | 406 | Version 1.4.1 407 | - 408 | * Upgrading Gradle to 4.9 409 | * Upgrading Kotlin to 1.2.60 410 | * Fix issue #4 411 | * Tweak generation code to be more concise 412 | * Add flag to code generation to allow for member functions instead of extension functions 413 | 414 | Version 1.4 415 | - 416 | **BREAKING CHANGES** 417 | * `org.redundent.kotlin.xml.Node.name` has been renamed to `org.redundent.kotlin.xml.Node.nodeName` to avoid clashes with attributes called `name`. 418 | --- 419 | * Adding DSL generator project to generate kotlin-xml-builder DSL from a schema file. 420 | See [kotlin-xml-dsl-generator](./kotlin-xml-dsl-generator) for details 421 | 422 | Version 1.3 423 | - 424 | * Added ability to parse an xml document into a builder object using the new `parse` method. 425 | * Upgrading Gradle and Kotlin versions. 426 | 427 | Version 1.2 428 | - 429 | * Added a `sitemap` method to allow for easy generation of sitemaps (which is what this project was created for in the first place). 430 | * Added `String.invoke` for elements allowing you to specify elements by just their name (see docs above) 431 | * Added some searching methods to search child nodes. `filter`, `first`, `firstOrNull`, and `exists`. 432 | * Added some mutation methods to allow you to add/remove/replace nodes in an element. 433 | 434 | Version 1.1 435 | - 436 | * Added convenience method for elements with just a name and value. `element("name", "value")` 437 | * Removed `ns` method. Please use `namespace(...)` instead. 438 | * Upgraded Gradle version 439 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/test/kotlin/org/redundent/kotlin/xml/XmlBuilderTest.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | import org.junit.Test 4 | import org.xml.sax.SAXException 5 | import java.io.ByteArrayInputStream 6 | import kotlin.test.assertEquals 7 | import kotlin.test.assertFailsWith 8 | import kotlin.test.assertFalse 9 | import kotlin.test.assertNotNull 10 | import kotlin.test.assertNull 11 | import kotlin.test.assertTrue 12 | 13 | class XmlBuilderTest : TestBase() { 14 | @Test 15 | fun basicTest() { 16 | val urlset = xml("urlset") { 17 | xmlns = "https://www.sitemaps.org/schemas/sitemap/0.9" 18 | 19 | for (i in 0..2) { 20 | element("url") { 21 | element("loc") { 22 | -"https://google.com/$i" 23 | } 24 | } 25 | } 26 | } 27 | 28 | validate(urlset) 29 | } 30 | 31 | @Test 32 | fun customNamespaces() { 33 | val root = xml("root") { 34 | xmlns = "https://someurl.org" 35 | namespace("t", "https://t.org") 36 | 37 | element("t:element") { 38 | -"Test" 39 | } 40 | 41 | element("p") { 42 | xmlns = "https://t.co" 43 | } 44 | 45 | element("d:p") { 46 | namespace("d", "https://b.co") 47 | } 48 | } 49 | 50 | validate(root) 51 | } 52 | 53 | @Test 54 | fun notPrettyFormatting() { 55 | val root = xml("root") { 56 | element("element") { 57 | -"Hello" 58 | } 59 | element("otherElement") { 60 | -"Test" 61 | } 62 | } 63 | 64 | validate(root, prettyFormat = false) 65 | } 66 | 67 | @Test 68 | fun zeroSpaceIndent() { 69 | val root = xml("root") { 70 | element("element") { 71 | -"Hello" 72 | } 73 | element("otherElement") { 74 | -"Test" 75 | } 76 | } 77 | 78 | validate(root, PrintOptions(indent = "")) 79 | } 80 | 81 | @Test 82 | fun zeroSpaceIndentNoPrettyFormatting() { 83 | val root = xml("root") { 84 | element("element") { 85 | -"Hello" 86 | } 87 | element("otherElement") { 88 | -"Test" 89 | } 90 | } 91 | 92 | validate(root, PrintOptions(pretty = false, indent = "")) 93 | } 94 | 95 | @Test 96 | fun singleLineTextElement() { 97 | val root = xml("root") { 98 | element("element") { 99 | -"Hello" 100 | } 101 | element("otherElement") { 102 | -"Test" 103 | } 104 | } 105 | 106 | validate(root, PrintOptions(pretty = true, singleLineTextElements = true)) 107 | } 108 | 109 | @Test 110 | fun singleLineCDATAElement() { 111 | val root = xml("root") { 112 | element("element") { 113 | cdata("Some & xml") 114 | } 115 | } 116 | 117 | validate(root, PrintOptions(pretty = true, singleLineTextElements = true)) 118 | } 119 | 120 | @Test 121 | fun singleLineProcessingInstructionElement() { 122 | val root = xml("root") { 123 | element("element") { 124 | processingInstruction("SomeProcessingInstruction") 125 | } 126 | } 127 | 128 | validate(root, PrintOptions(pretty = true, singleLineTextElements = true)) 129 | } 130 | 131 | @Test 132 | fun singleLineProcessingInstructionElementWithAttributes() { 133 | val root = xml("root") { 134 | element("element") { 135 | processingInstruction("SomeProcessingInstruction", "key" to "value") 136 | } 137 | } 138 | 139 | validate(root, PrintOptions(pretty = true, singleLineTextElements = true)) 140 | } 141 | 142 | @Test 143 | fun globalProcessingInstructionElement() { 144 | val root = xml("root") { 145 | globalProcessingInstruction( 146 | "xml-stylesheet", 147 | "key" to "value", 148 | "href" to "http://blah" 149 | ) 150 | 151 | element("element") { 152 | globalProcessingInstruction("test") 153 | } 154 | } 155 | 156 | validate(root, PrintOptions(pretty = true, singleLineTextElements = true)) 157 | } 158 | 159 | @Test 160 | fun comment() { 161 | val root = xml("root") { 162 | comment("my comment -->") 163 | element("someNode") { 164 | -"value" 165 | } 166 | } 167 | 168 | validate(root) 169 | } 170 | 171 | @Test 172 | fun noSelfClosingTag() { 173 | val root = xml("root") { 174 | element("element") 175 | } 176 | 177 | validate(root, PrintOptions(useSelfClosingTags = false)) 178 | } 179 | 180 | @Test 181 | fun multipleAttributes() { 182 | val root = xml("root") { 183 | element("test") { 184 | attribute("key", "value") 185 | attribute("otherAttr", "hello world") 186 | } 187 | element("attributes") { 188 | attributes( 189 | "test" to "value", 190 | "key" to "pair" 191 | ) 192 | } 193 | } 194 | 195 | validate(root) 196 | } 197 | 198 | @Test 199 | fun emptyRoot() { 200 | validate(xml("root")) 201 | } 202 | 203 | @Test 204 | fun emptyElement() { 205 | validate( 206 | xml("root") { 207 | element("test") 208 | } 209 | ) 210 | } 211 | 212 | @Test 213 | fun cdata() { 214 | val root = xml("root") { 215 | cdata("Some & xml") 216 | } 217 | 218 | validate(root) 219 | } 220 | 221 | @Test 222 | fun cdataNesting() { 223 | val root = xml("root") { 224 | cdata("") 225 | } 226 | 227 | validate(root) 228 | } 229 | 230 | @Test 231 | fun processingInstruction() { 232 | val root = xml("root") { 233 | processingInstruction("SomeProcessingInstruction") 234 | } 235 | 236 | validate(root) 237 | } 238 | 239 | @Test 240 | fun updateAttribute() { 241 | val root = xml("root") { 242 | attribute("key", "value") 243 | } 244 | 245 | root["key"] = "otherValue" 246 | 247 | validate(root) 248 | } 249 | 250 | @Test 251 | fun xmlEncode() { 252 | val root = xml("root") { 253 | -"&<>" 254 | } 255 | 256 | validate(root) 257 | } 258 | 259 | @Test 260 | fun elementValue() { 261 | val root = xml("root") { 262 | element("name", "value") 263 | } 264 | 265 | validate(root) 266 | } 267 | 268 | @Test 269 | fun elementAsString() { 270 | val root = xml("root") { 271 | "name"("value") 272 | } 273 | 274 | validate(root) 275 | } 276 | 277 | @Test 278 | fun elementAsStringWithAttributes() { 279 | validate( 280 | xml("root") { 281 | "name"("attr" to "value", "attr2" to "other") 282 | } 283 | ) 284 | } 285 | 286 | @Test 287 | fun elementAsStringWithAttributesAndContent() { 288 | validate( 289 | xml("root") { 290 | "name"("attr" to "value") { 291 | -"Content" 292 | } 293 | } 294 | ) 295 | } 296 | 297 | @Test 298 | fun attributes() { 299 | val xmlns = "testing" 300 | val value = "value" 301 | 302 | val xml = xml("root") { 303 | this.xmlns = xmlns 304 | attribute("attr", value) 305 | } 306 | 307 | assertEquals(xmlns, xml.xmlns, "xmlns is correct") 308 | assertNotNull(xml["attr"], "attr is not null") 309 | assertEquals(value, xml["attr"]!!, "attr getting is correct") 310 | 311 | // Update the attr value 312 | xml["attr"] = "something else" 313 | assertEquals("something else", xml["attr"]!!, "attr value is updated") 314 | 315 | // Remove the 316 | xml.xmlns = null 317 | assertNull(xml.xmlns, "xmlns is removed") 318 | 319 | xml["attr"] = null 320 | assertFalse(xml.attributes.containsKey("attr")) 321 | assertNull(xml["attr"], "attr value is null") 322 | } 323 | 324 | @Test 325 | fun quoteInAttribute() { 326 | val root = xml("root") { 327 | attribute("attr", "My \" Attribute value '") 328 | } 329 | 330 | validate(root) 331 | } 332 | 333 | @Test 334 | fun specialCharInAttribute() { 335 | val root = xml("root") { 336 | attribute("attr", "& < > \" '") 337 | } 338 | 339 | validate(root) 340 | } 341 | 342 | @Test(expected = SAXException::class) 343 | fun invalidElementName() { 344 | val root = xml("invalid root") 345 | 346 | validateXml(root.toString()) 347 | } 348 | 349 | @Test(expected = SAXException::class) 350 | fun invalidAttributeName() { 351 | val root = xml("root") { 352 | attribute("invalid name", "") 353 | } 354 | 355 | validateXml(root.toString()) 356 | } 357 | 358 | @Test 359 | fun filterFunctions() { 360 | val xml = xml("root") { 361 | "child1" { 362 | "other"() 363 | } 364 | "child2"() 365 | "multiple"() 366 | "multiple"() 367 | } 368 | 369 | val child1 = xml.filter("child1") 370 | assertEquals(1, child1.size, "filter returned one element") 371 | 372 | val hasChild = xml.filter { it.nodeName == "child1" && it.exists("other") } 373 | assertEquals(1, hasChild.size, "filter with exists returned one element") 374 | 375 | val multiple = xml.filter("multiple") 376 | assertEquals(2, multiple.size, "filter with multiple returned two element") 377 | 378 | assertNull(xml.firstOrNull("junk"), "firstOrNull returned null") 379 | assertNotNull(xml.firstOrNull("child1"), "firstOrNull returned element") 380 | 381 | assertFailsWith(NoSuchElementException::class) { 382 | xml.first("junk") 383 | } 384 | 385 | assertTrue("element exists") { xml.exists("child1") } 386 | assertFalse("element doesn't exists") { xml.exists("junk") } 387 | } 388 | 389 | @Test 390 | fun addElement() { 391 | val root = xml("root") { 392 | "a"() 393 | } 394 | 395 | root.addElement(node("b")) 396 | 397 | validate(root) 398 | } 399 | 400 | @Test 401 | fun removeElement() { 402 | val root = xml("root") { 403 | "a"() 404 | "b"() 405 | } 406 | 407 | root.removeElement(root.first("b")) 408 | 409 | validate(root) 410 | } 411 | 412 | @Test 413 | fun addElementAfter() { 414 | val root = xml("root") { 415 | "a"() 416 | "b"() 417 | } 418 | 419 | root.addElementAfter(node("c"), root.first("a")) 420 | 421 | validate(root) 422 | } 423 | 424 | @Test 425 | fun addElementAfterLastChild() { 426 | val root = xml("root") { 427 | "a"() 428 | "b"() 429 | } 430 | 431 | root.addElementAfter(node("c"), root.first("b")) 432 | 433 | validate(root) 434 | } 435 | 436 | @Test(expected = IllegalArgumentException::class) 437 | fun addElementAfterNonExistent() { 438 | val root = xml("root") { 439 | "a"() 440 | "b"() 441 | } 442 | 443 | root.addElementAfter(node("c"), node("d")) 444 | } 445 | 446 | @Test 447 | fun addElementBefore() { 448 | val root = xml("root") { 449 | "a"() 450 | "b"() 451 | } 452 | 453 | root.addElementBefore(node("c"), root.first("b")) 454 | 455 | validate(root) 456 | } 457 | 458 | @Test(expected = IllegalArgumentException::class) 459 | fun addElementBeforeNonExistent() { 460 | val root = xml("root") { 461 | "a"() 462 | "b"() 463 | } 464 | 465 | root.addElementBefore(node("c"), node("d")) 466 | } 467 | 468 | @Test 469 | fun replaceElement() { 470 | val root = xml("root") { 471 | "a"() 472 | "b"() 473 | } 474 | 475 | root.replaceElement(root.first("b"), node("c")) 476 | 477 | validate(root) 478 | } 479 | 480 | @Test 481 | fun parseAndVerify() { 482 | val xmlns = "https://blog.redundent.org" 483 | val value = "value" 484 | val input = ByteArrayInputStream("$value".toByteArray()) 485 | 486 | val root = parse(input) 487 | 488 | assertEquals("root", root.nodeName, "root element nodeName is correct") 489 | assertEquals(xmlns, root.xmlns, "root xmlns is correct") 490 | 491 | val children = root.children 492 | assertEquals(1, children.size, "root has 1 child") 493 | assertTrue(children[0] is Node, "child is a node") 494 | 495 | val child = children.first() as Node 496 | assertTrue(child.children[0] is TextElement, "element is text") 497 | assertEquals(value, (child.children[0] as TextElement).text) 498 | } 499 | 500 | @Test 501 | fun parseCData() = parseTest() 502 | 503 | @Test 504 | fun parseCDataWhitespace() = parseTest() 505 | 506 | @Test 507 | fun parseCustomNamespaces() = parseTest() 508 | 509 | @Test 510 | fun parseMultipleAttributes() = parseTest() 511 | 512 | @Test 513 | fun parseBasicTest() = parseTest() 514 | 515 | @Test 516 | fun parseXmlEncode() = parseTest() 517 | 518 | private fun parseTest() { 519 | val input = getInputStream() 520 | val xml = parse(input) 521 | 522 | validateTest(xml) 523 | } 524 | 525 | @Test 526 | fun checkIncludeXmlPrologFlag() { 527 | val node = xml("test") 528 | assertFalse(node.includeXmlProlog, "prolog is false") 529 | 530 | node.encoding = "UTF-8" 531 | assertTrue(node.includeXmlProlog, "prolog is included") 532 | } 533 | 534 | @Test 535 | fun encoding() { 536 | val xml = xml("test", encoding = "UTF-16").toString(prettyFormat = false) 537 | 538 | assertEquals("", xml) 539 | } 540 | 541 | @Test 542 | fun xmlVersion() { 543 | for (version in XmlVersion.values()) { 544 | val xml = xml("test", version = version).toString(prettyFormat = false) 545 | assertEquals("", xml) 546 | } 547 | } 548 | 549 | @Test 550 | fun characterReference() { 551 | val root = xml("root") { 552 | element("element") { 553 | -"Hello & Goodbye" 554 | } 555 | element("otherElement") { 556 | -"Test" 557 | } 558 | } 559 | 560 | validate(root, PrintOptions(pretty = true, singleLineTextElements = true, useCharacterReference = true)) 561 | } 562 | 563 | @Test 564 | fun selfClosingTag() { 565 | for (text in arrayOf("", null)) { 566 | val root = xml("root") { 567 | "element" { 568 | if (text != null) { 569 | -text 570 | } 571 | } 572 | } 573 | 574 | validate(root, PrintOptions(pretty = true, useSelfClosingTags = true)) 575 | } 576 | } 577 | 578 | @Test 579 | fun doctypeSimple() { 580 | val root = xml("root") { 581 | doctype() 582 | } 583 | 584 | validate(root) 585 | } 586 | 587 | @Test 588 | fun doctypeSystem() { 589 | val root = xml("root") { 590 | doctype(systemId = "test.dtd") 591 | } 592 | 593 | validate(root) 594 | } 595 | 596 | @Test 597 | fun doctypePublic() { 598 | val root = xml("root") { 599 | doctype(publicId = "-//redundent//PUBLIC DOCTYPE//EN", systemId = "test.dtd") 600 | } 601 | 602 | validate(root) 603 | } 604 | 605 | @Test 606 | fun advancedNamespaces() { 607 | val ns1 = Namespace("a", "https://ns1.org") 608 | val ns2 = Namespace("https://ns2.org") 609 | val ns3 = Namespace("b", "https://ns3.org") 610 | val ns4 = Namespace("c", "https://ns4.org") 611 | val ns5 = Namespace("d", "https://ns5.org") 612 | val ns6 = Namespace("e", "https://ns6.org") 613 | 614 | val root = xml("root", namespace = ns1) { 615 | namespace(ns2) 616 | "node"(ns3) { 617 | attribute("attr1", "value") 618 | attribute("attr2", "value", ns4) 619 | } 620 | 621 | "child" { 622 | attributes( 623 | ns5, 624 | "key1" to "value1", 625 | "key2" to "value2" 626 | ) 627 | attributes( 628 | Attribute("key3", "value3", ns6), 629 | Attribute("key4", "value4", ns1) 630 | ) 631 | 632 | "sub"(ns5, Attribute("key5", "value5", ns6)) 633 | } 634 | } 635 | 636 | validate(root) 637 | } 638 | 639 | @Test 640 | fun unsafeAttributeValue() { 641 | val root = xml("root") { 642 | unsafeText("{") 643 | attribute("test", unsafe("Lj")) 644 | } 645 | 646 | validate(root) 647 | } 648 | 649 | @Test 650 | fun emptyString() { 651 | val root = xml("root") { 652 | "a"() 653 | -" " 654 | "b"() 655 | } 656 | 657 | validate(root, prettyFormat = false) 658 | } 659 | 660 | @Test 661 | fun whitespace() { 662 | val root = xml("root") { 663 | "a"(" ") 664 | "b"("\n") 665 | } 666 | 667 | validate(root, prettyFormat = false) 668 | } 669 | } 670 | -------------------------------------------------------------------------------- /kotlin-xml-builder/src/main/kotlin/org/redundent/kotlin/xml/Node.kt: -------------------------------------------------------------------------------- 1 | package org.redundent.kotlin.xml 2 | 3 | import org.apache.commons.lang3.builder.EqualsBuilder 4 | import org.apache.commons.lang3.builder.HashCodeBuilder 5 | import java.util.* 6 | import kotlin.collections.LinkedHashMap 7 | import kotlin.collections.LinkedHashSet 8 | 9 | /** 10 | * Base type for all elements. This is what handles pretty much all the rendering and building. 11 | */ 12 | open class Node(val nodeName: String) : Element { 13 | private companion object { 14 | private val isReflectionAvailable: Boolean by lazy { 15 | Node::class.java.classLoader.getResource("kotlin/reflect/full") != null 16 | } 17 | } 18 | 19 | private var parent: Node? = null 20 | private val _globalLevelProcessingInstructions = ArrayList() 21 | private var doctype: Doctype? = null 22 | private val _namespaces: MutableSet = LinkedHashSet() 23 | private val _attributes: LinkedHashMap = LinkedHashMap() 24 | private val _children = ArrayList() 25 | private val childOrderMap: Map? by lazy { 26 | if (!isReflectionAvailable) { 27 | return@lazy null 28 | } 29 | 30 | val xmlTypeAnnotation = this::class.annotations.firstOrNull { it is XmlType } as? XmlType ?: return@lazy null 31 | 32 | val childOrder = xmlTypeAnnotation.childOrder 33 | 34 | childOrder.indices.associateBy { childOrder[it] } 35 | } 36 | 37 | val namespaces: Collection 38 | get() = LinkedHashSet(_namespaces) 39 | 40 | /** 41 | * The default `xmlns` for the document. To add other namespaces, use the [namespace] method 42 | */ 43 | var xmlns: String? 44 | get() = namespaces.firstOrNull(Namespace::isDefault)?.value 45 | set(value) { 46 | if (value != null) { 47 | addNamespace(Namespace(value)) 48 | } else { 49 | _namespaces.removeIf(Namespace::isDefault) 50 | } 51 | } 52 | 53 | /** 54 | * Whether to include the xml prolog, i.e. ``. 55 | * 56 | * NOTE: this only applies to the root element. It is ignored an all children. 57 | */ 58 | var includeXmlProlog = false 59 | 60 | /** 61 | * Sets the encoding on the document. Setting this value will set [includeXmlProlog] to `true`. 62 | */ 63 | var encoding: String = Charsets.UTF_8.name() 64 | set(value) { 65 | includeXmlProlog = true 66 | field = value 67 | } 68 | 69 | var version: XmlVersion = XmlVersion.V10 70 | set(value) { 71 | includeXmlProlog = true 72 | field = value 73 | } 74 | 75 | var standalone: Boolean? = null 76 | set(value) { 77 | includeXmlProlog = true 78 | field = value 79 | } 80 | 81 | /** 82 | * Any attributes that belong to this element. This will always return a copy of the attribute map. 83 | * @sample [set] 84 | */ 85 | val attributes: Map 86 | get() = _attributes.mapValues { 87 | val value = it.value 88 | if (value is Unsafe) value.value else it.value 89 | } 90 | 91 | val children: List 92 | get() = _children 93 | 94 | private fun getParentNamespaces(): Set { 95 | return generateSequence(parent, Node::parent) 96 | .flatMap { it.namespaces.asSequence() } 97 | .toSet() 98 | } 99 | 100 | private fun initTag(tag: T, init: (T.() -> Unit)?): T { 101 | if (init != null) { 102 | tag.init() 103 | } 104 | _children.add(tag) 105 | 106 | setParentIfNode(tag, this) 107 | 108 | return tag 109 | } 110 | 111 | /** 112 | * Allows for easy access of this node's attributes 113 | * 114 | * ``` 115 | * val attr = element["key"] 116 | * ``` 117 | */ 118 | operator fun get(attributeName: String): T? { 119 | val value = _attributes[attributeName] 120 | @Suppress("UNCHECKED_CAST") 121 | return (if (value is Unsafe) value.value else value) as T? 122 | } 123 | 124 | /** 125 | * Allows for easy access of adding/updating this node's attributes. 126 | * Setting the value of an attribute to `null` will remove the attribute. 127 | * 128 | * ``` 129 | * element["key"] = "value" 130 | * ``` 131 | */ 132 | operator fun set(attributeName: String, value: Any?) { 133 | if (value == null) { 134 | removeAttribute(attributeName) 135 | } else { 136 | _attributes[attributeName] = value 137 | } 138 | } 139 | 140 | fun hasAttribute(attributeName: String): Boolean = _attributes.containsKey(attributeName) 141 | 142 | /** 143 | * Removes the specified attribute from the attributes map. 144 | */ 145 | fun removeAttribute(attributeName: String) { 146 | _attributes.remove(attributeName) 147 | } 148 | 149 | override fun render(builder: Appendable, indent: String, printOptions: PrintOptions) { 150 | val lineEnding = getLineEnding(printOptions) 151 | builder.append("$indent<$nodeName${renderNamespaces()}${renderAttributes(printOptions)}") 152 | 153 | if (!isEmptyOrSingleEmptyTextElement()) { 154 | if (printOptions.pretty && printOptions.singleLineTextElements && 155 | _children.size == 1 && _children[0] is TextElement 156 | ) { 157 | builder.append(">") 158 | (_children[0] as TextElement).renderSingleLine(builder, printOptions) 159 | builder.append("$lineEnding") 160 | } else { 161 | builder.append(">$lineEnding") 162 | for (c in sortedChildren()) { 163 | c.render(builder, getIndent(printOptions, indent), printOptions) 164 | } 165 | 166 | builder.append("$indent$lineEnding") 167 | } 168 | } else { 169 | builder.append("${getEmptyTagClosing(printOptions)}$lineEnding") 170 | } 171 | } 172 | 173 | private fun isEmptyOrSingleEmptyTextElement(): Boolean { 174 | if (_children.isEmpty()) { 175 | return true 176 | } 177 | 178 | if (_children.size == 1 && _children[0] is TextElement) { 179 | return (_children[0] as TextElement).text.isEmpty() 180 | } 181 | 182 | return false 183 | } 184 | 185 | private fun getEmptyTagClosing(printOptions: PrintOptions): String = if (printOptions.useSelfClosingTags) { 186 | "/>" 187 | } else { 188 | ">" 189 | } 190 | 191 | private fun sortedChildren(): List { 192 | return if (childOrderMap == null) { 193 | _children 194 | } else { 195 | _children.sortedWith { a, b -> 196 | val indexA = if (a is Node) childOrderMap!![a.nodeName] else 0 197 | val indexB = if (b is Node) childOrderMap!![b.nodeName] else 0 198 | 199 | compareValues(indexA, indexB) 200 | } 201 | } 202 | } 203 | 204 | private fun renderNamespaces(): String { 205 | if (namespaces.isEmpty()) { 206 | return "" 207 | } 208 | 209 | val parentNamespaces = getParentNamespaces() 210 | val namespacesNeeded = namespaces 211 | .filterNot { parentNamespaces.contains(it) } 212 | 213 | if (namespacesNeeded.isEmpty()) { 214 | return "" 215 | } 216 | 217 | return namespacesNeeded.joinToString(" ", prefix = " ") 218 | } 219 | 220 | private fun renderAttributes(printOptions: PrintOptions): String { 221 | if (_attributes.isEmpty()) { 222 | return "" 223 | } 224 | 225 | return _attributes.entries.joinToString(" ", prefix = " ") { 226 | val value = it.value 227 | val text = if (value is Unsafe) { 228 | value.value?.toString() 229 | } else { 230 | escapeValue( 231 | it.value, 232 | printOptions.xmlVersion, 233 | printOptions.useCharacterReference 234 | ) 235 | } 236 | 237 | "${it.key}=\"$text\"" 238 | } 239 | } 240 | 241 | private fun getIndent(printOptions: PrintOptions, indent: String): String = 242 | if (!printOptions.pretty) { "" } else { "$indent${printOptions.indent}" } 243 | 244 | /** 245 | * Get the XML representation of this object with `prettyFormat = true`. 246 | */ 247 | override fun toString() = toString(prettyFormat = true) 248 | 249 | /** 250 | * Get the XML representation of this object. 251 | * 252 | * @param [prettyFormat] true to format the XML with newlines and tabs; otherwise no formatting 253 | */ 254 | fun toString(prettyFormat: Boolean): String = toString(PrintOptions(pretty = prettyFormat)) 255 | 256 | fun toString(printOptions: PrintOptions): String = 257 | StringBuilder().also { writeTo(it, printOptions) }.toString().trim() 258 | 259 | fun writeTo(appendable: Appendable, printOptions: PrintOptions = PrintOptions()) { 260 | val lineEnding = getLineEnding(printOptions) 261 | 262 | printOptions.xmlVersion = version 263 | 264 | if (includeXmlProlog) { 265 | appendable.append("$lineEnding") 272 | } 273 | 274 | doctype?.apply { 275 | render(appendable, "", printOptions) 276 | } 277 | 278 | if (_globalLevelProcessingInstructions.isNotEmpty()) { 279 | _globalLevelProcessingInstructions.forEach { it.render(appendable, "", printOptions) } 280 | } 281 | 282 | render(appendable, "", printOptions) 283 | } 284 | 285 | operator fun String.unaryMinus() = text(this) 286 | 287 | fun unsafeText(text: String) { 288 | _children.add(TextElement(text, unsafe = true)) 289 | } 290 | 291 | fun text(text: String) { 292 | _children.add(TextElement(text)) 293 | } 294 | 295 | /** 296 | * Adds an XML comment to the document. 297 | * ``` 298 | * comment("my comment") 299 | * ``` 300 | * 301 | * @param text The text of the comment. This text will be rendered unescaped except for replace `--` with `--` 302 | */ 303 | fun comment(text: String) { 304 | _children.add(Comment(text)) 305 | } 306 | 307 | /** 308 | * Adds a basic element with the specific name to the parent. 309 | * ``` 310 | * element("url") { 311 | * // ... 312 | * } 313 | * ``` 314 | * 315 | * @param name The name of the element. 316 | * @param namespace Optional namespace object to use to build the name of the attribute. 317 | * @param init The block that defines the content of the element. 318 | */ 319 | fun element(name: String, namespace: Namespace? = null, init: (Node.() -> Unit)? = null): Node { 320 | val node = Node(buildName(name, namespace)) 321 | if (namespace != null) { 322 | node.addNamespace(namespace) 323 | } 324 | initTag(node, init) 325 | return node 326 | } 327 | 328 | /** 329 | * Adds a basic element with the specific name and value to the parent. This cannot be used for complex elements. 330 | * ``` 331 | * element("url", "https://google.com") 332 | * ``` 333 | * 334 | * @param name The name of the element. 335 | * @param value The inner text of the element 336 | * @param namespace Optional namespace object to use to build the name of the attribute. 337 | */ 338 | fun element(name: String, value: String, namespace: Namespace? = null): Node { 339 | val node = Node(buildName(name, namespace)) 340 | if (namespace != null) { 341 | node.addNamespace(namespace) 342 | } 343 | 344 | initTag(node) { 345 | -value 346 | } 347 | return node 348 | } 349 | 350 | /** 351 | * Adds a basic element with the specific name and value to the parent. This cannot be used for complex elements. 352 | * ``` 353 | * "url"("https://google.com") 354 | * ``` 355 | * 356 | * @receiver The name of the element. 357 | * @param value The inner text of the element 358 | * @param namespace Optional namespace object to use to build the name of the attribute. 359 | */ 360 | operator fun String.invoke(value: String, namespace: Namespace? = null): Node = element(this, value, namespace) 361 | 362 | /** 363 | * Adds a basic element with the specific name to the parent. This method 364 | * allows you to specify optional attributes and content 365 | * ``` 366 | * "url"("key" to "value") { 367 | * // ... 368 | * } 369 | * ``` 370 | * 371 | * @receiver The name of the element. 372 | * @param attributes Any attributes to add to this element. Can be omitted. 373 | * @param init The block that defines the content of the element. 374 | */ 375 | operator fun String.invoke(vararg attributes: Pair, init: (Node.() -> Unit)? = null): Node { 376 | return addElement(this, attributes.map { Attribute(it.first, it.second) }.toTypedArray(), null, init) 377 | } 378 | 379 | /** 380 | * Adds a basic element with the specific name to the parent. This method 381 | * allows you to specify the namespace of the element as well as optional attributes and content 382 | * ``` 383 | * "url"(ns, "key" to "value") { 384 | * // ... 385 | * } 386 | * ``` 387 | * 388 | * @receiver The name of the element. 389 | * @param namespace Namespace object to use to build the name of the attribute. 390 | * @param attributes Any attributes to add to this element. Can be omitted. 391 | * @param init The block that defines the content of the element. 392 | */ 393 | operator fun String.invoke( 394 | namespace: Namespace, 395 | vararg attributes: Pair, 396 | init: (Node.() -> Unit)? = null 397 | ): Node { 398 | return addElement(this, attributes.map { Attribute(it.first, it.second) }.toTypedArray(), namespace, init) 399 | } 400 | 401 | /** 402 | * Adds a basic element with the specific name to the parent. This method 403 | * allows you to specify the namespace of the element 404 | * ``` 405 | * "url"(ns) { 406 | * ... 407 | * } 408 | * ``` 409 | * 410 | * @receiver The name of the element. 411 | * @param namespace Namespace object to use to build the name of the attribute. 412 | * @param init The block that defines the content of the element. 413 | */ 414 | operator fun String.invoke(namespace: Namespace, init: (Node.() -> Unit)? = null): Node { 415 | return addElement(this, emptyArray(), namespace, init) 416 | } 417 | 418 | /** 419 | * Adds a basic element with the specific name to the parent. This method 420 | * allows you to specify namespace of the element as well as optional attributes (with namespaces) and content 421 | * ``` 422 | * "url"(ns, Attribute("key", "value", otherNs)) { 423 | * ... 424 | * } 425 | * ``` 426 | * 427 | * @receiver The name of the element. 428 | * @param namespace Namespace object to use to build the name of the attribute. 429 | * @param attributes Any attributes to add to this element. Can be omitted. 430 | * @param init The block that defines the content of the element. 431 | */ 432 | operator fun String.invoke( 433 | namespace: Namespace, 434 | vararg attributes: Attribute, 435 | init: (Node.() -> Unit)? = null 436 | ): Node { 437 | return addElement(this, attributes, namespace, init) 438 | } 439 | 440 | private fun addElement( 441 | name: String, 442 | attributes: Array, 443 | namespace: Namespace?, 444 | init: (Node.() -> Unit)? 445 | ): Node { 446 | val e = element(name, namespace) { 447 | attributes(*attributes) 448 | } 449 | 450 | if (init != null) { 451 | e.apply(init) 452 | } 453 | 454 | return e 455 | } 456 | 457 | private fun addNamespace(namespace: Namespace) { 458 | if (namespace.isDefault) { 459 | _namespaces.removeIf(Namespace::isDefault) 460 | } 461 | 462 | _namespaces.add(namespace) 463 | } 464 | 465 | /** 466 | * Adds an attribute to the current element 467 | * ``` 468 | * "url" { 469 | * attribute("key", "value") 470 | * } 471 | * ``` 472 | * 473 | * @param name The name of the attribute. This is currently no validation against the name. 474 | * @param value The attribute value. 475 | * @param namespace Optional namespace object to use to build the name of the attribute. Note this does NOT declare 476 | * the namespace. It simply uses it to build the attribute name. 477 | */ 478 | fun attribute(name: String, value: Any, namespace: Namespace? = null) { 479 | if (namespace != null) { 480 | addNamespace(namespace) 481 | } 482 | _attributes[buildName(name, namespace)] = value 483 | } 484 | 485 | /** 486 | * Adds a set of attributes to the current element. 487 | * ``` 488 | * "url" { 489 | * attributes( 490 | * "key" to "value", 491 | * "id" to "1" 492 | * ) 493 | * } 494 | * ``` 495 | * @param attrs Collection of the attributes to apply to this element. 496 | * @see attribute 497 | */ 498 | fun attributes(vararg attrs: Pair) { 499 | attrs.forEach { attribute(it.first, it.second) } 500 | } 501 | 502 | /** 503 | * Adds a set of attributes to the current element. 504 | * ``` 505 | * "url" { 506 | * attributes( 507 | * Attribute("key", "value", namespace), 508 | * Attribute("id", "1", namespace) 509 | * ) 510 | * } 511 | * ``` 512 | * 513 | * @param attrs Collection of the attributes to apply to this element. 514 | * @see attribute 515 | */ 516 | fun attributes(vararg attrs: Attribute) { 517 | for (attr in attrs) { 518 | if (attr.namespace != null) { 519 | addNamespace(attr.namespace) 520 | } 521 | attribute(buildName(attr.name, attr.namespace), attr.value) 522 | } 523 | } 524 | 525 | /** 526 | * Adds a set of attributes to the current element. 527 | * 528 | * ``` 529 | * "url" { 530 | * attributes( 531 | * "key" to "value", 532 | * "id" to "1" 533 | * ) 534 | * } 535 | * ``` 536 | * 537 | * @param namespace Optional namespace object to use to build the name of the attribute. Note this does NOT declare 538 | * the namespace. It simply uses it to build the attribute name(s). 539 | * @param attrs Collection of the attributes to apply to this element. 540 | * @see attribute 541 | */ 542 | fun attributes(namespace: Namespace, vararg attrs: Pair) { 543 | attrs.forEach { attribute(it.first, it.second, namespace) } 544 | } 545 | 546 | /** 547 | * Adds the supplied text as a CDATA element 548 | * 549 | * @param text The inner text of the CDATA element. 550 | */ 551 | fun cdata(text: String) { 552 | _children.add(CDATAElement(text)) 553 | } 554 | 555 | /** 556 | * Adds the supplied text as a processing instruction element 557 | * 558 | * @param text The inner text of the processing instruction element. 559 | * @param attributes Optional set of attributes to apply to this processing instruction. 560 | */ 561 | fun processingInstruction(text: String, vararg attributes: Pair) { 562 | _children.add(ProcessingInstructionElement(text, linkedMapOf(*attributes))) 563 | } 564 | 565 | /** 566 | * Adds the supplied text as a processing instruction element to the root of the document. 567 | * 568 | * @param text The inner text of the processing instruction element. 569 | * @param attributes Optional set of attributes to apply to this processing instruction. 570 | */ 571 | fun globalProcessingInstruction(text: String, vararg attributes: Pair) { 572 | _globalLevelProcessingInstructions.add(ProcessingInstructionElement(text, linkedMapOf(*attributes))) 573 | } 574 | 575 | /** 576 | * Add a DTD to the document. 577 | * 578 | * @param name The name of the DTD element. Not supplying this or passing null will default to [nodeName]. 579 | * @param publicId The public declaration of the DTD. 580 | * @param systemId The system declaration of the DTD. 581 | */ 582 | fun doctype(name: String? = null, publicId: String? = null, systemId: String? = null) { 583 | if (publicId != null && systemId == null) { 584 | throw IllegalStateException("systemId must be provided if publicId is provided") 585 | } 586 | 587 | doctype = Doctype(name ?: nodeName, publicId = publicId, systemId = systemId) 588 | } 589 | 590 | /** 591 | * Adds the specified namespace to the element. 592 | * ``` 593 | * "url" { 594 | * namespace("t", "http://someurl.org") 595 | * } 596 | * ``` 597 | * 598 | * @param name The name of the namespace. 599 | * @param value The url or descriptor of the namespace. 600 | */ 601 | fun namespace(name: String, value: String): Namespace { 602 | val ns = Namespace(name, value) 603 | namespace(ns) 604 | return ns 605 | } 606 | 607 | /** 608 | * Adds the specified namespace to the element. 609 | * ``` 610 | * val ns = Namespace("t", "http://someurl.org") 611 | * "url" { 612 | * namespace(ns) 613 | * } 614 | * ``` 615 | * 616 | * @param namespace The namespace object to use for the element's namespace declaration. 617 | */ 618 | fun namespace(namespace: Namespace) { 619 | addNamespace(namespace) 620 | } 621 | 622 | /** 623 | * Adds a node to the node. 624 | * @param node The node to append. 625 | */ 626 | @Deprecated( 627 | message = "Use addElement instead", 628 | replaceWith = ReplaceWith("addElement(node)") 629 | ) 630 | fun addNode(node: Node) = addElement(node) 631 | 632 | /** 633 | * Adds a element to the node. 634 | * @param element The element to append. 635 | */ 636 | fun addElement(element: Element) { 637 | setParentIfNode(element, this) 638 | _children.add(element) 639 | } 640 | 641 | /** 642 | * Adds the provided elements to the node. 643 | * @param elements The elements to append. 644 | */ 645 | fun addElements(vararg elements: Element) = elements.forEach { addElement(it) } 646 | 647 | /** 648 | * Adds the provided elements to the node. 649 | * @param elements The elements to append. 650 | */ 651 | fun addElements(elements: Iterable) = elements.forEach { addElement(it) } 652 | 653 | /** 654 | * Adds a node to the node after the specific node. 655 | * @param node The node to add 656 | * @param after The node to add [node] after 657 | * 658 | * @throws IllegalArgumentException If [after] can't be found 659 | */ 660 | @Deprecated( 661 | message = "Use addElementAfter instead", 662 | replaceWith = ReplaceWith("addElementAfter(node, after)") 663 | ) 664 | fun addNodeAfter(node: Node, after: Node) = addElementAfter(node, after) 665 | 666 | /** 667 | * Adds an element to the node after the specific element. 668 | * @param element The element to add 669 | * @param after The element to add [element] after 670 | * 671 | * @throws IllegalArgumentException If [after] can't be found 672 | */ 673 | fun addElementAfter(element: Element, after: Element) { 674 | val index = findIndex(after) 675 | 676 | setParentIfNode(element, this) 677 | 678 | if (index + 1 == _children.size) { 679 | _children.add(element) 680 | } else { 681 | _children.add(index + 1, element) 682 | } 683 | } 684 | 685 | /** 686 | * Adds elements to the node after the specific element. 687 | * @param elements The elements to add 688 | * @param after The element to add [elements] after 689 | * 690 | * @throws IllegalArgumentException If [after] can't be found 691 | */ 692 | fun addElementsAfter(elements: Iterable, after: Element) { 693 | val index = findIndex(after) + 1 694 | 695 | if (index == _children.size) { 696 | addElements(elements) 697 | } else { 698 | val firstPart = _children.take(index) 699 | val lastPart = _children.drop(index) 700 | _children.clear() 701 | _children.addAll(firstPart) 702 | addElements(elements) 703 | _children.addAll(lastPart) 704 | } 705 | } 706 | 707 | /** 708 | * Adds elements to the node after the specific element. 709 | * @param after The element to add [elements] after 710 | * @param elements The elements to add 711 | * 712 | * @throws IllegalArgumentException If [after] can't be found 713 | */ 714 | fun addElementsAfter(after: Element, vararg elements: Element) = 715 | addElementsAfter(listOf(*elements), after) 716 | 717 | /** 718 | * Adds a node to the node before the specific node. 719 | * @param node The node to add 720 | * @param before The node to add [node] before 721 | * 722 | * @throws IllegalArgumentException If [before] can't be found 723 | */ 724 | @Deprecated( 725 | message = "Use addElementBefore instead", 726 | replaceWith = ReplaceWith("addElementBefore(node, before)") 727 | ) 728 | fun addNodeBefore(node: Node, before: Node) = addElementBefore(node, before) 729 | 730 | /** 731 | * Adds an element to the node before the specific element. 732 | * @param element The element to add 733 | * @param before The element to add [element] before 734 | * 735 | * @throws IllegalArgumentException If [before] can't be found 736 | */ 737 | fun addElementBefore(element: Element, before: Element) { 738 | val index = findIndex(before) 739 | setParentIfNode(element, this) 740 | _children.add(index, element) 741 | } 742 | 743 | /** 744 | * Adds elements to the node before the specific element. 745 | * @param elements The elements to add 746 | * @param before The element to add [elements] before 747 | * 748 | * @throws IllegalArgumentException If [before] can't be found 749 | */ 750 | fun addElementsBefore(elements: Iterable, before: Element) { 751 | val index = findIndex(before) 752 | val firstPart = _children.take(index) 753 | val lastPart = _children.drop(index) 754 | _children.clear() 755 | _children.addAll(firstPart) 756 | addElements(elements) 757 | _children.addAll(lastPart) 758 | } 759 | 760 | /** 761 | * Adds elements to the node before the specific element. 762 | * @param before The element to add [elements] before 763 | * @param elements The elements to add 764 | * 765 | * @throws IllegalArgumentException If [before] can't be found 766 | */ 767 | fun addElementsBefore(before: Element, vararg elements: Element) = 768 | addElementsBefore(listOf(*elements), before) 769 | 770 | /** 771 | * Removes a node from the node. 772 | * @param node The node to remove 773 | * 774 | * @throws IllegalArgumentException If [node] can't be found 775 | */ 776 | @Deprecated( 777 | message = "Use removeElement instead", 778 | replaceWith = ReplaceWith("removeElement(node)") 779 | ) 780 | fun removeNode(node: Node) = removeElement(node) 781 | 782 | /** 783 | * Removes an element from the node. 784 | * @param element The element to remove 785 | * 786 | * @throws IllegalArgumentException If [element] can't be found 787 | */ 788 | fun removeElement(element: Element) { 789 | val index = findIndex(element) 790 | removeChildAt(index) 791 | } 792 | 793 | /** 794 | * Removes the elements from the node. 795 | * @param elements The elements to remove 796 | * 797 | * @throws IllegalArgumentException If any [elements] can't be found 798 | */ 799 | fun removeElements(vararg elements: Element) = removeElements(listOf(*elements)) 800 | 801 | /** 802 | * Removes the elements from the node. 803 | * @param elements The elements to remove 804 | * 805 | * @throws IllegalArgumentException If any [elements] can't be found 806 | */ 807 | fun removeElements(elements: Iterable) = 808 | elements 809 | .map { findIndex(it) } 810 | .sortedDescending() 811 | .forEach { removeChildAt(it) } 812 | 813 | private fun removeChildAt(index: Int) { 814 | val child = _children.removeAt(index) 815 | setParentIfNode(child, null) 816 | } 817 | 818 | /** 819 | * Replaces a node with a different node. 820 | * @param existing The existing node to replace 821 | * @param newNode The node to replace [existing] with 822 | * 823 | * @throws IllegalArgumentException If [existing] can't be found 824 | */ 825 | @Deprecated( 826 | message = "Use replaceElement instead", 827 | replaceWith = ReplaceWith("replaceElement(exising, newNode)") 828 | ) 829 | fun replaceNode(existing: Node, newNode: Node) = replaceElement(existing, newNode) 830 | 831 | /** 832 | * Replaces an element with a different element. 833 | * @param existing The existing element to replace 834 | * @param newElement The element to replace [existing] with 835 | * 836 | * @throws IllegalArgumentException If [existing] can't be found 837 | */ 838 | fun replaceElement(existing: Element, newElement: Element) { 839 | val index = findIndex(existing) 840 | 841 | setParentIfNode(newElement, this) 842 | setParentIfNode(existing, null) 843 | 844 | _children[index] = newElement 845 | } 846 | 847 | /** 848 | * Returns a list containing only elements whose nodeName matches [name]. 849 | */ 850 | fun filter(name: String): List = filter { it.nodeName == name } 851 | 852 | /** 853 | * Returns a list containing only elements matching the given [predicate]. 854 | */ 855 | fun filter(predicate: (Node) -> Boolean): List = filterChildrenToNodes().filter(predicate) 856 | 857 | /** 858 | * Returns the first element whose nodeName matches [name]. 859 | * @throws [NoSuchElementException] if no such element is found. 860 | */ 861 | fun first(name: String): Node = filterChildrenToNodes().first { it.nodeName == name } 862 | 863 | /** 864 | * Returns the first element matching the given [predicate]. 865 | * @throws [NoSuchElementException] if no such element is found. 866 | */ 867 | fun first(predicate: (Element) -> Boolean): Element = _children.first(predicate) 868 | 869 | /** 870 | * Returns the first element whose nodeName matches [name], or `null` if element was not found. 871 | */ 872 | fun firstOrNull(name: String): Node? = filterChildrenToNodes().firstOrNull { it.nodeName == name } 873 | 874 | /** 875 | * Returns the first element matching the given [predicate], or `null` if element was not found. 876 | */ 877 | fun firstOrNull(predicate: (Element) -> Boolean): Element? = _children.firstOrNull(predicate) 878 | 879 | /** 880 | * Returns `true` if at least one element's nodeName matches [name]. 881 | */ 882 | fun exists(name: String): Boolean = filterChildrenToNodes().any { it.nodeName == name } 883 | 884 | /** 885 | * Returns `true` if at least one element matches the given [predicate]. 886 | */ 887 | fun exists(predicate: (Element) -> Boolean): Boolean = _children.any(predicate) 888 | 889 | private fun filterChildrenToNodes(): List = _children.filterIsInstance(Node::class.java) 890 | 891 | private fun findIndex(element: Element): Int { 892 | return _children 893 | .indexOfFirst { it === element } 894 | .takeUnless { it == -1 } 895 | ?: throw IllegalArgumentException("Element (${element.javaClass} is not a child of '$nodeName'") 896 | } 897 | 898 | private fun setParentIfNode(element: Element, newParent: Node?) { 899 | if (element is Node) { 900 | element.parent = newParent 901 | } 902 | } 903 | 904 | override fun equals(other: Any?): Boolean { 905 | if (other !is Node) { 906 | return false 907 | } 908 | 909 | return EqualsBuilder() 910 | .append(nodeName, other.nodeName) 911 | .append(encoding, other.encoding) 912 | .append(version, other.version) 913 | .append(_attributes, other._attributes) 914 | .append(_globalLevelProcessingInstructions, other._globalLevelProcessingInstructions) 915 | .append(_children, other._children) 916 | .isEquals 917 | } 918 | 919 | override fun hashCode(): Int = HashCodeBuilder() 920 | .append(nodeName) 921 | .append(encoding) 922 | .append(version) 923 | .append(_attributes) 924 | .append(_globalLevelProcessingInstructions) 925 | .append(_children) 926 | .toHashCode() 927 | } 928 | --------------------------------------------------------------------------------