> pendingReplacements = new HashMap<>();
public MethodCallContext(TestElement testElement, HashTree childrenTree,
MethodCallContext parent, MethodCallBuilderRegistry builderRegistry) {
this.testElement = testElement;
// sorting simplifies code builder
this.childrenTree = childrenTree == null ? new ListedHashTree() : sortTree(childrenTree);
this.parent = parent;
this.root = parent == null ? this : parent.root;
this.builderRegistry = builderRegistry;
}
private HashTree sortTree(HashTree tree) {
ListedHashTree ret = new ListedHashTree();
tree.list().stream()
.sorted(Comparator.comparingInt(k -> k instanceof ConfigElement ? 0 : 1))
.forEach(k -> ret.set(k, sortTree(tree.getTree(k))));
return ret;
}
/**
* Gets the JMeter test element associated to this context.
*
* @return the test element.
*/
public TestElement getTestElement() {
return testElement;
}
/**
* Gets the parent context.
*
* This is useful in some scenarios to register end listeners on parent node, or access root
* context for globally shared entries (check {@link #setEntry(Object, Object)}).
*
* @return the parent context. Null is returned if the current context is the root context.
*/
public MethodCallContext getParent() {
return parent;
}
/**
* Gets the root context associated to the test plan.
*
* This is useful when some data has to only be processed once and at root of the test plan build
* context.
*
* @return the parent context. Null is returned if the current context is the root context.
*/
public MethodCallContext getRoot() {
return root;
}
/**
* Gets the JMeter test plan subtree of children elements of current context test element.
*
* This is useful when some alteration or inspection is required in the tree before other builder
* methods try to convert contained test elements.
*
* Eg: {@link us.abstracta.jmeter.javadsl.http.DslHttpSampler.CodeBuilder} uses this method to
* remove children HTTP Headers which are directly included as chained methods of httpSampler
* method invocation.
*
* @return the JMeter test plan children subtree.
*/
public HashTree getChildrenTree() {
return childrenTree;
}
/**
* Gets a value associated to a given key in the context.
*
* The context allows you to store (through {@link #setEntry(Object, Object)}) any sort of
* information on it that may be required later on be used by the builder in some other test
* element context (for example: check if a test element already was processed by this builder).
*
* @param key is an object identifying an entry in the context. A simple way of sharing info for a
* MethodCallBuilder is just use the MethodCallBuilder class as key, storing some
* custom class instance with structured context info for the particular
* MethodCallBuilder.
* @return the value associated to the key. Null is returned if no entry is associated to the key.
*/
public Object getEntry(Object key) {
return entries.get(key);
}
/**
* Allows to store a value associated to a given key in the context.
*
* @param key identifies the entry in context to later on be able to retrieve it.
* @param value the value to store in the context, associated to the given key.
* @see #getEntry(Object) for more details
*/
public void setEntry(Object key, Object value) {
entries.put(key, value);
}
/**
* Gets existing entry or creates a new one using provided computation function.
*
* @param key identifies the entry in context to later on be able to retrieve it.
* @param computation function used to build the new entry for the given key, if none exists.
* @see #getEntry(Object) for more details
* @since 1.3
*/
public V computeEntryIfAbsent(Object key, Supplier computation) {
return (V) entries.computeIfAbsent(key, k -> computation.get());
}
/**
* Allows registering logic that needs to be executed at the end of MethodCall build for this
* context.
*
* This allows to do some advance stuff, like registering some action/logic to be executed on a
* parent context after processing current context and only under some specific condition (eg:
* when no other sibling test element is included in parent context).
*
* @param listener specifies the listener containing the logic to be executed at the end of
* MethodCall build.
*/
public void addEndListener(MethodCallContextEndListener listener) {
endListeners.add(listener);
}
/**
* Builds a MethodCall for the current context.
*
* This might be useful in some MethodCallBuilders to trigger a build of children context (after
* removal for example).
*
* @return the {@link MethodCall} instance.
*/
public MethodCall buildMethodCall() {
try {
methodCall = builderRegistry.findBuilderMatchingContext(this)
.map(b -> b.buildMethodCall(this))
.orElseGet(() -> {
LOG.warn("No builder found for {}({}). " + UNSUPPORTED_USAGE_WARNING,
testElement.getClass(), testElement.getName());
return MethodCall.buildUnsupported();
});
root.contextRegistry.put(testElement, this);
methodCall.setCommented(!testElement.isEnabled());
addChildrenTo(methodCall);
executeEndListeners(methodCall);
UnaryOperator replacement = root.pendingReplacements.remove(testElement);
if (replacement != null) {
methodCall = replacement.apply(methodCall);
}
return methodCall;
} catch (RuntimeException e) {
LOG.warn("Could not build code for {}({}). " + UNSUPPORTED_USAGE_WARNING,
testElement.getClass(), testElement.getName(), e);
return MethodCall.buildUnsupported();
}
}
private void executeEndListeners(MethodCall ret) {
endListeners.forEach(l -> l.execute(this, ret));
}
private void addChildrenTo(MethodCall call) {
childrenTree.list().stream()
.map(e -> child((TestElement) e, childrenTree.getTree(e))
.buildMethodCall())
.forEach(call::child);
}
/**
* Allows creating a child context for the given test element and tree.
*
* @param element the test element associated to the child context.
* @param childrenTree the test element subtree.
* @return the created child method context.
* @since 1.3
*/
public MethodCallContext child(TestElement element, HashTree childrenTree) {
return new MethodCallContext(element, childrenTree, this, builderRegistry);
}
/**
* Adds a child method call context, as first child call, built using provided test element and
* tree.
*
* This is useful when it is needed to modify existing test plan, for example, to optimize it and
* use default config elements that avoid code duplication in children elements.
*
* @param testElement specifies the test element from which to create the new method call.
* @param childrenTree specifies children elements of the test element, which are also going to be
* built and attached as children method calls of the method call created for
* the test element.
* @return the created child method call context.
* @since 1.8
*/
public MethodCallContext prependChild(TestElement testElement, HashTree childrenTree) {
MethodCallContext child = child(testElement, childrenTree);
MethodCall childCall = child.buildMethodCall();
methodCall.prependChild(childCall);
return child;
}
/**
* Allows removing an instance of the given test element class from the children tree.
*
* If multiple instances exists, then only the first one is removed.
*
* @param filter specifies condition to be matched by test element to be removed.
* @return the context associated to the removed test element, or null if no test element is
* found.
*/
public MethodCallContext removeChild(Predicate filter) {
Optional> child = childrenTree.list().stream()
.map(o -> (TestElement) o)
.filter(filter)
.findAny();
child.ifPresent(childrenTree::remove);
return child
.map(c -> child((TestElement) c, childrenTree.getTree(c)))
.orElse(null);
}
/**
* Finds the builder associated to the given JMeter test element class.
*
* @param builderClass is the class of the builder to find.
* @param is the type of the builder to find. This provides proper type safety when
* using the method.
* @return the builder associated to the given JMeter test element class, or null if none is
* found.
*/
public T findBuilder(Class builderClass) {
return builderRegistry.findBuilderByClass(builderClass);
}
/**
* The method call created for this context.
*
* This is useful mainly when method calls need to be modified after their creation, for example
* in an end listener ({@link #addEndListener(MethodCallContextEndListener)}).
*
* @return the created method call, if it has been already created, null otherwise.
* @since 1.8
*/
public MethodCall getMethodCall() {
return methodCall;
}
/**
* Allows replacing or transforming the method call associated to a given test element.
*
* This is particularly helpful in scenarios like module controller, where basic conversion of a
* controller has to be replaced by a call to a test fragment containing the target controller
* pointed by the module controller
*
* @param element is the test element associated to the method call to be replaced/transformed.
* @param operator provides the logic to be applied to create a new method call from the original
* test element method call.
* @since 1.3
*/
public void replaceMethodCall(TestElement element, UnaryOperator operator) {
MethodCallContext elementContext = root.contextRegistry.get(element);
if (elementContext != null) {
MethodCall original = elementContext.methodCall;
MethodCall replacement = operator.apply(original);
if (replacement != original) {
elementContext.parent.methodCall.replaceChild(original, replacement);
}
} else {
root.pendingReplacements.put(element, operator);
}
}
public Object getBuilderOption(String optionName) {
return root.getEntry(optionName);
}
public interface MethodCallContextEndListener {
void execute(MethodCallContext context, MethodCall call);
}
}