// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.processing.request; import com.yahoo.concurrent.CopyOnWriteHashMap; import java.util.AbstractList; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; import static com.yahoo.text.Lowercase.toLowerCase; /** * An immutable compound name of the general form "a.bb.ccc", * where there can be any number of such compounds, including one or zero. *

* Using CompoundName is generally substantially faster than using strings. * * @author bratseth */ public final class CompoundName { private static final int MAX_CACHE_SIZE = 10_000; private static final Map cache = new CopyOnWriteHashMap<>(); /** The empty compound */ public static final CompoundName empty = CompoundName.from(""); /* The string name of this compound. */ private final String name; private final String lowerCasedName; private final List compounds; /** A hashcode which is always derived from the compounds (NEVER the string) */ private final int hashCode; /** This name with the first component removed */ private final CompoundName rest; /** This name with the last component removed */ private final CompoundName first; /** * Constructs this from a string which may contains dot-separated components * * @throws NullPointerException if name is null */ public CompoundName(String name) { this(name, false); } private CompoundName(String name, boolean useCache) { this(name, parse(name).toArray(new String[0]), useCache); } /** Constructs this from an array of name components which are assumed not to contain dots */ public static CompoundName fromComponents(String ... components) { return new CompoundName(List.of(components)); } /** Constructs this from a list of compounds. */ public CompoundName(List compounds) { this(compounds.toArray(new String[0])); } private CompoundName(String [] compounds) { this(toCompoundString(compounds), compounds, true); } /** * Constructs this from a name with already parsed compounds. * Private to avoid creating names with inconsistencies. * * @param name the string representation of the compounds * @param compounds the compounds of this name */ private CompoundName(String name, String [] compounds, boolean useCache) { this.name = Objects.requireNonNull(name, "Name can not be null"); this.lowerCasedName = toLowerCase(name); if (compounds.length == 1) { if (compounds[0].isEmpty()) { this.compounds = List.of(); this.hashCode = 0; rest = first = this; return; } this.compounds = new ImmutableArrayList(compounds); this.hashCode = this.compounds.hashCode(); rest = first = empty; return; } CompoundName[] children = new CompoundName[compounds.length]; for (int i = 0; i + 1 < children.length; i++) { int start = 0, end = i == 0 ? -1 : children[0].name.length(); for (int j = 0; j + i < children.length; j++) { end += compounds[j + i].length() + 1; if (end == start) throw new IllegalArgumentException("'" + name + "' is not a legal compound name. " + "Consecutive, leading or trailing dots are not allowed."); String subName = this.name.substring(start, end); CompoundName cached = cache.get(subName); children[j] = cached != null ? cached : new CompoundName(subName, this.lowerCasedName.substring(start, end), Arrays.copyOfRange(compounds, j, j + i + 1), i == 0 ? empty : children[j + 1], i == 0 ? empty : children[j]); if (useCache && cached == null) cache.put(subName, children[j]); start += compounds[j].length() + 1; } } this.compounds = new ImmutableArrayList(compounds); this.hashCode = this.compounds.hashCode(); this.rest = children[1]; this.first = children[0]; } private CompoundName(String name, String lowerCasedName, String[] compounds, CompoundName rest, CompoundName first) { this.name = name; this.lowerCasedName = lowerCasedName; this.compounds = new ImmutableArrayList(compounds); this.hashCode = this.compounds.hashCode(); this.rest = rest; this.first = first; } private static List parse(String s) { ArrayList l = null; int p = 0; final int m = s.length(); for (int i = 0; i < m; i++) { if (s.charAt(i) == '.') { if (l == null) l = new ArrayList<>(8); l.add(s.substring(p, i)); p = i + 1; } } if (p == 0) { if (l == null) return List.of(s); l.add(s); } else if (p < m) { l.add(s.substring(p, m)); } else { throw new IllegalArgumentException("'" + s + "' is not a legal compound name. Names can not end with a dot."); } return l; } /** * Returns a compound name which has the given compound string appended to it * * @param name if name is empty this returns this */ public CompoundName append(String name) { if (name.isEmpty()) return this; return append(new CompoundName(name)); } /** * Returns a compound name which has the given compounds appended to it * * @param name if name is empty this returns this */ public CompoundName append(CompoundName name) { if (name.isEmpty()) return this; if (isEmpty()) return name; String [] newCompounds = new String[compounds.size() + name.compounds.size()]; int count = 0; for (String s : compounds) { newCompounds[count++] = s; } for (String s : name.compounds) { newCompounds[count++] = s; } return new CompoundName(concat(this.name, name.name), newCompounds, false); } private static String concat(String name1, String name2) { return name1 + "." + name2; } /** * Returns a compound name which has the given name components prepended to this name, * in the given order, i.e new ComponentName("c").prepend("a","b") will yield "a.b.c". * * @param nameParts if name is empty this returns this */ public CompoundName prepend(String ... nameParts) { if (nameParts.length == 0) return this; if (isEmpty()) return fromComponents(nameParts); List newCompounds = new ArrayList<>(nameParts.length + compounds.size()); newCompounds.addAll(List.of(nameParts)); newCompounds.addAll(this.compounds); return new CompoundName(newCompounds); } /** * Returns the name after the last dot. If there are no dots, the full name is returned. */ public String last() { if (compounds.isEmpty()) return ""; return compounds.get(compounds.size() - 1); } /** * Returns the name before the first dot. If there are no dots the full name is returned. */ public String first() { if (compounds.isEmpty()) return ""; return compounds.get(0); } /** * Returns the first n components of this. * * @throws IllegalArgumentException if this does not have at least n components */ public CompoundName first(int n) { if (compounds.size() < n) throw new IllegalArgumentException("Asked for the first " + n + " components but '" + this + "' only have " + compounds.size() + " components."); if (compounds.size() == n) return this; if (compounds.isEmpty()) return empty; if (compounds.size() - 1 == n) return first; return first.first(n); } /** * Returns the name after the first dot, or "" if this name has no dots */ public CompoundName rest() { return rest; } /** * Returns the name starting after the n first components (i.e dots). * This may be the empty name. * * @throws IllegalArgumentException if this does not have at least that many components */ public CompoundName rest(int n) { if (n == 0) return this; if (compounds.size() < n) throw new IllegalArgumentException("Asked for the rest after " + n + " components but '" + this + "' only have " + compounds.size() + " components."); if (n == 1) return rest(); if (compounds.size() == n) return empty; return rest.rest(n - 1); } /** * Returns the number of compound elements in this. Which is exactly the number of dots in the string plus one. * The size of an empty compound is 0. */ public int size() { return compounds.size(); } /** * Returns the compound element as the given index */ public String get(int i) { return compounds.get(i); } /** * Returns a compound which have the name component at index i set to the given name. * As an optimization, if the given name == the name component at this index, this is returned. */ public CompoundName set(int i, String name) { if (get(i).equals(name)) return this; List newCompounds = new ArrayList<>(compounds); newCompounds.set(i, name); return new CompoundName(newCompounds); } /** * Returns whether this name has more than one element */ public boolean isCompound() { return compounds.size() > 1; } public boolean isEmpty() { return compounds.isEmpty(); } /** * Returns whether the given name is a prefix of this. * Prefixes are taken on the component, not character level, so * "a" is a prefix of "a.b", but not a prefix of "ax.b */ public boolean hasPrefix(CompoundName prefix) { if (prefix.size() > this.size()) return false; int prefixLength = prefix.name.length(); if (prefixLength == 0) return true; if (name.length() > prefixLength && name.charAt(prefixLength) != '.') return false; return name.startsWith(prefix.name); } /** * Returns an immutable list of the components of this */ public List asList() { return compounds; } @Override public int hashCode() { return hashCode; } @Override public boolean equals(Object arg) { if (arg == this) return true; return (arg instanceof CompoundName o) && name.equals(o.name); } /** * Returns the string representation of this - all the name components in order separated by dots. */ @Override public String toString() { return name; } public String getLowerCasedName() { return lowerCasedName; } private static String toCompoundString(String [] compounds) { int all = compounds.length; for (String compound : compounds) all += compound.length(); StringBuilder b = new StringBuilder(all); for (String compound : compounds) b.append(compound).append("."); return b.isEmpty() ? "" : b.substring(0, b.length()-1); } /** * Creates a CompoundName from a string, possibly reusing from cache. * Prefer over constructing on the fly. **/ public static CompoundName from(String name) { CompoundName found = cache.get(name); if (found != null) return found; if (cache.size() < MAX_CACHE_SIZE) { CompoundName compound = new CompoundName(name, true); cache.put(name, compound); return compound; } return new CompoundName(name, false); } private static class ImmutableArrayList extends AbstractList { private final String [] array; ImmutableArrayList(String [] array) { this.array = array; } @Override public String get(int index) { return array[index]; } @Override public int size() { return array.length; } @Override public int hashCode() { int hashCode = 0; for (String s : array) { hashCode = hashCode ^ s.hashCode(); } return hashCode; } } }