// 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.component.AbstractComponent; import com.yahoo.component.ComponentId; import com.yahoo.processing.request.CompoundName; import com.yahoo.processing.request.Properties; import com.yahoo.search.query.profile.QueryProfileProperties; import com.yahoo.search.query.profile.SubstituteString; import com.yahoo.search.query.profile.types.QueryProfileType; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A query profile in a state where it is optimized for fast lookups. * * @author bratseth */ public class CompiledQueryProfile extends AbstractComponent implements Cloneable { private static final Pattern namePattern = Pattern.compile("[$a-zA-Z_/][-$a-zA-Z0-9_/()]*"); private final CompiledQueryProfileRegistry registry; /** The type of this, or null if none */ private final QueryProfileType type; /** The values of this */ private final DimensionalMap entries; /** Keys which have a type in this */ private final DimensionalMap types; /** Keys which are (typed or untyped) references to other query profiles in this. Used as a set. */ private final DimensionalMap references; /** Values which are not overridable in this. Used as a set. */ private final DimensionalMap unoverridables; /** * Creates a new query profile from an id. */ public CompiledQueryProfile(ComponentId id, QueryProfileType type, DimensionalMap entries, DimensionalMap types, DimensionalMap references, DimensionalMap unoverridables, CompiledQueryProfileRegistry registry) { super(id); this.registry = registry; if (type != null) type.freeze(); this.type = type; this.entries = entries; this.types = types; this.references = references; this.unoverridables = unoverridables; if ( ! id.isAnonymous()) validateName(id.getName()); } // ----------------- Public API ------------------------------------------------------------------------------- /** Returns the registry this belongs to, or null if none (in which case runtime profile reference assignment won't work) */ public CompiledQueryProfileRegistry getRegistry() { return registry; } /** Returns the type of this or null if it has no type */ // TODO: Move into below public QueryProfileType getType() { return type; } /** * Returns whether or not the given field name can be overridden at runtime. * Attempts to override values which cannot be overridden will not fail but be ignored. * Default: true. * * @param name the name of the field to check * @param context the context in which to check, or null if none */ public final boolean isOverridable(CompoundName name, Map context) { return unoverridables.get(name, context) == null; } /** Returns the type of a given prefix reachable from this profile, or null if none */ public final QueryProfileType getType(CompoundName name, Map context) { return types.get(name, context); } /** Returns the types reachable from this, or an empty map (never null) if none */ public DimensionalMap getTypes() { return types; } /** Returns the references reachable from this, or an empty map (never null) if none */ public DimensionalMap getReferences() { return references; } /** * 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, Collections.emptyMap()); } public final Map listValues(String prefix) { return listValues(new CompoundName(prefix)); } /** * 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 = new HashMap<>(); for (Map.Entry> entry : entries.entrySet()) { if ( ! entry.getKey().hasPrefix(prefix)) continue; ValueWithSource valueWithSource = entry.getValue().get(context); if (valueWithSource == null) continue; Object value = valueWithSource.value(); if (value == null) continue; value = substitute(value, context, substitution); CompoundName suffixName = entry.getKey().rest(prefix.size()); values.put(suffixName.toString(), value); } return values; } public Map listValuesWithSources(CompoundName prefix, Map context, Properties substitution) { Map values = new HashMap<>(); for (Map.Entry> entry : entries.entrySet()) { if ( entry.getKey().size() <= prefix.size()) continue; if ( ! entry.getKey().hasPrefix(prefix)) continue; ValueWithSource valueWithSource = entry.getValue().get(context); if (valueWithSource == null) continue; if (valueWithSource.value() == null) continue; valueWithSource = valueWithSource.withValue(substitute(valueWithSource.value(), context, substitution)); CompoundName suffixName = entry.getKey().rest(prefix.size()); values.put(suffixName.toString(), valueWithSource); } return values; } public final Object get(String name) { return get(name, Collections.emptyMap()); } public final Object get(String name, Map context) { return get(name, context, new QueryProfileProperties(this)); } public final Object get(String name, Map context, Properties substitution) { return get(new CompoundName(name), context, substitution); } public final Object get(CompoundName name, Map context, Properties substitution) { ValueWithSource value = entries.get(name, context); if (value == null) return null; return substitute(value.value(), context, substitution); } private Object substitute(Object value, Map context, Properties substitution) { if (value == null) return value; if (substitution == null) return value; if (value.getClass() != SubstituteString.class) return value; return ((SubstituteString)value).substitute(context, substitution); } /** Throws IllegalArgumentException if the given string is not a valid query profile name */ private static void validateName(String name) { Matcher nameMatcher = namePattern.matcher(name); if ( ! nameMatcher.matches()) throw new IllegalArgumentException("Illegal name '" + name + "'"); } @Override public CompiledQueryProfile clone() { return this; // immutable } @Override public String toString() { return "query profile '" + getId() + "'" + (type!=null ? " of type '" + type.getId() + "'" : ""); } }