| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to you under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.jmeter.dsl |
| |
| import org.apache.jmeter.gui.tree.JMeterTreeNode |
| import org.apache.jmeter.testelement.TestElement |
| import org.apache.jmeter.testelement.TestElementSchema |
| import org.apache.jmeter.testelement.property.BooleanProperty |
| import org.apache.jmeter.testelement.property.CollectionProperty |
| import org.apache.jmeter.testelement.property.DoubleProperty |
| import org.apache.jmeter.testelement.property.FloatProperty |
| import org.apache.jmeter.testelement.property.IntegerProperty |
| import org.apache.jmeter.testelement.property.JMeterProperty |
| import org.apache.jmeter.testelement.property.LongProperty |
| import org.apache.jmeter.testelement.property.MultiProperty |
| import org.apache.jmeter.testelement.property.StringProperty |
| import org.apache.jmeter.testelement.property.TestElementProperty |
| import org.apache.jmeter.testelement.schema.PropertyDescriptor |
| import org.apache.jorphan.collections.HashTree |
| import org.apache.jorphan.collections.HashTreeTraverser |
| import org.apiguardian.api.API |
| import java.io.StringWriter |
| |
| /** |
| * Prints [HashTree] or [TestElement] as JMeter DSL. |
| * @since 5.6 |
| */ |
| @API(status = API.Status.EXPERIMENTAL, since = "5.6") |
| public class DslPrinterTraverser( |
| private val detailLevel: DetailLevel = DetailLevel.NON_DEFAULT |
| ) : HashTreeTraverser { |
| public enum class DetailLevel { |
| ALL, NON_DEFAULT |
| } |
| private companion object { |
| val SPECIAL_CHARS = Regex("[\"\n\r\t\b\\\\$]") |
| } |
| |
| private val sw = StringWriter() |
| private var level = 0 |
| private var plusPosition = -1 |
| |
| override fun toString(): String = sw.toString() |
| |
| public fun append(testElement: TestElement): DslPrinterTraverser { |
| addNode(testElement, HashTree()) |
| subtractNode() |
| return this |
| } |
| |
| override fun addNode(node: Any, subTree: HashTree) { |
| if (sw.buffer.isNotEmpty()) { |
| append('\n') |
| } |
| when (node) { |
| is TestElement -> appendElement(node) |
| is JMeterTreeNode -> appendElement(node.testElement) |
| } |
| } |
| |
| private fun appendElement(te: TestElement) { |
| indent() |
| plusPosition = sw.buffer.length |
| append(te::class.java.name).append("::class {\n") |
| level += 1 |
| appendProperties(te, true) |
| } |
| |
| override fun subtractNode() { |
| level -= 1 |
| // Omit empty braces in case the element did not have properties |
| // Note: if all the properties are default, we do not print them, |
| // so we don't use "properties.isEmpty" condition |
| if (sw.buffer.endsWith(" {\n")) { |
| sw.buffer.setLength(sw.buffer.length - " {\n".length) |
| sw.buffer.insert(plusPosition, '+') |
| append("\n") |
| } else { |
| indent().append("}\n") |
| } |
| } |
| |
| override fun processPath() { |
| } |
| |
| private fun appendProperties(te: TestElement, canSkipTestClass: Boolean) { |
| val emptyTe = te::class.java.getDeclaredConstructor().newInstance() |
| |
| val schema = te.schema |
| val schemaProps = mutableMapOf<PropertyDescriptor<*, *>, JMeterProperty>() |
| val otherProps = mutableListOf<JMeterProperty>() |
| for (property in te.propertyIterator()) { |
| if (emptyTe.getPropertyOrNull(property.name) == property && detailLevel != DetailLevel.ALL) { |
| // If the property is the same as in newly created element, avoid printing it in the DSL |
| continue |
| } |
| val prop = schema.properties[property.name] |
| if (detailLevel != DetailLevel.ALL) { |
| val stringValue = property.stringValue |
| if (prop == TestElementSchema.testClass && stringValue == te::class.java.name && canSkipTestClass) { |
| continue |
| } |
| // It might be tempting to skip printing the property if its value matches the default value, |
| // However, it would be wrong because "unset" values might be overriden by "... Request Defaults", |
| // so we do not want accidental overrides if the user explicitly set some of the properties |
| } |
| |
| if (prop == null) { |
| otherProps += property |
| continue |
| } |
| |
| schemaProps[prop] = property |
| } |
| if (schemaProps.isNotEmpty()) { |
| indent().append("props {\n") |
| level += 1 |
| for ((prop, value) in schemaProps) { |
| indent().append("it[") |
| for (item in schema.getGroupPath(prop)) { |
| append(item).append('.') |
| } |
| append(prop.shortName).append("] = ") |
| appendPropertyValue(value) |
| append('\n') |
| } |
| level -= 1 |
| indent().append("}\n") |
| } |
| |
| for (property in otherProps) { |
| if (property is StringProperty && property.stringValue == "") { |
| continue |
| } |
| indent() |
| .append("setProperty(") |
| // For TestElementProperty we have to use setProperty(Property) method |
| // which lacks property name argument, so we generate property name argument |
| // only for simple properties |
| val usePropertyConstructor = property is TestElementProperty || property is MultiProperty |
| if (usePropertyConstructor) { |
| append(property::class.java.simpleName).append('(') |
| } |
| appendLiteral(property.name).append(", ") |
| appendPropertyValue(property) |
| if (usePropertyConstructor) { |
| append(')') |
| } |
| append(")\n") |
| } |
| } |
| |
| private fun appendPropertyValue(property: JMeterProperty): DslPrinterTraverser = apply { |
| when (property) { |
| is BooleanProperty, is IntegerProperty -> append(property.stringValue) |
| is DoubleProperty -> append(property.stringValue) |
| is FloatProperty -> append(property.stringValue).append('f') |
| is LongProperty -> append(property.stringValue).append('d') |
| is StringProperty -> property.stringValue?.let { appendLiteral(it) } ?: run { append("null") } |
| |
| is TestElementProperty -> { |
| val element = property.element |
| append(element::class.java.name).append("()").append(".apply {\n") |
| level += 1 |
| appendProperties(element, canSkipTestClass = false) |
| level -= 1 |
| if (sw.buffer.endsWith(".apply {\n")) { |
| sw.buffer.setLength(sw.buffer.length - ".apply {\n".length) |
| } else { |
| indent().append("}") |
| } |
| } |
| |
| is CollectionProperty -> { |
| append("listOf(\n") |
| level += 1 |
| for (property1 in property.iterator()) { |
| indent() |
| appendPropertyValue(property1) |
| append(",\n") |
| } |
| level -= 1 |
| if (sw.buffer.endsWith("(\n")) { |
| sw.buffer.setLength(sw.buffer.length - 1) |
| append(")") |
| } else { |
| indent().append(")") |
| } |
| } |
| |
| else -> append("/* unsupported property type ${property::class.java.simpleName}*/").appendLiteral(property.stringValue) |
| } |
| } |
| |
| private fun append(value: String) = apply { sw.append(value) } |
| |
| private fun append(value: Char) = apply { sw.append(value) } |
| |
| private fun appendLiteral(literal: String): DslPrinterTraverser { |
| return apply { |
| append('"') |
| append( |
| literal.replace(SPECIAL_CHARS) { |
| when (it.value) { |
| "\"" -> "\\\"" |
| "\n" -> "\\n" |
| "\r" -> "\\r" |
| "\t" -> "\\t" |
| "\b" -> "\\b" |
| "\\" -> "\\\\" |
| "$" -> "\\$" |
| else -> "\\${it.value}" |
| } |
| } |
| ) |
| append('"') |
| } |
| } |
| |
| private fun indent(): DslPrinterTraverser = apply { |
| repeat(level) { |
| sw.append(" ") |
| } |
| } |
| } |