// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
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;
import java.util.Map;
/**
* A string which contains one or more elements of the form %{name},
* where these occurrences are to be replaced by a query profile lookup on name.
*
* This objects does the analysis on creation and provides a (reasonably) fast method of
* performing the actual substitution (at lookup time).
*
* This is a value object. Lookups in this are thread safe.
*
* @author bratseth
*/
public class SubstituteString {
private final List components;
private final String stringValue;
private final boolean hasRelative;
/**
* Returns a new SubstituteString if the given string contains substitutions, null otherwise.
*/
public static SubstituteString create(String value) {
int lastEnd = 0;
int start = value.indexOf("%{");
if (start < 0) return null; // Shortcut
List components = new ArrayList<>();
while (start >= 0) {
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.contains("%{"))
throw new IllegalArgumentException("Unterminated value substitution '" + value.substring(start) + "'");
components.add(new StringComponent(value.substring(lastEnd, start)));
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);
}
public SubstituteString(List 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 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 Object substitute(Map context, Properties substitution) {
StringBuilder b = new StringBuilder();
for (Component component : components)
b.append(component.getValue(context, substitution));
return b.toString();
}
public List components() { return components; }
public String stringValue() { return stringValue; }
@Override
public int hashCode() {
return stringValue.hashCode();
}
@Override
public boolean equals(Object other) {
if (other == this) return true;
if ( ! (other instanceof SubstituteString)) return false;
return this.stringValue.equals(((SubstituteString)other).stringValue);
}
/** Returns this string in original (unsubstituted) form */
@Override
public String toString() {
return stringValue;
}
public abstract static class Component {
protected abstract String getValue(Map context, Properties substitution);
}
public final static class StringComponent extends Component {
private final String value;
public StringComponent(String value) {
this.value = value;
}
@Override
public String getValue(Map context, Properties substitution) {
return value;
}
@Override
public String toString() {
return value;
}
}
public final static class PropertyComponent extends Component {
private final String propertyName;
public PropertyComponent(String propertyName) {
this.propertyName = propertyName;
}
@Override
public String getValue(Map context, Properties substitution) {
Object value = substitution.get(propertyName, context, substitution);
if (value == null) return "";
return String.valueOf(value);
}
@Override
public String toString() {
return "%{" + propertyName + "}";
}
}
/**
* 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 context, Properties substitution) {
throw new IllegalStateException("Should be resolved during compilation");
}
public String fieldName() { return fieldName; }
@Override
public String toString() {
return "%{" + fieldName + "}";
}
}
}