org.jboss.hal.meta.AddressTemplate Maven / Gradle / Ivy
/*
* Copyright 2022 Red Hat
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://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.jboss.hal.meta;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import org.jboss.hal.dmr.ModelNode;
import org.jboss.hal.dmr.ModelNodeHelper;
import org.jboss.hal.dmr.Property;
import org.jboss.hal.dmr.ResourceAddress;
import org.jboss.hal.spi.EsParam;
import com.google.common.collect.Lists;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
/**
* Template for a DMR address which might contain multiple variable parts.
*
* An address template can be defined using the following BNF:
*
*
* <address template> ::= "/" | <segment>
* <segment> ::= <tuple> | <segment>"/"<tuple>
* <tuple> ::= <variable> | <key>"="<value>
* <variable> ::= "{"<alpha>"}"
* <key> ::= <alpha>
* <value> ::= <variable> | <alpha> | "*"
* <alpha> ::= <upper> | <lower>
* <upper> ::= "A" | "B" | … | "Z"
* <lower> ::= "a" | "b" | … | "z"
*
*
* Following variables are supported: - {domain.controller}
- {selected.profile}
-
* {selected.group}
- {selected.server-config}
- {selected.server}
*
* To get a fully qualified address from an address template use the method resolve()
. For standalone mode the
* variables will resolve to an empty string. The values of the variables are managed by the {@link StatementContext}.
*
*
* AddressTemplate a2 = AddressTemplate.of("{selected.profile}");
* AddressTemplate a3 = AddressTemplate.of("{selected.profile}/subsystem=mail");
* AddressTemplate a4 = AddressTemplate.of("{selected.profile}/subsystem=mail/mail-session=*");
*
*/
public final class AddressTemplate implements Iterable {
/**
* The root template
*/
public static final AddressTemplate ROOT = AddressTemplate.of("/");
public static final String EQUALS = "=";
/** Creates a new address template from a well known placeholder. */
public static AddressTemplate of(StatementContext.Expression placeholder) {
return AddressTemplate.of(String.join("/", placeholder.expression()));
}
/**
* Creates a new address template from a placeholder and an encoded string template. '/' characters inside values must have
* been encoded using {@link ModelNodeHelper#encodeValue(String)}.
*/
public static AddressTemplate of(StatementContext.Expression placeholder, String template) {
return AddressTemplate.of(String.join("/", placeholder.expression(), withoutSlash(template)));
}
/** Creates a new address template from two placeholders. */
public static AddressTemplate of(StatementContext.Expression placeholder1,
StatementContext.Expression placeholder2) {
return AddressTemplate.of(
String.join("/", placeholder1.expression(), placeholder2.expression()));
}
/**
* Creates a new address template from two placeholders and an encoded string template. '/' characters inside values must
* have been encoded using {@link ModelNodeHelper#encodeValue(String)}.
*/
public static AddressTemplate of(StatementContext.Expression placeholder1, StatementContext.Expression placeholder2,
String template) {
return AddressTemplate.of(
String.join("/", placeholder1.expression(), placeholder2.expression(), withoutSlash(template)));
}
/** Creates a new address template from an encoded string template. */
public static AddressTemplate of(String template) {
return new AddressTemplate(withSlash(template));
}
/**
* Turns a resource address into an address template which is the opposite of {@link #resolve(StatementContext, String...)}.
*/
public static AddressTemplate of(ResourceAddress address) {
return of(address, null);
}
/**
* Turns a resource address into an address template which is the opposite of {@link #resolve(StatementContext, String...)}.
* Use the {@link Unresolver} function to specify how the segments of the resource address are "unresolved". It is called
* for each segment of the specified resource address.
*/
public static AddressTemplate of(ResourceAddress address, Unresolver unresolver) {
int index = 0;
boolean first = true;
StringBuilder builder = new StringBuilder();
if (address.isDefined()) {
int size = address.size();
for (Iterator iterator = address.asPropertyList().iterator(); iterator.hasNext();) {
Property property = iterator.next();
String name = property.getName();
String value = property.getValue().asString();
if (value.contains("/")) {
value = ModelNodeHelper.encodeValue(value);
}
String segment = unresolver == null
? name + EQUALS + value
: unresolver.unresolve(name, value, first, !iterator.hasNext(), index, size);
builder.append(segment);
if (iterator.hasNext()) {
builder.append("/");
}
first = false;
index++;
}
}
return of(builder.toString());
}
private static String withoutSlash(String template) {
if (template != null) {
return template.startsWith("/") ? template.substring(1) : template;
}
return null;
}
private static String withSlash(String template) {
if (template != null && !template.startsWith(OPTIONAL) && !template.startsWith("/")) {
return "/" + template;
}
return template;
}
// ------------------------------------------------------ template methods
public static final String OPTIONAL = "opt://";
private static final String BLANK = "_blank";
private final String template;
private final LinkedList tokens;
private final boolean optional;
/**
* Creates a new instance from an encoded string template. '/' characters inside values must have been encoded using
* {@link ModelNodeHelper#encodeValue(String)}.
*
* @param template the encoded template.
*/
private AddressTemplate(String template) {
assert template != null : "template must not be null";
this.tokens = parse(template);
this.optional = template.startsWith(OPTIONAL);
this.template = join(optional, tokens);
}
private LinkedList parse(String template) {
LinkedList tokens = new LinkedList<>();
if (template.equals("/")) {
return tokens;
}
String normalized = template.startsWith(OPTIONAL) ? template.substring(OPTIONAL.length()) : template;
StringTokenizer tok = new StringTokenizer(normalized);
while (tok.hasMoreTokens()) {
String nextToken = tok.nextToken();
if (nextToken.contains(EQUALS)) {
String[] split = nextToken.split(EQUALS);
tokens.add(new Token(split[0], split[1]));
} else {
tokens.add(new Token(nextToken));
}
}
return tokens;
}
private String join(boolean optional, List tokens) {
String path = tokens.stream().map(Token::toString).collect(joining("/"));
return optional ? OPTIONAL + path : path;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof AddressTemplate)) {
return false;
}
AddressTemplate that = (AddressTemplate) o;
return optional == that.optional && template.equals(that.template);
}
@Override
public int hashCode() {
int result = template.hashCode();
result = 31 * result + (optional ? 1 : 0);
return result;
}
/**
* @return the string representation of this address template
*/
@Override
public String toString() {
return template.length() == 0 ? "/" : template;
}
/**
* @return true if this template contains no tokens, false otherwise
*/
public boolean isEmpty() {
return tokens.isEmpty();
}
/**
* @return the number of tokens
*/
public int size() {
return tokens.size();
}
@Override
public Iterator iterator() {
return tokens.stream().map(Token::toString).collect(toList()).iterator();
}
/**
* Appends the specified encoded template to this template and returns a new template. If the specified template does not
* start with a slash, '/' is automatically appended. '/' characters inside values must have been encoded using
* {@link ModelNodeHelper#encodeValue(String)}.
*
* @param template the encoded template to append (makes no difference whether it starts with '/' or not)
* @return a new template
*/
public AddressTemplate append(String template) {
String slashTemplate = template.startsWith("/") ? template : "/" + template;
return AddressTemplate.of(this.template + slashTemplate);
}
public AddressTemplate append(AddressTemplate template) {
return append(template.toString());
}
/**
* Works like {@link List#subList(int, int)} over the tokens of this template and throws the same exceptions.
*
* @param fromIndex low endpoint (inclusive) of the sub template
* @param toIndex high endpoint (exclusive) of the sub template
* @return a new address template containing the specified tokens.
* @throws IndexOutOfBoundsException for an illegal endpoint index value (fromIndex < 0 || toIndex > size
* || fromIndex > toIndex)
*/
public AddressTemplate subTemplate(int fromIndex, int toIndex) {
LinkedList subTokens = new LinkedList<>(this.tokens.subList(fromIndex, toIndex));
return AddressTemplate.of(join(this.optional, subTokens));
}
/**
* @return the parent address template or the root template
*/
public AddressTemplate getParent() {
if (isEmpty() || size() == 1) {
return AddressTemplate.of("/");
} else {
return subTemplate(0, size() - 1);
}
}
/**
* Replaces one or more wildcards with the specified values starting from left to right and returns a new address template.
*
* This method does not resolve the address template. The returned template is still unresolved.
*
* @param wildcard the first wildcard (mandatory)
* @param wildcards more wildcards (optional)
* @return a new (still unresolved) address template with the wildcards replaced by the specified values.
*/
public AddressTemplate replaceWildcards(String wildcard, String... wildcards) {
List allWildcards = new ArrayList<>();
allWildcards.add(wildcard);
if (wildcards != null) {
allWildcards.addAll(Arrays.asList(wildcards));
}
LinkedList replacedTokens = new LinkedList<>();
Iterator wi = allWildcards.iterator();
for (Token token : tokens) {
if (wi.hasNext() && token.hasKey() && "*".equals(token.getValue())) {
replacedTokens.add(new Token(token.getKey(), wi.next()));
} else {
replacedTokens.add(new Token(token.key, token.value));
}
}
return AddressTemplate.of(join(this.optional, replacedTokens));
}
/**
* @return the name of the first segment or null if this address template is empty.
*/
public String firstName() {
if (!tokens.isEmpty() && tokens.getFirst().hasKey()) {
return tokens.getFirst().getKey();
}
return null;
}
/**
* @return the value of the first segment or null if this address template is empty.
*/
public String firstValue() {
if (!tokens.isEmpty() && tokens.getFirst().hasKey()) {
return tokens.getFirst().getValue();
}
return null;
}
/**
* @return the name of the last segment or null if this address template is empty.
*/
public String lastName() {
if (!tokens.isEmpty() && tokens.getLast().hasKey()) {
return tokens.getLast().getKey();
}
return null;
}
/**
* @return the value of the last segment or null if this address template is empty.
*/
public String lastValue() {
if (!tokens.isEmpty() && tokens.getLast().hasKey()) {
return tokens.getLast().getValue();
}
return null;
}
public boolean isOptional() {
return optional;
}
/**
* @return the address template
*/
String getTemplate() {
return template;
}
// ------------------------------------------------------ resolve
/**
* Resolve this address template against the specified statement context.
*
* @param context the statement context
* @param wildcards An optional list of values which are used to resolve any wildcards in this address template from left to
* right
* @return a fully qualified resource address which might be empty, but which does not contain any tokens
*/
public ResourceAddress resolve(StatementContext context, String... wildcards) {
if (isEmpty()) {
return ResourceAddress.root();
}
int wildcardCount = 0;
ModelNode model = new ModelNode();
Memory tupleMemory = new Memory<>();
Memory valueMemory = new Memory<>();
for (Token token : tokens) {
if (!token.hasKey()) {
// a single token, something like "{foo}" of "bar"
String value = token.getValue();
String[] resolvedValue;
if (value.startsWith("{")) {
String variable = value.substring(1, value.length() - 1);
value = variable;
if (!tupleMemory.contains(variable)) {
String[] resolvedTuple = context.resolveTuple(variable, this);
if (resolvedTuple != null) {
tupleMemory.memorize(variable, singletonList(resolvedTuple));
}
}
resolvedValue = tupleMemory.next(value);
} else {
assert value.contains(EQUALS) : "Invalid token expression " + value;
resolvedValue = value.split(EQUALS);
}
if (resolvedValue != null) {
model.add(resolvedValue[0], ModelNodeHelper.decodeValue(resolvedValue[1]));
}
} else {
// a key/value token, something like "foo=bar", "foo=*", "{foo}=bar" or "foo={bar}"
String keyRef = token.getKey();
String valueRef = token.getValue();
String resolvedKey = resolveSome(context, valueMemory, keyRef);
String resolvedValue = resolveSome(context, valueMemory, valueRef);
if (resolvedKey == null) {
resolvedKey = BLANK;
}
if (resolvedValue == null) {
resolvedValue = BLANK;
}
// wildcards
String addressValue = resolvedValue;
if ("*".equals(
resolvedValue) && wildcards != null && wildcards.length > 0 && wildcardCount < wildcards.length) {
addressValue = wildcards[wildcardCount];
wildcardCount++;
}
model.add(resolvedKey, ModelNodeHelper.decodeValue(addressValue));
}
}
return new ResourceAddress(model);
}
private String resolveSome(StatementContext context, Memory memory, String input) {
String resolved;
if (input.startsWith("{")) {
input = input.substring(1, input.length() - 1);
if (!memory.contains(input)) {
if (context.resolve(input, this) != null) {
memory.memorize(input, Lists.newArrayList(context.resolve(input, this)));
}
}
resolved = memory.next(input);
} else {
resolved = input;
}
return resolved;
}
// ------------------------------------------------------ JS methods
/**
* Append an address to this addrress template and return a new one.
*
* @param address The address to append.
* @return a new address template with the specified address added at the end.
*/
public AddressTemplate jsAppend(@EsParam("string|AddressTemplate") Object address) {
if (address instanceof String) {
return append(((String) address));
} else if (address instanceof AddressTemplate) {
return append(((AddressTemplate) address));
}
return this;
}
// ------------------------------------------------------ inner classes
@FunctionalInterface
public interface Unresolver {
String unresolve(String name, String value, boolean first, boolean last, int index, int size);
}
private static class Memory {
final Map> values = new HashMap<>();
final Map indexes = new HashMap<>();
boolean contains(String key) {
return values.containsKey(key);
}
void memorize(String key, List resolved) {
int startIdx = resolved.isEmpty() ? 0 : resolved.size() - 1;
values.put(key, resolved);
indexes.put(key, startIdx);
}
T next(String key) {
T result = null;
if (values.containsKey(key) && indexes.containsKey(key)) {
List items = values.get(key);
Integer idx = indexes.get(key);
if (!items.isEmpty() && idx >= 0) {
result = items.get(idx);
indexes.put(key, --idx);
}
}
return result;
}
}
private static class StringTokenizer {
private static final String DELIMITER = "/";
private int pos;
private final LinkedList tokens;
StringTokenizer(String s) {
this.tokens = tokenize(s);
}
private LinkedList tokenize(String s) {
int pos = 0;
final int len = s.length();
while (pos < len && StringTokenizer.DELIMITER.indexOf(s.charAt(pos)) != -1) {
pos++;
}
final String forTokenization = (pos == 0) ? s : s.substring(pos);
final LinkedList ephemeralTokens = new LinkedList();
final String[] rawTokens = forTokenization.split(StringTokenizer.DELIMITER);
for (int i = 0; i < rawTokens.length; i++) {
final String rawToken = rawTokens[i];
if (rawToken.contains("=") || rawToken.startsWith("{")) {
ephemeralTokens.add(rawToken);
} else {
// HAL-1906
ephemeralTokens.set(ephemeralTokens.size() - 1,
ephemeralTokens.getLast() + StringTokenizer.DELIMITER + rawToken);
}
}
return ephemeralTokens;
}
String nextToken() {
if (!hasMoreTokens()) {
throw new NoSuchElementException();
}
final String result = this.tokens.get(pos++);
return result;
}
boolean hasMoreTokens() {
return pos < this.tokens.size();
}
}
private static class Token {
final String key;
final String value;
Token(String key, String value) {
this.key = key;
this.value = value;
}
Token(String value) {
this.key = null;
this.value = value;
}
boolean hasKey() {
return key != null;
}
String getKey() {
return key;
}
String getValue() {
return value;
}
@Override
public String toString() {
return hasKey() ? key + EQUALS + value : value;
}
}
}