aboutsummaryrefslogtreecommitdiffstats
path: root/container-search/src/main/java/com/yahoo/search/query/profile/DimensionBinding.java
blob: bfd62032d82b3380bafe1517efd25aa772990fa3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
// 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 java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * An immutable, binding of a list of dimensions to dimension values
 *
 * @author bratseth
 */
public class DimensionBinding {

    /** The dimensions of this */
    private List<String> dimensions;

    /** The values matching those dimensions */
    private DimensionValues values;

    /** The binding from those dimensions to values, and possibly other values */
    private Map<String, String> context;

    public static final DimensionBinding nullBinding =
        new DimensionBinding(List.of(), DimensionValues.empty, null);

    public static final DimensionBinding invalidBinding =
        new DimensionBinding(List.of(), DimensionValues.empty, null);

    /** Whether the value array contains only nulls */
    private final boolean containsAllNulls;

    // NOTE: Map must be ordered
    public static DimensionBinding createFrom(Map<String, String> values) {
        return createFrom(new ArrayList<>(values.keySet()), values);
    }

    /** Creates a binding from a variant and a context. Any of the arguments may be null. */
    // NOTE: Map must be ordered
    public static DimensionBinding createFrom(List<String> dimensions, Map<String, String> 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<String> dimensions, DimensionValues dimensionValues) {
        if (dimensionValues == null || dimensionValues == DimensionValues.empty) return nullBinding;

        // If null, preserve raw material for creating a context later (in createFor)
        if (dimensions == null) return new DimensionBinding(null, dimensionValues, null);

        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<String> newDimensions) {
        if (newDimensions == null) return this; // Note: Not necessarily null - if no new variants then keep the existing binding
        if (this.dimensions == newDimensions) return this; // Avoid creating a new object if the dimensions are the same

        Map<String,String> 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<String> dimensions, DimensionValues values, Map<String, String> 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<String> getDimensions() { return dimensions; }

    /** Returns a context created from the dimensions and values of this */
    public Map<String, String> 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<String> dimensions, Map<String,String> context) {
        String[] dimensionValues = new String[dimensions.size()];
        if (context == null || context.size() == 0) return DimensionValues.createFrom(dimensionValues);
        for (int i = 0; i < dimensions.size(); i++)
            dimensionValues[i] = context.get(dimensions.get(i));
        return DimensionValues.createFrom(dimensionValues);
    }

    /**
     * Combines this binding with another if compatible.
     * Two bindings are incompatible if
     * <ul>
     *     <li>They contain a different value for the same key, or</li>
     *     <li>They contain the same pair of dimensions in a different order</li>
     * </ul>
     *
     * @return the combined binding, or the special invalidBinding if these two bindings are incompatible
     */
    public DimensionBinding combineWith(DimensionBinding other) {
        List<String> d1 = getDimensions();
        List<String> d2 = other.getDimensions();
        DimensionValues v1 = getValues();
        DimensionValues v2 = other.getValues();
        List<String> dimensions = new ArrayList<>();
        List<String> values = new ArrayList<>();
        int i1 = 0, i2 = 0;
        while (i1 < d1.size() && i2 < d2.size()) {
            if (d1.get(i1).equals(d2.get(i2))) { // agreement on next dimension
                String s1 = v1.get(i1), s2 = v2.get(i2);
                if (s1 == null)
                    values.add(s2);
                else if (s2 == null || s1.equals(s2))
                    values.add(s1);
                else
                    return invalidBinding; // disagreement on next value

                dimensions.add(d1.get(i1));
                i1++;
                i2++;
            }
            else if ( ! d2.contains(d1.get(i1))) { // next dimension in d1 is independent from d2
                dimensions.add(d1.get(i1));
                values.add(v1.get(i1));
                i1++;
            }
            else if ( ! d1.contains(d2.get(i2))) { // next dimension in d2 is independent from d1
                dimensions.add(d2.get(i2));
                values.add(v2.get(i2));
                i2++;
            }
            else {
                return invalidBinding; // not independent and no agreement
            }
        }
        while (i1 < d1.size()) {
            dimensions.add(d1.get(i1));
            values.add(v1.get(i1));
            i1++;
        }
        while (i2 < d2.size()) {
            dimensions.add(d2.get(i2));
            values.add(v2.get(i2));
            i2++;
        }

        return DimensionBinding.createFrom(dimensions, DimensionValues.createFrom(values.toArray(new String[0])));
    }

    /** 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 < dimensions.size(); i++) {
            b.append(dimensions.get(i)).append("=").append(values.get(i));
            if (i < dimensions.size()-1)
                b.append(", ");
        }
        b.append("]");
        return b.toString();
    }

    /** Two bindings are equal if they contain the same dimensions and the same non-null values */
    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (! (o instanceof DimensionBinding)) return false;
        DimensionBinding other = (DimensionBinding)o;
        if ( ! this.dimensions.equals(other.dimensions)) return false;
        if ( ! this.values.equals(other.values)) return false;
        return true;
    }

    @Override
    public int hashCode() {
        return Objects.hash(dimensions, values);
    }

}