context) {
setOverridable(new CompoundName(fieldName), overridable, DimensionBinding.createFrom(getDimensions(), context));
}
/**
* Sets the overridability of a field in this profile,
* this overrides the corresponding setting in the type (if any)
*/
public final void setOverridable(String fieldName, boolean overridable, DimensionValues dimensionValues) {
setOverridable(new CompoundName(fieldName), overridable, DimensionBinding.createFrom(getDimensions(), dimensionValues));
}
/**
* Return all objects that start with the given prefix path using no context. Use "" to list all.
*
* For example, if {a.d => "a.d-value" ,a.e => "a.e-value", b.d => "b.d-value", then calling listValues("a")
* will return {"d" => "a.d-value","e" => "a.e-value"}
*/
public final Map listValues(String prefix) { return listValues(new CompoundName(prefix)); }
/**
* Return all objects that start with the given prefix path using no context. Use "" to list all.
*
* For example, if {a.d => "a.d-value" ,a.e => "a.e-value", b.d => "b.d-value", then calling listValues("a")
* will return {"d" => "a.d-value","e" => "a.e-value"}
*/
public final Map listValues(CompoundName prefix) { return listValues(prefix, null); }
/**
* Return all objects that start with the given prefix path. Use "" to list all.
*
* For example, if {a.d => "a.d-value" ,a.e => "a.e-value", b.d => "b.d-value", then calling listValues("a")
* will return {"d" => "a.d-value","e" => "a.e-value"}
*/
public final Map listValues(String prefix, Map context) {
return listValues(new CompoundName(prefix), context);
}
/**
* Return all objects that start with the given prefix path. Use "" to list all.
*
* For example, if {a.d => "a.d-value" ,a.e => "a.e-value", b.d => "b.d-value", then calling listValues("a")
* will return {"d" => "a.d-value","e" => "a.e-value"}
*/
public final Map listValues(CompoundName prefix, Map context) {
return listValues(prefix, context, null);
}
/**
* Adds all objects that start with the given path prefix to the given value map. Use "" to list all.
*
* For example, if {a.d => "a.d-value" ,a.e => "a.e-value", b.d => "b.d-value", then calling listValues("a")
* will return {"d" => "a.d-value","e" => "a.e-value"}
*/
public Map listValues(CompoundName prefix, Map context, Properties substitution) {
Map values = visitValues(prefix, context).values();
if (substitution == null) return values;
for (Map.Entry entry : values.entrySet()) {
if (entry.getValue().getClass() == String.class) continue; // Shortcut
if (entry.getValue() instanceof SubstituteString) {
entry.setValue(((SubstituteString) entry.getValue()).substitute(context, substitution));
}
}
return values;
}
AllValuesQueryProfileVisitor visitValues(CompoundName prefix, Map context) {
return visitValues(prefix, context, new CompoundNameChildCache());
}
AllValuesQueryProfileVisitor visitValues(CompoundName prefix, Map context,
CompoundNameChildCache pathCache) {
AllValuesQueryProfileVisitor visitor = new AllValuesQueryProfileVisitor(prefix, pathCache);
accept(visitor, DimensionBinding.createFrom(getDimensions(), context), null);
return visitor;
}
/**
* Returns a value from this query profile by resolving the given name:
*
* - The name up to the first dot is the value looked up in the value of this profile
*
- The rest of the name (if any) is used as the name to look up in the referenced query profile
*
*
* If this name does not resolve completely into a value in this or any inherited profile, null is returned.
*/
public final Object get(String name) { return get(name,(Map)null); }
/** Returns a value from this using the given property context for resolution and using this for substitution */
public final Object get(String name, Map context) {
return get(name, context, null);
}
/** Returns a value from this using the given dimensions for resolution */
public final Object get(String name, String[] dimensionValues) {
return get(name, dimensionValues,null);
}
public final Object get(String name, String[] dimensionValues, Properties substitution) {
return get(name, DimensionValues.createFrom(dimensionValues), substitution);
}
/** Returns a value from this using the given dimensions for resolution */
public final Object get(String name, DimensionValues dimensionValues, Properties substitution) {
return get(name, DimensionBinding.createFrom(getDimensions(), dimensionValues), substitution);
}
public final Object get(String name, Map context, Properties substitution) {
return get(name, DimensionBinding.createFrom(getDimensions(), context), substitution);
}
public final Object get(CompoundName name, Map context, Properties substitution) {
return get(name, DimensionBinding.createFrom(getDimensions(), context), substitution);
}
final Object get(String name, DimensionBinding binding,Properties substitution) {
return get(new CompoundName(name), binding, substitution);
}
final Object get(CompoundName name, DimensionBinding binding, Properties substitution) {
Object node = get(name, binding);
if (node != null && node.getClass() == String.class) return node; // Shortcut
if (node instanceof SubstituteString) return ((SubstituteString)node).substitute(binding.getContext(), substitution);
return node;
}
final Object get(CompoundName name, DimensionBinding dimensionBinding) {
return lookup(name, false, dimensionBinding);
}
/**
* Returns the node at the position prescribed by the given name (without doing substitutions) -
* a primitive value, a substitutable string, a query profile, or null if not found.
*/
public final Object lookup(String name, Map context) {
return lookup(new CompoundName(name), true, DimensionBinding.createFrom(getDimensions(),context));
}
/** Sets a value in this or any nested profile using null as context */
public final void set(String name, Object value, QueryProfileRegistry registry) {
set(name, value, (Map)null, registry);
}
/**
* Sets a value in this or any nested profile. Any missing structure needed to set this will be created.
* If this value is already set, this will overwrite the previous value.
*
* @param name the name of the field, possibly a dotted name which will cause setting of a variable in a subprofile
* @param value the value to assign to the name, a primitive wrapper, string or a query profile
* @param context the context used to resolve where this value should be set, or null if none
* @throws IllegalArgumentException if the given name is illegal given the types of this or any nested query profile
* @throws IllegalStateException if this query profile is frozen
*/
public final void set(CompoundName name, Object value, Map context, QueryProfileRegistry registry) {
set(name, value, DimensionBinding.createFrom(getDimensions(), context), registry);
}
public final void set(String name, Object value, Map context, QueryProfileRegistry registry) {
set(new CompoundName(name), value, DimensionBinding.createFrom(getDimensions(), context), registry);
}
public final void set(String name, Object value, String[] dimensionValues, QueryProfileRegistry registry) {
set(name, value, DimensionValues.createFrom(dimensionValues), registry);
}
/**
* Sets a value in this or any nested profile. Any missing structure needed to set this will be created.
* If this value is already set, this will overwrite the previous value.
*
* @param name the name of the field, possibly a dotted name which will cause setting of a variable in a subprofile
* @param value the value to assign to the name, a primitive wrapper, string or a query profile
* @param dimensionValues the dimension values - will be matched by order to the dimensions set in this - if this is
* shorter or longer than the number of dimensions it will be adjusted as needed
* @param registry the registry used to resolve query profile references. If null is passed query profile references
* will cause an exception
* @throws IllegalArgumentException if the given name is illegal given the types of this or any nested query profile
* @throws IllegalStateException if this query profile is frozen
*/
public final void set(String name, Object value, DimensionValues dimensionValues, QueryProfileRegistry registry) {
set(new CompoundName(name), value, DimensionBinding.createFrom(getDimensions(), dimensionValues), registry);
}
// ----------------- Misc
public boolean isExplicit() {
return !getId().isAnonymous();
}
/**
* Switches this from write-only to read-only mode.
* This profile can never be modified again after this method returns.
* Calling this on an already frozen profile has no effect.
*
* Calling this will also freeze any profiles inherited and referenced by this.
*/
// TODO: Remove/simplify as query profiles are not used at query time
public synchronized void freeze() {
if (isFrozen()) return;
resolvedDimensions=getDimensions();
if (variants !=null)
variants.freeze();
if (inherited!=null) {
for (QueryProfile inheritedProfile : inherited)
inheritedProfile.freeze();
}
content.freeze();
inherited= inherited==null ? List.of() : List.copyOf(inherited);
super.freeze();
}
@Override
public String toString() {
return "query profile '" + getId() + "'" + (type!=null ? " of type '" + type.getId() + "'" : "");
}
/**
* Returns a clone of this. The clone will not be frozen and will contain copied inherited and content collections
* pointing to the same values as this.
*/
@Override
public QueryProfile clone() {
if (isFrozen()) return this;
QueryProfile clone = (QueryProfile)super.clone();
if (variants != null)
clone.variants = variants.clone();
if (inherited != null)
clone.inherited = new ArrayList<>(inherited);
if (this.content != null)
clone.content = content.clone();
return clone;
}
/**
* Clones a value of a type which may appear in a query profile if cloning is necessary (i.e if it is
* not immutable). Returns the input type otherwise.
*/
static Object cloneIfNecessary(Object object) {
if (object instanceof QueryProfile) return ((QueryProfile)object).clone();
return object; // Other types are immutable
}
/** Throws IllegalArgumentException if the given string is not a valid query profile name */
public static void validateName(String name) {
Matcher nameMatcher = namePattern.matcher(name);
if ( ! nameMatcher.matches())
throw new IllegalArgumentException("Illegal name '" + name + "'");
}
// ----------------- For subclass use --------------------------------------------------------------------
/** Override this to intercept all writes to this profile (or any nested profiles) */
protected void set(CompoundName name, Object value, DimensionBinding binding, QueryProfileRegistry registry) {
try {
setNode(name, value, null, binding, registry);
}
catch (IllegalArgumentException e) {
throw new IllegalInputException("Could not set '" + name + "' to '" + value + "'", e);
}
}
/**
* Returns true if this value is definitely overridable in this (set and not unoverridable),
* false if it is declared unoverridable (in instance or type), and null if this profile has no
* opinion on the matter because the value is not set in this.
*/
Boolean isLocalOverridable(String localName, DimensionBinding binding) {
if (localLookup(localName, binding) == null) return null; // Not set
if ( ! binding.isNull() && getVariants() != null) {
Boolean variantIsOverriable = getVariants().isOverridable(localName, binding.getValues());
if (variantIsOverriable != null)
return variantIsOverriable;
}
Boolean isLocalInstanceOverridable = isLocalInstanceOverridable(localName);
if (isLocalInstanceOverridable != null)
return isLocalInstanceOverridable;
if (type != null) return type.isOverridable(localName);
return true;
}
protected Boolean isLocalInstanceOverridable(String localName) {
if (overridable == null) return null;
return overridable.get(localName);
}
protected Object lookup(CompoundName name,
boolean allowQueryProfileResult,
DimensionBinding dimensionBinding) {
SingleValueQueryProfileVisitor visitor = new SingleValueQueryProfileVisitor(name.asList(),
allowQueryProfileResult);
accept(visitor, dimensionBinding, null);
return visitor.getResult();
}
protected final void accept(QueryProfileVisitor visitor,
DimensionBinding dimensionBinding,
QueryProfile owner) {
acceptAndEnter("", visitor, dimensionBinding, owner);
}
void acceptAndEnter(String key,
QueryProfileVisitor visitor,
DimensionBinding dimensionBinding,
QueryProfile owner) {
boolean allowContent = visitor.enter(key);
accept(allowContent, visitor, dimensionBinding, owner);
if (allowContent)
visitor.leave(key);
}
/**
* Visit the profiles and values referenced from this in order of decreasing precedence
*
* @param allowContent whether content in this should be visited
* @param visitor the visitor
* @param dimensionBinding the dimension binding to use
*/
final void accept(boolean allowContent,
QueryProfileVisitor visitor,
DimensionBinding dimensionBinding,
QueryProfile owner) {
visitor.onQueryProfile(this, dimensionBinding, owner, null);
if (visitor.isDone()) return;
visitVariants(allowContent, visitor, dimensionBinding);
if (visitor.isDone()) return;
if (allowContent) {
visitContent(visitor, dimensionBinding);
if (visitor.isDone()) return;
}
if (visitor.visitInherited())
visitInherited(allowContent, visitor, dimensionBinding, owner);
}
protected void visitVariants(boolean allowContent, QueryProfileVisitor visitor, DimensionBinding dimensionBinding) {
if (getVariants() != null)
getVariants().accept(allowContent, getType(), visitor, dimensionBinding);
}
protected void visitInherited(boolean allowContent,
QueryProfileVisitor visitor,
DimensionBinding dimensionBinding,
QueryProfile owner) {
if (inherited == null) return;
for (QueryProfile inheritedProfile : inherited) {
inheritedProfile.accept(allowContent,
visitor,
dimensionBinding.createFor(inheritedProfile.getDimensions()),
owner);
if (visitor.isDone()) return;
}
}
private void visitContent(QueryProfileVisitor visitor, DimensionBinding dimensionBinding) {
String contentKey = visitor.getLocalKey();
// Visit this' content
if (contentKey != null) { // Get only the content of the current key
if (type != null)
contentKey = type.unalias(contentKey);
visitor.acceptValue(contentKey, getContent(contentKey), dimensionBinding, this, null);
}
else { // get all content in this
for (Map.Entry entry : getContent().entrySet()) {
visitor.acceptValue(entry.getKey(), entry.getValue(), dimensionBinding, this, null);
if (visitor.isDone()) return;
}
}
}
/** Returns a value from the content of this, or null if not present */
protected Object getContent(String key) {
return content.get(key);
}
/** Returns all the content from this as an unmodifiable map */
protected Map getContent() {
return content.unmodifiableMap();
}
/** Sets the value of a node in this profile - the local name given must not be nested (contain dots) */
protected QueryProfile setLocalNode(String localName, Object value, QueryProfileType parentType,
DimensionBinding dimensionBinding, QueryProfileRegistry registry) {
if (parentType != null && type == null && ! isFrozen())
type = parentType;
value = convertToSubstitutionString(value);
value = checkAndConvertAssignment(localName, value, registry);
localPut(localName, value, dimensionBinding);
return this;
}
/**
* Combines an existing and a new value for a query property key.
* Return the new object to add to the state of the owning profile (/variant), or null if no new value needs to
* be added (usually because the new value was added to the existing).
*/
static Object combineValues(Object newValue, Object existingValue) {
if (newValue instanceof QueryProfile newProfile) {
if ( ! (existingValue instanceof QueryProfile)) {
if ( ! isModifiable(newProfile)) {
// Make the query profile reference overridable
newProfile = new BackedOverridableQueryProfile(newProfile);
}
newProfile.value = existingValue;
return newProfile;
}
// if both are profiles:
return combineProfiles(newProfile, (QueryProfile)existingValue);
}
else {
if (existingValue instanceof QueryProfile existingProfile) { // we need to set a non-leaf value on a query profile
if (isModifiable(existingProfile)) {
existingProfile.setValue(newValue);
return null;
}
else {
QueryProfile existingOverridable = new BackedOverridableQueryProfile((QueryProfile)existingValue);
existingOverridable.setValue(newValue);
return existingOverridable;
}
}
else {
return newValue;
}
}
}
private static QueryProfile combineProfiles(QueryProfile newProfile, QueryProfile existingProfile) {
QueryProfile returnValue = null;
QueryProfile existingModifiable;
// Ensure the existing profile is modifiable
if (existingProfile.getClass() == QueryProfile.class) {
existingModifiable = new BackedOverridableQueryProfile(existingProfile);
returnValue = existingModifiable;
}
else { // is an overridable wrapper
existingModifiable = existingProfile; // May be used as-is
}
// Make the existing profile inherit the new one
if (existingModifiable instanceof BackedOverridableQueryProfile)
((BackedOverridableQueryProfile)existingModifiable).addInheritedHere(newProfile);
else
existingModifiable.addInherited(newProfile);
// Remove content from the existing which the new one does not allow overrides of
if (existingModifiable.content != null) {
for (String key : existingModifiable.content.unmodifiableMap().keySet()) {
if ( ! newProfile.isLocalOverridable(key, null)) {
existingModifiable.content.remove(key);
}
}
}
return returnValue;
}
/** Returns whether the given profile may be modified from this profile */
private static boolean isModifiable(QueryProfile profile) {
if (profile.isFrozen()) return false;
if ( ! profile.isExplicit()) return true; // Implicitly defined from this - ok to modify then
if (! (profile instanceof BackedOverridableQueryProfile)) return false;
return true;
}
/**
* Converts to the type of the receiving field, if possible and necessary.
*
* @return the value to be assigned: the original or a converted value
* @throws IllegalArgumentException if the assignment is illegal
*/
protected Object checkAndConvertAssignment(String localName, Object value, QueryProfileRegistry registry) {
if (type == null) return value; // no type checking
FieldDescription fieldDescription = type.getField(localName);
if (fieldDescription == null) {
if (type.isStrict())
throw new IllegalInputException("'" + localName + "' is not declared in " + type + ", and the type is strict");
return value;
}
if (registry == null && (fieldDescription.getType() instanceof QueryProfileFieldType))
throw new IllegalInputException("A registry was not passed: Query profile references is not supported");
Object convertedValue = fieldDescription.getType().convertFrom(value, registry);
if (convertedValue == null)
throw new IllegalInputException("'" + value + "' is not a " + fieldDescription.getType().toInstanceDescription());
return convertedValue;
}
/**
* Looks up all inherited profiles and adds any that matches this name.
* This default implementation returns an empty profile.
*/
protected QueryProfile createSubProfile(String name, DimensionBinding dimensionBinding) {
var id = owner != null ? owner.createAnonymousId(name) : ComponentId.createAnonymousComponentId(name);
return new QueryProfile(id, source, owner);
}
/** Do a variant-aware content lookup in this */
protected Object localLookup(String name, DimensionBinding dimensionBinding) {
Object node = null;
if ( variants != null && ! dimensionBinding.isNull())
node = variants.get(name,type,true, dimensionBinding);
if (node == null)
node = content == null ? null : content.get(name);
return node;
}
// ----------------- Private ----------------------------------------------------------------------------------
private Boolean isDeclaredOverridable(CompoundName name, DimensionBinding dimensionBinding) {
QueryProfile parent = lookupParentExact(name, true, dimensionBinding);
if (parent.overridable == null) return null;
return parent.overridable.get(name.last());
}
/**
* Sets the overridability of a field in this profile,
* this overrides the corresponding setting in the type (if any)
*/
private void setOverridable(CompoundName name, boolean overridable, DimensionBinding dimensionBinding) {
QueryProfile parent = lookupParentExact(name, true, dimensionBinding);
if (dimensionBinding.isNull()) {
if (parent.overridable == null)
parent.overridable = new HashMap<>();
parent.overridable.put(name.last(), overridable);
}
else {
variants.setOverridable(name.last(), overridable, dimensionBinding.getValues());
}
}
/** Sets a value to a (possibly non-local) node. */
private void setNode(CompoundName name, Object value, QueryProfileType parentType,
DimensionBinding dimensionBinding, QueryProfileRegistry registry) {
ensureNotFrozen();
if (name.isCompound()) {
QueryProfile parent = getQueryProfileExact(name.first(), true, dimensionBinding);
parent.setNode(name.rest(), value, parentType, dimensionBinding.createFor(parent.getDimensions()), registry);
}
else {
setLocalNode(name.toString(), value,parentType, dimensionBinding, registry);
}
}
/**
* Looks up and, if necessary, creates, the query profile which should hold the given local name portion of the
* given name. If the name contains no dots, this is returned.
*
* @param name the name of the variable to lookup the parent of
* @param create whether or not to create the parent if it is not present
* @return the parent, or null if not present and created is false
*/
private QueryProfile lookupParentExact(CompoundName name, boolean create, DimensionBinding dimensionBinding) {
CompoundName rest = name.rest();
if (rest.isEmpty()) return this;
QueryProfile topmostParent = getQueryProfileExact(name.first(), create, dimensionBinding);
if (topmostParent == null) return null;
return topmostParent.lookupParentExact(rest, create, dimensionBinding.createFor(topmostParent.getDimensions()));
}
/**
* Returns a query profile from this by name
*
* @param localName the local name of the profile in this, this is never a compound
* @param create whether the profile should be created if missing
* @return the created profile, or null if not present, and create is false
*/
private QueryProfile getQueryProfileExact(String localName, boolean create, DimensionBinding dimensionBinding) {
Object node = localExactLookup(localName, dimensionBinding);
if (node instanceof QueryProfile) return (QueryProfile)node;
if ( ! create) return null;
QueryProfile queryProfile = createSubProfile(localName,dimensionBinding);
if (type != null) {
Class> legalClass = type.getValueClass(localName);
if (legalClass == null || ! legalClass.isInstance(queryProfile))
throw new RuntimeException("'" + localName + "' is not a legal query profile reference name in " + this);
queryProfile.setType(type.getType(localName));
}
localPut(localName, queryProfile, dimensionBinding);
return queryProfile;
}
/** Do a variant-aware content lookup in this - without looking in any wrapped content. But by matching variant bindings exactly only */
private Object localExactLookup(String name, DimensionBinding dimensionBinding) {
if (dimensionBinding.isNull()) return content == null ? null : content.get(name);
if (variants == null) return null;
QueryProfileVariant variant = variants.getVariant(dimensionBinding.getValues(),false);
if (variant == null) return null;
return variant.values().get(name);
}
/** Sets a value directly in this query profile (unless frozen) */
private void localPut(String localName, Object value, DimensionBinding dimensionBinding) {
ensureNotFrozen();
if (type != null)
localName = type.unalias(localName);
validateName(localName);
if (dimensionBinding.isNull()) {
Object combinedValue = value instanceof QueryProfile
? combineValues(value, content == null ? null : content.get(localName))
: combineValues(value, localLookup(localName, dimensionBinding));
if (combinedValue != null)
content.put(localName, combinedValue);
}
else {
if (variants == null)
variants = new QueryProfileVariants(dimensionBinding.getDimensions(), this);
variants.set(localName, dimensionBinding.getValues(), value);
}
}
/** Returns this value, or its corresponding substitution string if it contains substitutions */
static Object convertToSubstitutionString(Object value) {
if (value == null) return value;
if (value.getClass() != String.class) return value;
SubstituteString substituteString = SubstituteString.create((String)value);
if (substituteString == null) return value;
return substituteString;
}
private static final Pattern namePattern = Pattern.compile("[$a-zA-Z_/][-$a-zA-Z0-9_/()]*");
/**
* Returns a compiled version of this which produces faster lookup times
*
* @param registry the registry this will be added to by the caller, or null if none
*/
public CompiledQueryProfile compile(CompiledQueryProfileRegistry registry) {
return QueryProfileCompiler.compile(this, registry);
}
}