001package io.prometheus.metrics.model.snapshots;
002
003import java.util.ArrayList;
004import java.util.Arrays;
005import java.util.Collections;
006import java.util.Iterator;
007import java.util.List;
008import java.util.stream.Stream;
009
010import static io.prometheus.metrics.model.snapshots.PrometheusNaming.isValidLabelName;
011import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName;
012
013/**
014 * Immutable set of name/value pairs, sorted by name.
015 */
016public class Labels implements Comparable<Labels>, Iterable<Label> {
017
018    public static final Labels EMPTY;
019
020    static {
021        String[] names = new String[]{};
022        String[] values = new String[]{};
023        EMPTY = new Labels(names, names, values);
024    }
025
026    // prometheusNames is the same as names, but dots are replaced with underscores.
027    // Labels is sorted by prometheusNames.
028    // If names[i] does not contain a dot, prometheusNames[i] references the same String as names[i]
029    // so that we don't have unnecessary duplicates of strings.
030    // If none of the names contains a dot, then prometheusNames references the same array as names
031    // so that we don't have unnecessary duplicate arrays.
032    private final String[] prometheusNames;
033    private final String[] names;
034    private final String[] values;
035
036    private Labels(String[] names, String[] prometheusNames, String[] values) {
037        this.names = names;
038        this.prometheusNames = prometheusNames;
039        this.values = values;
040    }
041
042    public boolean isEmpty() {
043        return this == EMPTY || this.equals(EMPTY);
044    }
045
046    /**
047     * Create a new Labels instance.
048     * You can either create Labels with one of the static {@code Labels.of(...)} methods,
049     * or you can use the {@link Labels#builder()}.
050     *
051     * @param keyValuePairs as in {@code {name1, value1, name2, value2}}. Length must be even.
052     *                      {@link PrometheusNaming#isValidLabelName(String)} must be true for each name.
053     *                      Use {@link PrometheusNaming#sanitizeLabelName(String)} to convert arbitrary strings
054     *                      to valid label names. Label names must be unique (no duplicate label names).
055     */
056    public static Labels of(String... keyValuePairs) {
057        if (keyValuePairs.length % 2 != 0) {
058            throw new IllegalArgumentException("Key/value pairs must have an even length");
059        }
060        if (keyValuePairs.length == 0) {
061            return EMPTY;
062        }
063        String[] names = new String[keyValuePairs.length / 2];
064        String[] values = new String[keyValuePairs.length / 2];
065        for (int i = 0; 2 * i < keyValuePairs.length; i++) {
066            names[i] = keyValuePairs[2 * i];
067            values[i] = keyValuePairs[2 * i + 1];
068        }
069        String[] prometheusNames = makePrometheusNames(names);
070        sortAndValidate(names, prometheusNames, values);
071        return new Labels(names, prometheusNames, values);
072    }
073
074    // package private for testing
075    static String[] makePrometheusNames(String[] names) {
076        String[] prometheusNames = names;
077        for (int i=0; i<names.length; i++) {
078            if (names[i].contains(".")) {
079                if (prometheusNames == names) {
080                    prometheusNames = Arrays.copyOf(names, names.length);
081                }
082                prometheusNames[i] = PrometheusNaming.prometheusName(names[i]);
083            }
084        }
085        return prometheusNames;
086    }
087
088    /**
089     * Create a new Labels instance.
090     * You can either create Labels with one of the static {@code Labels.of(...)} methods,
091     * or you can use the {@link Labels#builder()}.
092     *
093     * @param names  label names. {@link PrometheusNaming#isValidLabelName(String)} must be true for each name.
094     *               Use {@link PrometheusNaming#sanitizeLabelName(String)} to convert arbitrary strings
095     *               to valid label names. Label names must be unique (no duplicate label names).
096     * @param values label values. {@code names.size()} must be equal to {@code values.size()}.
097     */
098    public static Labels of(List<String> names, List<String> values) {
099        if (names.size() != values.size()) {
100            throw new IllegalArgumentException("Names and values must have the same size.");
101        }
102        if (names.size() == 0) {
103            return EMPTY;
104        }
105        String[] namesCopy = names.toArray(new String[0]);
106        String[] valuesCopy = values.toArray(new String[0]);
107        String[] prometheusNames = makePrometheusNames(namesCopy);
108        sortAndValidate(namesCopy, prometheusNames, valuesCopy);
109        return new Labels(namesCopy, prometheusNames, valuesCopy);
110    }
111
112    /**
113     * Create a new Labels instance.
114     * You can either create Labels with one of the static {@code Labels.of(...)} methods,
115     * or you can use the {@link Labels#builder()}.
116     *
117     * @param names  label names. {@link PrometheusNaming#isValidLabelName(String)} must be true for each name.
118     *               Use {@link PrometheusNaming#sanitizeLabelName(String)} to convert arbitrary strings
119     *               to valid label names. Label names must be unique (no duplicate label names).
120     * @param values label values. {@code names.length} must be equal to {@code values.length}.
121     */
122    public static Labels of(String[] names, String[] values) {
123        if (names.length != values.length) {
124            throw new IllegalArgumentException("Names and values must have the same length.");
125        }
126        if (names.length == 0) {
127            return EMPTY;
128        }
129        String[] namesCopy = Arrays.copyOf(names, names.length);
130        String[] valuesCopy = Arrays.copyOf(values, values.length);
131        String[] prometheusNames = makePrometheusNames(namesCopy);
132        sortAndValidate(namesCopy, prometheusNames, valuesCopy);
133        return new Labels(namesCopy, prometheusNames, valuesCopy);
134    }
135
136    /**
137     * Test if these labels contain a specific label name.
138     * <p>
139     * Dots are treated as underscores, so {@code contains("my.label")} and {@code contains("my_label")} are the same.
140     */
141    public boolean contains(String labelName) {
142        return get(labelName) != null;
143    }
144
145    /**
146     * Get the label value for a given label name.
147     * <p>
148     * Returns {@code null} if the {@code labelName} is not found.
149     * <p>
150     * Dots are treated as underscores, so {@code get("my.label")} and {@code get("my_label")} are the same.
151     */
152    public String get(String labelName) {
153        labelName = prometheusName(labelName);
154        for (int i=0; i<prometheusNames.length; i++) {
155            if (prometheusNames[i].equals(labelName)) {
156                return values[i];
157            }
158        }
159        return null;
160    }
161
162    private static void sortAndValidate(String[] names, String[] prometheusNames, String[] values) {
163        sort(names, prometheusNames, values);
164        validateNames(names, prometheusNames);
165    }
166
167    private static void validateNames(String[] names, String[] prometheusNames) {
168        for (int i = 0; i < names.length; i++) {
169            if (!isValidLabelName(names[i])) {
170                throw new IllegalArgumentException("'" + names[i] + "' is an illegal label name");
171            }
172            // The arrays are sorted, so duplicates are next to each other
173            if (i > 0 && prometheusNames[i - 1].equals(prometheusNames[i])) {
174                throw new IllegalArgumentException(names[i] + ": duplicate label name");
175            }
176        }
177    }
178
179    private static void sort(String[] names, String[] prometheusNames, String[] values) {
180        // bubblesort
181        int n = prometheusNames.length;
182        for (int i = 0; i < n - 1; i++) {
183            for (int j = 0; j < n - i - 1; j++) {
184                if (prometheusNames[j].compareTo(prometheusNames[j + 1]) > 0) {
185                    swap(j, j + 1, names, prometheusNames, values);
186                }
187            }
188        }
189    }
190
191    private static void swap(int i, int j, String[] names, String[] prometheusNames, String[] values) {
192        String tmp = names[j];
193        names[j] = names[i];
194        names[i] = tmp;
195        tmp = values[j];
196        values[j] = values[i];
197        values[i] = tmp;
198        if (prometheusNames != names) {
199            tmp = prometheusNames[j];
200            prometheusNames[j] = prometheusNames[i];
201            prometheusNames[i] = tmp;
202        }
203    }
204
205    @Override
206    public Iterator<Label> iterator() {
207        return asList().iterator();
208    }
209
210    public Stream<Label> stream() {
211        return asList().stream();
212    }
213
214    public int size() {
215        return names.length;
216    }
217
218    public String getName(int i) {
219        return names[i];
220    }
221
222    /**
223     * Like {@link #getName(int)}, but dots are replaced with underscores.
224     * <p>
225     * This is used by Prometheus exposition formats.
226     */
227    public String getPrometheusName(int i) {
228        return prometheusNames[i];
229    }
230
231    public String getValue(int i) {
232        return values[i];
233    }
234
235    /**
236     * Create a new Labels instance containing the labels of this and the labels of other.
237     * This and other must not contain the same label name.
238     */
239    public Labels merge(Labels other) {
240        if (this.isEmpty()) {
241            return other;
242        }
243        if (other.isEmpty()) {
244            return this;
245        }
246        String[] names = new String[this.names.length + other.names.length];
247        String[] prometheusNames = names;
248        if (this.names != this.prometheusNames || other.names != other.prometheusNames) {
249            prometheusNames = new String[names.length];
250        }
251        String[] values = new String[names.length];
252        int thisPos = 0;
253        int otherPos = 0;
254        while (thisPos + otherPos < names.length) {
255            if (thisPos >= this.names.length) {
256                names[thisPos + otherPos] = other.names[otherPos];
257                values[thisPos + otherPos] = other.values[otherPos];
258                if (prometheusNames != names) {
259                    prometheusNames[thisPos + otherPos] = other.prometheusNames[otherPos];
260                }
261                otherPos++;
262            } else if (otherPos >= other.names.length) {
263                names[thisPos + otherPos] = this.names[thisPos];
264                values[thisPos + otherPos] = this.values[thisPos];
265                if (prometheusNames != names) {
266                    prometheusNames[thisPos + otherPos] = this.prometheusNames[thisPos];
267                }
268                thisPos++;
269            } else if (this.prometheusNames[thisPos].compareTo(other.prometheusNames[otherPos]) < 0) {
270                names[thisPos + otherPos] = this.names[thisPos];
271                values[thisPos + otherPos] = this.values[thisPos];
272                if (prometheusNames != names) {
273                    prometheusNames[thisPos + otherPos] = this.prometheusNames[thisPos];
274                }
275                thisPos++;
276            } else if (this.prometheusNames[thisPos].compareTo(other.prometheusNames[otherPos]) > 0) {
277                names[thisPos + otherPos] = other.names[otherPos];
278                values[thisPos + otherPos] = other.values[otherPos];
279                if (prometheusNames != names) {
280                    prometheusNames[thisPos + otherPos] = other.prometheusNames[otherPos];
281                }
282                otherPos++;
283            } else {
284                throw new IllegalArgumentException("Duplicate label name: '" + this.names[thisPos] + "'.");
285            }
286        }
287        return new Labels(names, prometheusNames, values);
288    }
289
290    /**
291     * Create a new Labels instance containing the labels of this and the label passed as name and value.
292     * The label name must not already be contained in this Labels instance.
293     */
294    public Labels add(String name, String value) {
295        return merge(Labels.of(name, value));
296    }
297
298    /**
299     * Create a new Labels instance containing the labels of this and the labels passed as names and values.
300     * The new label names must not already be contained in this Labels instance.
301     */
302    public Labels merge(String[] names, String[] values) {
303        if (this.equals(EMPTY)) {
304            return Labels.of(names, values);
305        }
306        String[] mergedNames = new String[this.names.length + names.length];
307        String[] mergedValues = new String[this.values.length + values.length];
308        System.arraycopy(this.names, 0, mergedNames, 0, this.names.length);
309        System.arraycopy(this.values, 0, mergedValues, 0, this.values.length);
310        System.arraycopy(names, 0, mergedNames, this.names.length, names.length);
311        System.arraycopy(values, 0, mergedValues, this.values.length, values.length);
312        String[] prometheusNames = makePrometheusNames(mergedNames);
313        sortAndValidate(mergedNames, prometheusNames, mergedValues);
314        return new Labels(mergedNames, prometheusNames, mergedValues);
315    }
316
317    public boolean hasSameNames(Labels other) {
318        return Arrays.equals(prometheusNames, other.prometheusNames);
319    }
320
321    public boolean hasSameValues(Labels other) {
322        return Arrays.equals(values, other.values);
323    }
324
325    @Override
326    public int compareTo(Labels other) {
327        int result = compare(prometheusNames, other.prometheusNames);
328        if (result != 0) {
329            return result;
330        }
331        return compare(values, other.values);
332    }
333
334    // Looks like Java doesn't have a compareTo() method for arrays.
335    private int compare(String[] array1, String[] array2) {
336        int result;
337        for (int i = 0; i < array1.length; i++) {
338            if (array2.length <= i) {
339                return 1;
340            }
341            result = array1[i].compareTo(array2[i]);
342            if (result != 0) {
343                return result;
344            }
345        }
346        if (array2.length > array1.length) {
347            return -1;
348        }
349        return 0;
350    }
351
352    private List<Label> asList() {
353        List<Label> result = new ArrayList<>(names.length);
354        for (int i = 0; i < names.length; i++) {
355            result.add(new Label(names[i], values[i]));
356        }
357        return Collections.unmodifiableList(result);
358    }
359
360    /**
361     * This must not be used in Prometheus exposition formats because names may contain dots.
362     * <p>
363     * However, for debugging it's better to show the original names rather than the Prometheus names.
364     */
365    @Override
366    public String toString() {
367        StringBuilder b = new StringBuilder();
368        b.append("{");
369        for (int i = 0; i < names.length; i++) {
370            if (i > 0) {
371                b.append(",");
372            }
373            b.append(names[i]);
374            b.append("=\"");
375            appendEscapedLabelValue(b, values[i]);
376            b.append("\"");
377        }
378        b.append("}");
379        return b.toString();
380    }
381
382    private void appendEscapedLabelValue(StringBuilder b, String value) {
383        for (int i = 0; i < value.length(); i++) {
384            char c = value.charAt(i);
385            switch (c) {
386                case '\\':
387                    b.append("\\\\");
388                    break;
389                case '\"':
390                    b.append("\\\"");
391                    break;
392                case '\n':
393                    b.append("\\n");
394                    break;
395                default:
396                    b.append(c);
397            }
398        }
399    }
400
401    @Override
402    public boolean equals(Object o) {
403        if (this == o) return true;
404        if (o == null || getClass() != o.getClass()) return false;
405        Labels labels = (Labels) o;
406        return labels.hasSameNames(this) && labels.hasSameValues(this);
407    }
408
409    @Override
410    public int hashCode() {
411        int result = Arrays.hashCode(prometheusNames);
412        result = 31 * result + Arrays.hashCode(values);
413        return result;
414    }
415
416    public static Builder builder() {
417        return new Builder();
418    }
419
420    public static class Builder {
421        private final List<String> names = new ArrayList<>();
422        private final List<String> values = new ArrayList<>();
423
424        private Builder() {
425        }
426
427        /**
428         * Add a label. Call multiple times to add multiple labels.
429         */
430        public Builder label(String name, String value) {
431            names.add(name);
432            values.add(value);
433            return this;
434        }
435
436        public Labels build() {
437            return Labels.of(names, values);
438        }
439    }
440}