summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@verizonmedia.com>2019-10-07 13:38:54 +0200
committerJon Bratseth <bratseth@verizonmedia.com>2019-10-07 13:38:54 +0200
commit9f67d07036d056673b7b907371005b126b957589 (patch)
treeeea7c09f06e373bfcfe76130b48768de6a7bd39e
parent81e811c1952f77aa5da5b66c6848b224f61aa0a0 (diff)
Support relative substitution by prefixing a property by dot
-rw-r--r--container-search/abi-spec.json63
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/QueryProfile.java96
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/SubstituteString.java64
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/compiled/Binding.java2
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/compiled/CompiledQueryProfile.java3
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalMap.java2
-rw-r--r--container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalValue.java64
-rw-r--r--container-search/src/test/java/com/yahoo/search/query/profile/test/QueryProfileSubstitutionTestCase.java29
-rw-r--r--container-search/src/test/java/com/yahoo/search/query/profile/test/QueryProfileVariantsTestCase.java57
9 files changed, 279 insertions, 101 deletions
diff --git a/container-search/abi-spec.json b/container-search/abi-spec.json
index facf894272d..edaa5b4b824 100644
--- a/container-search/abi-spec.json
+++ b/container-search/abi-spec.json
@@ -5884,6 +5884,62 @@
],
"fields": []
},
+ "com.yahoo.search.query.profile.SubstituteString$Component": {
+ "superClass": "java.lang.Object",
+ "interfaces": [],
+ "attributes": [
+ "public",
+ "abstract"
+ ],
+ "methods": [
+ "public void <init>()",
+ "protected abstract java.lang.String getValue(java.util.Map, com.yahoo.processing.request.Properties)"
+ ],
+ "fields": []
+ },
+ "com.yahoo.search.query.profile.SubstituteString$PropertyComponent": {
+ "superClass": "com.yahoo.search.query.profile.SubstituteString$Component",
+ "interfaces": [],
+ "attributes": [
+ "public",
+ "final"
+ ],
+ "methods": [
+ "public void <init>(java.lang.String)",
+ "public java.lang.String getValue(java.util.Map, com.yahoo.processing.request.Properties)",
+ "public java.lang.String toString()"
+ ],
+ "fields": []
+ },
+ "com.yahoo.search.query.profile.SubstituteString$RelativePropertyComponent": {
+ "superClass": "com.yahoo.search.query.profile.SubstituteString$Component",
+ "interfaces": [],
+ "attributes": [
+ "public",
+ "final"
+ ],
+ "methods": [
+ "public void <init>(java.lang.String)",
+ "public java.lang.String getValue(java.util.Map, com.yahoo.processing.request.Properties)",
+ "public java.lang.String fieldName()",
+ "public java.lang.String toString()"
+ ],
+ "fields": []
+ },
+ "com.yahoo.search.query.profile.SubstituteString$StringComponent": {
+ "superClass": "com.yahoo.search.query.profile.SubstituteString$Component",
+ "interfaces": [],
+ "attributes": [
+ "public",
+ "final"
+ ],
+ "methods": [
+ "public void <init>(java.lang.String)",
+ "public java.lang.String getValue(java.util.Map, com.yahoo.processing.request.Properties)",
+ "public java.lang.String toString()"
+ ],
+ "fields": []
+ },
"com.yahoo.search.query.profile.SubstituteString": {
"superClass": "java.lang.Object",
"interfaces": [],
@@ -5892,7 +5948,11 @@
],
"methods": [
"public static com.yahoo.search.query.profile.SubstituteString create(java.lang.String)",
+ "public void <init>(java.util.List, java.lang.String)",
+ "public boolean hasRelative()",
"public java.lang.String substitute(java.util.Map, com.yahoo.processing.request.Properties)",
+ "public java.util.List components()",
+ "public java.lang.String stringValue()",
"public int hashCode()",
"public boolean equals(java.lang.Object)",
"public java.lang.String toString()"
@@ -6004,8 +6064,9 @@
],
"methods": [
"public void <init>()",
+ "public java.lang.Object valueFor(com.yahoo.search.query.profile.DimensionBinding)",
"public void add(java.lang.Object, com.yahoo.search.query.profile.DimensionBinding)",
- "public com.yahoo.search.query.profile.compiled.DimensionalValue build()"
+ "public com.yahoo.search.query.profile.compiled.DimensionalValue build(java.util.Map)"
],
"fields": []
},
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfile.java b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfile.java
index acca2d403be..f5f6b2d2550 100644
--- a/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfile.java
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfile.java
@@ -102,7 +102,7 @@ public class QueryProfile extends FreezableSimpleComponent implements Cloneable
*/
public List<QueryProfile> inherited() {
if (isFrozen()) return inherited; // Frozen profiles always have an unmodifiable, non-null list
- if (inherited==null) return Collections.emptyList();
+ if (inherited == null) return Collections.emptyList();
return Collections.unmodifiableList(inherited);
}
@@ -474,17 +474,17 @@ public class QueryProfile extends FreezableSimpleComponent implements Cloneable
/** Returns this value, or its corresponding substitution string if it contains substitutions */
protected 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;
+ 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;
}
/** Returns the field description of this field, or null if it is not typed */
protected FieldDescription getFieldDescription(CompoundName name, DimensionBinding binding) {
- FieldDescriptionQueryProfileVisitor visitor=new FieldDescriptionQueryProfileVisitor(name.asList());
- accept(visitor, binding,null);
+ FieldDescriptionQueryProfileVisitor visitor = new FieldDescriptionQueryProfileVisitor(name.asList());
+ accept(visitor, binding, null);
return visitor.result();
}
@@ -493,23 +493,23 @@ public class QueryProfile extends FreezableSimpleComponent implements Cloneable
* 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
- Boolean isLocalInstanceOverridable=isLocalInstanceOverridable(localName);
- if (isLocalInstanceOverridable!=null)
+ Boolean isLocalOverridable(String localName, DimensionBinding binding) {
+ if (localLookup(localName, binding) == null) return null; // Not set
+ Boolean isLocalInstanceOverridable = isLocalInstanceOverridable(localName);
+ if (isLocalInstanceOverridable != null)
return isLocalInstanceOverridable.booleanValue();
- if (type!=null) return type.isOverridable(localName);
+ if (type != null) return type.isOverridable(localName);
return true;
}
protected Boolean isLocalInstanceOverridable(String localName) {
- if (overridable==null) return null;
+ 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);
+ protected Object lookup(CompoundName name, boolean allowQueryProfileResult, DimensionBinding dimensionBinding) {
+ SingleValueQueryProfileVisitor visitor = new SingleValueQueryProfileVisitor(name.asList(), allowQueryProfileResult);
+ accept(visitor, dimensionBinding, null);
return visitor.getResult();
}
@@ -518,7 +518,7 @@ public class QueryProfile extends FreezableSimpleComponent implements Cloneable
}
void acceptAndEnter(String key, QueryProfileVisitor visitor,DimensionBinding dimensionBinding, QueryProfile owner) {
- boolean allowContent=visitor.enter(key);
+ boolean allowContent = visitor.enter(key);
accept(allowContent, visitor, dimensionBinding, owner);
if (allowContent)
visitor.leave(key);
@@ -548,25 +548,25 @@ public class QueryProfile extends FreezableSimpleComponent implements Cloneable
}
protected void visitVariants(boolean allowContent,QueryProfileVisitor visitor,DimensionBinding dimensionBinding) {
- if (getVariants()!=null)
+ if (getVariants() != null)
getVariants().accept(allowContent, getType(), visitor, dimensionBinding);
}
protected void visitInherited(boolean allowContent,QueryProfileVisitor visitor,DimensionBinding dimensionBinding, QueryProfile owner) {
- if (inherited==null) return;
+ if (inherited == null) return;
for (QueryProfile inheritedProfile : inherited) {
- inheritedProfile.accept(allowContent,visitor,dimensionBinding.createFor(inheritedProfile.getDimensions()), owner);
+ inheritedProfile.accept(allowContent, visitor, dimensionBinding.createFor(inheritedProfile.getDimensions()), owner);
if (visitor.isDone()) return;
}
}
private void visitContent(QueryProfileVisitor visitor,DimensionBinding dimensionBinding) {
- String contentKey=visitor.getLocalKey();
+ 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);
+ 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);
}
else { // get all content in this
@@ -590,11 +590,11 @@ public class QueryProfile extends FreezableSimpleComponent implements Cloneable
/** Sets the value of a node in <i>this</i> 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;
+ if (parentType != null && type == null && ! isFrozen())
+ type = parentType;
- value=checkAndConvertAssignment(localName, value, registry);
- localPut(localName,value,dimensionBinding);
+ value = checkAndConvertAssignment(localName, value, registry);
+ localPut(localName, value, dimensionBinding);
return this;
}
@@ -605,20 +605,20 @@ public class QueryProfile extends FreezableSimpleComponent implements Cloneable
*/
static Object combineValues(Object newValue, Object existingValue) {
if (newValue instanceof QueryProfile) {
- QueryProfile newProfile=(QueryProfile)newValue;
- if ( existingValue==null || ! (existingValue instanceof QueryProfile)) {
+ QueryProfile newProfile = (QueryProfile)newValue;
+ if ( existingValue == null || ! (existingValue instanceof QueryProfile)) {
if (!isModifiable(newProfile))
- newProfile=new BackedOverridableQueryProfile(newProfile); // Make the query profile reference overridable
- newProfile.value=existingValue;
+ newProfile = new BackedOverridableQueryProfile(newProfile); // Make the query profile reference overridable
+ newProfile.value = existingValue;
return newProfile;
}
// if both are profiles:
- return combineProfiles(newProfile,(QueryProfile)existingValue);
+ return combineProfiles(newProfile, (QueryProfile)existingValue);
}
else {
if (existingValue instanceof QueryProfile) { // we need to set a non-leaf value on a query profile
- QueryProfile existingProfile=(QueryProfile)existingValue;
+ QueryProfile existingProfile = (QueryProfile)existingValue;
if (isModifiable(existingProfile)) {
existingProfile.setValue(newValue);
return null;
@@ -636,16 +636,16 @@ public class QueryProfile extends FreezableSimpleComponent implements Cloneable
}
private static QueryProfile combineProfiles(QueryProfile newProfile,QueryProfile existingProfile) {
- QueryProfile returnValue=null;
+ QueryProfile returnValue = null;
QueryProfile existingModifiable;
// Ensure the existing profile is modifiable
- if (existingProfile.getClass()==QueryProfile.class) {
+ if (existingProfile.getClass() == QueryProfile.class) {
existingModifiable = new BackedOverridableQueryProfile(existingProfile);
- returnValue=existingModifiable;
+ returnValue = existingModifiable;
}
else { // is an overridable wrapper
- existingModifiable=existingProfile; // May be used as-is
+ existingModifiable = existingProfile; // May be used as-is
}
// Make the existing profile inherit the new one
@@ -655,7 +655,7 @@ public class QueryProfile extends FreezableSimpleComponent implements Cloneable
existingModifiable.addInherited(newProfile);
// Remove content from the existing which the new one does not allow overrides of
- if (existingModifiable.content!=null) {
+ if (existingModifiable.content != null) {
for (String key : existingModifiable.content.unmodifiableMap().keySet()) {
if ( ! newProfile.isLocalOverridable(key, null)) {
existingModifiable.content.remove(key);
@@ -681,10 +681,10 @@ public class QueryProfile extends FreezableSimpleComponent implements Cloneable
* @throws IllegalArgumentException if the assignment is illegal
*/
protected Object checkAndConvertAssignment(String localName, Object value, QueryProfileRegistry registry) {
- if (type==null) return value; // no type checking
+ if (type == null) return value; // no type checking
- FieldDescription fieldDescription=type.getField(localName);
- if (fieldDescription==null) {
+ FieldDescription fieldDescription = type.getField(localName);
+ if (fieldDescription == null) {
if (type.isStrict())
throw new IllegalArgumentException("'" + localName + "' is not declared in " + type + ", and the type is strict");
return value;
@@ -710,8 +710,8 @@ public class QueryProfile extends FreezableSimpleComponent implements Cloneable
/** 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 ( variants != null && ! dimensionBinding.isNull())
+ node = variants.get(name,type,true, dimensionBinding);
if (node == null)
node = content == null ? null : content.get(name);
return node;
@@ -801,7 +801,7 @@ public class QueryProfile extends FreezableSimpleComponent implements Cloneable
}
/** Sets a value directly in this query profile (unless frozen) */
- private void localPut(String localName,Object value, DimensionBinding dimensionBinding) {
+ private void localPut(String localName, Object value, DimensionBinding dimensionBinding) {
ensureNotFrozen();
if (type != null)
@@ -813,17 +813,17 @@ public class QueryProfile extends FreezableSimpleComponent implements Cloneable
if (dimensionBinding.isNull()) {
Object combinedValue;
if (value instanceof QueryProfile)
- combinedValue = combineValues(value,content==null ? null : content.get(localName));
+ combinedValue = combineValues(value, content == null ? null : content.get(localName));
else
combinedValue = combineValues(value, localLookup(localName, dimensionBinding));
if (combinedValue!=null)
- content.put(localName,combinedValue);
+ content.put(localName, combinedValue);
}
else {
if (variants == null)
variants = new QueryProfileVariants(dimensionBinding.getDimensions(), this);
- variants.set(localName,dimensionBinding.getValues(),value);
+ variants.set(localName, dimensionBinding.getValues(), value);
}
}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/SubstituteString.java b/container-search/src/main/java/com/yahoo/search/query/profile/SubstituteString.java
index 3252f0f4662..446bb250856 100644
--- a/container-search/src/main/java/com/yahoo/search/query/profile/SubstituteString.java
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/SubstituteString.java
@@ -2,6 +2,7 @@
package com.yahoo.search.query.profile;
import com.yahoo.processing.request.Properties;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfile;
import java.util.ArrayList;
import java.util.List;
@@ -22,6 +23,7 @@ public class SubstituteString {
private final List<Component> components;
private final String stringValue;
+ private final boolean hasRelative;
/**
* Returns a new SubstituteString if the given string contains substitutions, null otherwise.
@@ -35,34 +37,48 @@ public class SubstituteString {
int end = value.indexOf("}", start + 2);
if (end < 0)
throw new IllegalArgumentException("Unterminated value substitution '" + value.substring(start) + "'");
- String propertyName = value.substring(start+2,end);
- if (propertyName.indexOf("%{") >= 0)
+ String propertyName = value.substring(start + 2, end);
+ if (propertyName.contains("%{"))
throw new IllegalArgumentException("Unterminated value substitution '" + value.substring(start) + "'");
components.add(new StringComponent(value.substring(lastEnd, start)));
- components.add(new PropertyComponent(propertyName));
- lastEnd = end+1;
+ if (propertyName.startsWith("."))
+ components.add(new RelativePropertyComponent(propertyName.substring(1)));
+ else
+ components.add(new PropertyComponent(propertyName));
+ lastEnd = end + 1;
start = value.indexOf("%{", lastEnd);
}
components.add(new StringComponent(value.substring(lastEnd)));
return new SubstituteString(components, value);
}
- private SubstituteString(List<Component> components, String stringValue) {
+ public SubstituteString(List<Component> components, String stringValue) {
this.components = components;
this.stringValue = stringValue;
+ this.hasRelative = components.stream().anyMatch(component -> component instanceof RelativePropertyComponent);
}
+ /** Returns whether this has at least one relative component */
+ public boolean hasRelative() { return hasRelative; }
+
/**
- * Perform the substitution in this, by looking up in the given query profile,
+ * Perform the substitution in this, by looking up in the given properties,
* and returns the resulting string
+ *
+ * @param context the content which is used to resolve profile variants when looking up substitution values
+ * @param substitution the properties in which values to be substituted are looked up
*/
public String substitute(Map<String, String> context, Properties substitution) {
StringBuilder b = new StringBuilder();
for (Component component : components)
- b.append(component.getValue(context,substitution));
+ b.append(component.getValue(context, substitution));
return b.toString();
}
+ public List<Component> components() { return components; }
+
+ public String stringValue() { return stringValue; }
+
@Override
public int hashCode() {
return stringValue.hashCode();
@@ -81,13 +97,13 @@ public class SubstituteString {
return stringValue;
}
- private abstract static class Component {
+ public abstract static class Component {
protected abstract String getValue(Map<String, String> context, Properties substitution);
}
- private final static class StringComponent extends Component {
+ public final static class StringComponent extends Component {
private final String value;
@@ -107,7 +123,7 @@ public class SubstituteString {
}
- private final static class PropertyComponent extends Component {
+ public final static class PropertyComponent extends Component {
private final String propertyName;
@@ -116,7 +132,7 @@ public class SubstituteString {
}
@Override
- public String getValue(Map<String,String> context, Properties substitution) {
+ public String getValue(Map<String, String> context, Properties substitution) {
Object value = substitution.get(propertyName, context, substitution);
if (value == null) return "";
return String.valueOf(value);
@@ -129,4 +145,30 @@ public class SubstituteString {
}
+ /**
+ * A component where the value should be looked up in the profile containing the substitution field
+ * rather than globally
+ */
+ public final static class RelativePropertyComponent extends Component {
+
+ private final String fieldName;
+
+ public RelativePropertyComponent(String fieldName) {
+ this.fieldName = fieldName;
+ }
+
+ @Override
+ public String getValue(Map<String, String> context, Properties substitution) {
+ throw new IllegalStateException("Should be resolved during compilation");
+ }
+
+ public String fieldName() { return fieldName; }
+
+ @Override
+ public String toString() {
+ return "%{" + fieldName + "}";
+ }
+
+ }
+
}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/compiled/Binding.java b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/Binding.java
index d94d601f103..2774bd4ebf2 100644
--- a/container-search/src/main/java/com/yahoo/search/query/profile/compiled/Binding.java
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/Binding.java
@@ -34,7 +34,7 @@ public class Binding implements Comparable<Binding> {
private final int hashCode;
@SuppressWarnings("unchecked")
- public static final Binding nullBinding= new Binding(Integer.MAX_VALUE, Collections.<String,String>emptyMap());
+ public static final Binding nullBinding = new Binding(Integer.MAX_VALUE, Collections.<String,String>emptyMap());
public static Binding createFrom(DimensionBinding dimensionBinding) {
if (dimensionBinding.getDimensions().size() > maxDimensions)
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/compiled/CompiledQueryProfile.java b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/CompiledQueryProfile.java
index d6e93701ca1..ea85a2be242 100644
--- a/container-search/src/main/java/com/yahoo/search/query/profile/compiled/CompiledQueryProfile.java
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/CompiledQueryProfile.java
@@ -45,7 +45,8 @@ public class CompiledQueryProfile extends AbstractComponent implements Cloneable
/**
* Creates a new query profile from an id.
*/
- public CompiledQueryProfile(ComponentId id, QueryProfileType type,
+ public CompiledQueryProfile(ComponentId id,
+ QueryProfileType type,
DimensionalMap<CompoundName, Object> entries,
DimensionalMap<CompoundName, QueryProfileType> types,
DimensionalMap<CompoundName, Object> references,
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalMap.java b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalMap.java
index b4a1c66e4e0..2e8f5dcf91c 100644
--- a/container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalMap.java
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalMap.java
@@ -58,7 +58,7 @@ public class DimensionalMap<KEY, VALUE> {
public DimensionalMap<KEY, VALUE> build() {
Map<KEY, DimensionalValue<VALUE>> map = new HashMap<>();
for (Map.Entry<KEY, DimensionalValue.Builder<VALUE>> entry : entries.entrySet()) {
- map.put(entry.getKey(), entry.getValue().build());
+ map.put(entry.getKey(), entry.getValue().build(entries));
}
return new DimensionalMap<>(map);
}
diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalValue.java b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalValue.java
index 506472c97d1..5e7f2783393 100644
--- a/container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalValue.java
+++ b/container-search/src/main/java/com/yahoo/search/query/profile/compiled/DimensionalValue.java
@@ -1,7 +1,9 @@
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.search.query.profile.compiled;
+import com.yahoo.processing.request.CompoundName;
import com.yahoo.search.query.profile.DimensionBinding;
+import com.yahoo.search.query.profile.SubstituteString;
import java.util.ArrayList;
import java.util.Collections;
@@ -58,6 +60,15 @@ public class DimensionalValue<VALUE> {
/** The minimal set of variants needed to capture all values at this key */
private Map<VALUE, Value.Builder<VALUE>> buildableVariants = new HashMap<>();
+ /** Returns the value for the given binding, or null if none */
+ public VALUE valueFor(DimensionBinding variantBinding) {
+ for (var entry : buildableVariants.entrySet()) {
+ if (entry.getValue().variants.contains(variantBinding))
+ return entry.getKey();
+ }
+ return null;
+ }
+
public void add(VALUE value, DimensionBinding variantBinding) {
// Note: We know we can index by the value because its possible types are constrained
// to what query profiles allow: String, primitives and query profiles
@@ -69,10 +80,10 @@ public class DimensionalValue<VALUE> {
variant.addVariant(variantBinding);
}
- public DimensionalValue<VALUE> build() {
+ public DimensionalValue<VALUE> build(Map<?, DimensionalValue.Builder<VALUE>> entries) {
List<Value> variants = new ArrayList<>();
for (Value.Builder buildableVariant : buildableVariants.values()) {
- variants.addAll(buildableVariant.build());
+ variants.addAll(buildableVariant.build(entries));
}
return new DimensionalValue(variants);
}
@@ -139,14 +150,17 @@ public class DimensionalValue<VALUE> {
}
/** Build a separate value object for each dimension combination which has this value */
- public List<Value<VALUE>> build() {
+ public List<Value<VALUE>> build(Map<CompoundName, DimensionalValue.Builder<VALUE>> entries) {
// Shortcut for efficiency of the normal case
- if (variants.size()==1)
- return Collections.singletonList(new Value<>(value, Binding.createFrom(variants.iterator().next())));
+ if (variants.size() == 1) {
+ return Collections.singletonList(new Value<>(substituteIfRelative(value, variants.iterator().next(), entries),
+ Binding.createFrom(variants.iterator().next())));
+ }
List<Value<VALUE>> values = new ArrayList<>(variants.size());
- for (DimensionBinding variant : variants)
- values.add(new Value<>(value, Binding.createFrom(variant)));
+ for (DimensionBinding variant : variants) {
+ values.add(new Value<>(substituteIfRelative(value, variant, entries), Binding.createFrom(variant)));
+ }
return values;
}
@@ -154,6 +168,42 @@ public class DimensionalValue<VALUE> {
return value;
}
+ @SuppressWarnings("unchecked")
+ private VALUE substituteIfRelative(VALUE value,
+ DimensionBinding variant,
+ Map<CompoundName, DimensionalValue.Builder<VALUE>> entries) {
+ if (value instanceof SubstituteString) {
+ SubstituteString substitute = (SubstituteString)value;
+ if (substitute.hasRelative()) {
+ List<SubstituteString.Component> resolvedComponents = new ArrayList<>(substitute.components().size());
+ for (SubstituteString.Component component : substitute.components()) {
+ if (component instanceof SubstituteString.RelativePropertyComponent) {
+ SubstituteString.RelativePropertyComponent relativeComponent = (SubstituteString.RelativePropertyComponent)component;
+ var substituteValues = lookupByLocalName(relativeComponent.fieldName(), entries);
+ String resolved = substituteValues == null ? "(null)" : substituteValues.valueFor(variant).toString();
+ resolvedComponents.add(new SubstituteString.StringComponent(resolved));
+ }
+ else {
+ resolvedComponents.add(component);
+ }
+ }
+ return (VALUE)new SubstituteString(resolvedComponents, substitute.stringValue());
+ }
+ }
+ return value;
+ }
+
+ private DimensionalValue.Builder<VALUE> lookupByLocalName(String localName,
+ Map<CompoundName, DimensionalValue.Builder<VALUE>> entries) {
+ for (var entry : entries.entrySet()) {
+ if (entry.getKey().last().equals(localName))
+ return entry.getValue();
+ }
+ return null;
+ }
+
}
+
}
+
}
diff --git a/container-search/src/test/java/com/yahoo/search/query/profile/test/QueryProfileSubstitutionTestCase.java b/container-search/src/test/java/com/yahoo/search/query/profile/test/QueryProfileSubstitutionTestCase.java
index ca1447b475a..1e51a4890be 100644
--- a/container-search/src/test/java/com/yahoo/search/query/profile/test/QueryProfileSubstitutionTestCase.java
+++ b/container-search/src/test/java/com/yahoo/search/query/profile/test/QueryProfileSubstitutionTestCase.java
@@ -17,13 +17,34 @@ import static org.junit.Assert.fail;
public class QueryProfileSubstitutionTestCase {
@Test
+ public void testSubstitutionOnly() {
+ QueryProfile p = new QueryProfile("test");
+ p.set("message","%{world}", null);
+ p.set("world", "world", null);
+ assertEquals("world", p.compile(null).get("message"));
+ }
+
+ @Test
public void testSingleSubstitution() {
QueryProfile p = new QueryProfile("test");
p.set("message","Hello %{world}!", null);
p.set("world", "world", null);
- assertEquals("Hello world!",p.compile(null).get("message"));
+ assertEquals("Hello world!", p.compile(null).get("message"));
- QueryProfile p2=new QueryProfile("test2");
+ QueryProfile p2 = new QueryProfile("test2");
+ p2.addInherited(p);
+ p2.set("world", "universe", null);
+ assertEquals("Hello universe!", p2.compile(null).get("message"));
+ }
+
+ @Test
+ public void testRelativeSubstitution() {
+ QueryProfile p = new QueryProfile("test");
+ p.set("message","Hello %{.world}!", null);
+ p.set("world", "world", null);
+ assertEquals("Hello world!", p.compile(null).get("message"));
+
+ QueryProfile p2 = new QueryProfile("test2");
p2.addInherited(p);
p2.set("world", "universe", null);
assertEquals("Hello universe!", p2.compile(null).get("message"));
@@ -31,12 +52,12 @@ public class QueryProfileSubstitutionTestCase {
@Test
public void testMultipleSubstitutions() {
- QueryProfile p=new QueryProfile("test");
+ QueryProfile p = new QueryProfile("test");
p.set("message","%{greeting} %{entity}%{exclamation}", null);
p.set("greeting","Hola", null);
p.set("entity","local group", null);
p.set("exclamation","?", null);
- assertEquals("Hola local group?",p.compile(null).get("message"));
+ assertEquals("Hola local group?", p.compile(null).get("message"));
QueryProfile p2 = new QueryProfile("test2");
p2.addInherited(p);
diff --git a/container-search/src/test/java/com/yahoo/search/query/profile/test/QueryProfileVariantsTestCase.java b/container-search/src/test/java/com/yahoo/search/query/profile/test/QueryProfileVariantsTestCase.java
index d9bf4a1db97..3da4558d67c 100644
--- a/container-search/src/test/java/com/yahoo/search/query/profile/test/QueryProfileVariantsTestCase.java
+++ b/container-search/src/test/java/com/yahoo/search/query/profile/test/QueryProfileVariantsTestCase.java
@@ -978,44 +978,47 @@ public class QueryProfileVariantsTestCase {
@Test
public void testQueryProfileReferencesWithSubstitution() {
- QueryProfile main=new QueryProfile("main");
+ QueryProfile main = new QueryProfile("main");
main.setDimensions(new String[] {"x1"});
- QueryProfile referencedMain=new QueryProfile("referencedMain");
+ QueryProfile referencedMain = new QueryProfile("referencedMain");
referencedMain.set("r1","%{prefix}mainReferenced-r1", null); // In both
referencedMain.set("r2","%{prefix}mainReferenced-r2", null); // Only in this
- QueryProfile referencedVariant=new QueryProfile("referencedVariant");
+ QueryProfile referencedVariant = new QueryProfile("referencedVariant");
referencedVariant.set("r1","%{prefix}variantReferenced-r1", null); // In both
referencedVariant.set("r3","%{prefix}variantReferenced-r3", null); // Only in this
+ referencedVariant.set("inthis", "local value", null);
+ referencedVariant.set("r4","This has %{.inthis}", null); // Relative
- main.set("a",referencedMain, null);
- main.set("a",referencedVariant,new String[] {"x1"}, null);
- main.set("prefix","mainPrefix:", null);
- main.set("prefix","variantPrefix:",new String[] {"x1"}, null);
+ main.set("a", referencedMain, null);
+ main.set("a", referencedVariant,new String[] {"x1"}, null);
+ main.set("prefix", "mainPrefix:", null);
+ main.set("prefix", "variantPrefix:", new String[] {"x1"}, null);
- Properties properties=new QueryProfileProperties(main.compile(null));
+ Properties properties = new QueryProfileProperties(main.compile(null));
// No context
- Map<String,Object> listed=properties.listProperties();
- assertEquals(3,listed.size());
- assertEquals("mainPrefix:mainReferenced-r1",listed.get("a.r1"));
- assertEquals("mainPrefix:mainReferenced-r2",listed.get("a.r2"));
+ Map<String,Object> listed = properties.listProperties();
+ assertEquals(3, listed.size());
+ assertEquals("mainPrefix:mainReferenced-r1", listed.get("a.r1"));
+ assertEquals("mainPrefix:mainReferenced-r2", listed.get("a.r2"));
// Context x=x1
- listed=properties.listProperties(toMap(main,new String[] {"x1"}));
- assertEquals(4,listed.size());
- assertEquals("variantPrefix:variantReferenced-r1",listed.get("a.r1"));
- assertEquals("variantPrefix:mainReferenced-r2",listed.get("a.r2"));
- assertEquals("variantPrefix:variantReferenced-r3",listed.get("a.r3"));
+ listed = properties.listProperties(toMap(main, new String[] {"x1"}));
+ assertEquals(6, listed.size());
+ assertEquals("variantPrefix:variantReferenced-r1", listed.get("a.r1"));
+ assertEquals("variantPrefix:mainReferenced-r2", listed.get("a.r2"));
+ assertEquals("variantPrefix:variantReferenced-r3", listed.get("a.r3"));
+ assertEquals("This has local value", listed.get("a.r4"));
}
@Test
public void testNewsCase1() {
- QueryProfile shortcuts=new QueryProfile("shortcuts");
- shortcuts.setDimensions(new String[] {"custid_1","custid_2","custid_3","custid_4","custid_5","custid_6"});
- shortcuts.set("testout","outside", null);
- shortcuts.set("test.out","dotoutside", null);
- shortcuts.set("testin","inside",new String[] {"yahoo","ca","sc"}, null);
- shortcuts.set("test.in","dotinside",new String[] {"yahoo","ca","sc"}, null);
+ QueryProfile shortcuts = new QueryProfile("shortcuts");
+ shortcuts.setDimensions(new String[] {"custid_1", "custid_2", "custid_3", "custid_4", "custid_5", "custid_6"});
+ shortcuts.set("testout", "outside", null);
+ shortcuts.set("test.out", "dotoutside", null);
+ shortcuts.set("testin", "inside", new String[] {"yahoo","ca","sc"}, null);
+ shortcuts.set("test.in", "dotinside", new String[] {"yahoo","ca","sc"}, null);
QueryProfile profile=new QueryProfile("default");
profile.setDimensions(new String[] {"custid_1","custid_2","custid_3","custid_4","custid_5","custid_6"});
@@ -1024,10 +1027,10 @@ public class QueryProfileVariantsTestCase {
profile.freeze();
Query query = new Query(HttpRequest.createTestRequest("?query=test&custid_1=yahoo&custid_2=ca&custid_3=sc", Method.GET), profile.compile(null));
- assertEquals("outside",query.properties().get("testout"));
- assertEquals("dotoutside",query.properties().get("test.out"));
- assertEquals("inside",query.properties().get("testin"));
- assertEquals("dotinside",query.properties().get("test.in"));
+ assertEquals("outside", query.properties().get("testout"));
+ assertEquals("dotoutside", query.properties().get("test.out"));
+ assertEquals("inside", query.properties().get("testin"));
+ assertEquals("dotinside", query.properties().get("test.in"));
}
@Test