// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.search.query.profile; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * An immutable, binding of a list of dimensions to dimension values * * @author bratseth */ public class DimensionBinding { /** The dimensions of this */ private List dimensions=null; /** The values matching those dimensions */ private DimensionValues values; /** The binding from those dimensions to values, and possibly other values */ private Map context; public static final DimensionBinding nullBinding = new DimensionBinding(Collections.unmodifiableList(Collections.emptyList()), DimensionValues.empty, null); public static final DimensionBinding invalidBinding = new DimensionBinding(Collections.unmodifiableList(Collections.emptyList()), DimensionValues.empty, null); /** Whether the value array contains only nulls */ private boolean containsAllNulls; /** Creates a binding from a variant and a context. Any of the arguments may be null. */ public static DimensionBinding createFrom(List dimensions, Map context) { if (dimensions==null || dimensions.size()==0) { if (context==null) return nullBinding; if (dimensions==null) return new DimensionBinding(null,DimensionValues.empty,context); // Null, but must preserve context } return new DimensionBinding(dimensions,extractDimensionValues(dimensions,context),context); } /** Creates a binding from a variant and a context. Any of the arguments may be null. */ public static DimensionBinding createFrom(List dimensions, DimensionValues dimensionValues) { if (dimensionValues==null || dimensionValues==DimensionValues.empty) return nullBinding; if (dimensions==null) return new DimensionBinding(null,dimensionValues,null); // Null, but preserve raw material for creating a context later (in createFor) return new DimensionBinding(dimensions,dimensionValues,null); } /** Returns a binding for a (possibly) new set of variants. Variants may be null, but not bindings */ public DimensionBinding createFor(List newDimensions) { if (newDimensions==null) return this; // Note: Not necessarily null - if no new variants then keep the existing binding // if (this.context==null && values.length==0) return nullBinding; // No data from which to create a non-null binding if (this.dimensions==newDimensions) return this; // Avoid creating a new object if the dimensions are the same Map context=this.context; if (context==null) context=this.values.asContext(this.dimensions !=null ? this.dimensions : newDimensions); return new DimensionBinding(newDimensions,extractDimensionValues(newDimensions,context),context); } /** * Creates a dimension binding. The dimensions list given should be unmodifiable. * The array will not be modified. The context is needed in order to convert this binding to another * given another set of variant dimensions. */ private DimensionBinding(List dimensions, DimensionValues values, Map context) { this.dimensions=dimensions; this.values=values; this.context = context; containsAllNulls=values.isEmpty(); } /** Returns a read-only list of the dimensions of this. This value is undefined if this isNull() */ public List getDimensions() { return dimensions; } /** Returns a context created from the dimensions and values of this */ public Map getContext() { if (context !=null) return context; context =values.asContext(dimensions); return context; } /** * Returns the values for the dimensions of this. This value is undefined if this isEmpty() * This array is always of the same length as the * length of the dimension list - missing elements are represented as nulls. * This is never null but may be empty. */ public DimensionValues getValues() { return values; } /** Returns true only if this binding is null (contains no values for its dimensions (if any) */ public boolean isNull() { return dimensions==null || containsAllNulls; } /** * Returns an array of the dimension values corresponding to the dimensions of this from the given context, * in the corresponding order. The array is always of the same length as the number of dimensions. * Dimensions which are not set in this context get a null value. */ private static DimensionValues extractDimensionValues(List dimensions,Map context) { String[] dimensionValues=new String[dimensions.size()]; if (context==null || context.size()==0) return DimensionValues.createFrom(dimensionValues); for (int i=0; i *
  • They contain a different value for the same key, or
  • *
  • They contain the same pair of dimensions in a different order
  • * * * @return the combined binding, or the special invalidBinding if these two bindings are incompatible */ public DimensionBinding combineWith(DimensionBinding binding) { List combinedDimensions = combineDimensions(getDimensions(), binding.getDimensions()); if (combinedDimensions == null) return invalidBinding; // not runtime, so assume we don't need to preserve values outside the dimensions Map combinedValues = combineValues(getContext(), binding.getContext()); if (combinedValues == null) return invalidBinding; return DimensionBinding.createFrom(combinedDimensions, combinedValues); } /** * Returns a combined list of dimensions from two separate lists, * or null if they are incompatible. * This is to combine two lists to one such that the partial order in both is preserved * (or return null if impossible). */ private List combineDimensions(List d1, List d2) { List combined = new ArrayList<>(); int d1Index = 0, d2Index=0; while (d1Index < d1.size() && d2Index < d2.size()) { if (d1.get(d1Index).equals(d2.get(d2Index))) { // agreement on next element combined.add(d1.get(d1Index)); d1Index++; d2Index++; } else if ( ! d2.contains(d1.get(d1Index))) { // next in d1 is independent from d2 combined.add(d1.get(d1Index++)); } else if ( ! d1.contains(d2.get(d2Index))) { // next in d2 is independent from d1 combined.add(d2.get(d2Index++)); } else { return null; // no independent and no agreement } } if (d1Index < d1.size()) combined.addAll(d1.subList(d1Index, d1.size())); else if (d2Index < d2.size()) combined.addAll(d2.subList(d2Index, d2.size())); return combined; } /** * Returns a combined map of dimension values from two separate maps, * or null if they are incompatible. */ private Map combineValues(Map m1, Map m2) { Map combinedValues = new HashMap<>(m1); for (Map.Entry m2Entry : m2.entrySet()) { if (m2Entry.getValue() == null) continue; String m1Value = m1.get(m2Entry.getKey()); if (m1Value != null && ! m1Value.equals(m2Entry.getValue())) return null; // conflicting values of a key combinedValues.put(m2Entry.getKey(), m2Entry.getValue()); } return combinedValues; } private boolean intersects(List l1, List l2) { for (String l1Item : l1) if (l2.contains(l1Item)) return true; return false; } /** * Returns true if this == invalidBinding */ public boolean isInvalid() { return this == invalidBinding; } @Override public String toString() { if (isInvalid()) return "Invalid DimensionBinding"; if (dimensions==null) return "DimensionBinding []"; StringBuilder b=new StringBuilder("DimensionBinding ["); for (int i=0; i