aboutsummaryrefslogtreecommitdiffstats
path: root/vespajlib/src/main/java/com/yahoo/collections/ListMap.java
blob: 32b1ced15d718e8f911a97df24b9141a715d7ff3 (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
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.collections;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;

/**
 * A map holding multiple items at each key (using ArrayList and HashMap).
 *
 * @author  bratseth
 */
public class ListMap<K, V> {

    private boolean frozen = false;

    private Map<K, List<V>> map;

    public ListMap() {
        this(HashMap.class);
    }

    /** Copy constructor. This will not be frozen even if the argument map is */
    public ListMap(ListMap<K, V> original) {
        map = new HashMap<>();
        original.map.forEach((k, v) -> this.map.put(k, new ArrayList<>(v)));
    }

    @SuppressWarnings("unchecked")
    public ListMap(@SuppressWarnings("rawtypes") Class<? extends Map> implementation) {
        try {
            this.map = implementation.getDeclaredConstructor().newInstance();
        } catch (InvocationTargetException e) {
            // For backwards compatibility from when this method used implementation.newInstance()
            throw new IllegalArgumentException(e.getCause());
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

    /** Puts an element into this. Multiple elements at the same position are added to the list at this key */
    public void put(K key, V value) {
        List<V> list = map.computeIfAbsent(key, k -> new ArrayList<>());
        list.add(value);
    }

    /** Put a key without adding a new value, such that there is an empty list of values if no values are already added */
    public void put(K key) {
        map.computeIfAbsent(key, k -> new ArrayList<>());
    }

    /** Put this map in the state where it has just the given value of the given key */
    public void replace(K key, V value) {
        List<V> list = map.get(key);
        if (list == null) {
            put(key);
        }
        else {
            list.clear();
            list.add(value);
        }
    }

    public void removeAll(K key) {
        map.remove(key);
    }

    public boolean removeValue(K key, V value) {
        List<V> list = map.get(key);
        if (list != null)
            return list.remove(value);
        else
            return false;
    }

    /**
     * Removes the value at the given index.
     *
     * @return the removed value
     * @throws IndexOutOfBoundsException if there is no value at the given index for this key
     */
    public V removeValue(K key, int index) {
        List<V> list = map.get(key);
        if (list != null)
            return list.remove(index);
        else
            throw new IndexOutOfBoundsException("The list at '" + key + "' is empty");
    }

    /**
     * Returns the List containing the elements with this key, or an empty list
     * if there are no elements for this key.
     * The returned list can be modified to add and remove values if the value exists.
     */
    public List<V> get(K key) {
        List<V> list = map.get(key);
        if (list == null) return List.of();;
        return list;
    }

    /** The same as get */
    public List<V> getList(K key) {
        return get(key);
    }

    /** Returns the entries of this. Entries will be unmodifiable if this is frozen. */
    public Set<Map.Entry<K,List<V>>> entrySet() { return map.entrySet(); }

    /** Returns the keys of this */
    public Set<K> keySet() { return map.keySet(); }

    /** Returns the list values of this */
    public Collection<List<V>> values() { return map.values(); }

    /**
     * Irreversibly prevent changes to the content of this.
     * If this is already frozen, this method does nothing.
     */
    public void freeze() {
        if (frozen) return;
        frozen = true;

        for (Map.Entry<K,List<V>> entry : map.entrySet())
            entry.setValue(List.copyOf(entry.getValue()));
        this.map = Map.copyOf(this.map);
    }

    /** Returns whether this allows changes */
    public boolean isFrozen() { return frozen; }

    @Override
    public String toString() {
        return "ListMap{" +
                "frozen=" + frozen +
                ", map=" + map +
                '}';
    }

    /** Returns the number of keys in this map */
    public int size() { return map.size(); }

    public void forEach(BiConsumer<K, List<V>> action) { map.forEach(action); }

}