Add lazily-instantiated maps
[mdsal.git] / binding / mdsal-binding-dom-codec / src / main / java / org / opendaylight / mdsal / binding / dom / codec / impl / LazyBindingMap.java
1 /*
2  * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.mdsal.binding.dom.codec.impl;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.annotations.VisibleForTesting;
13 import com.google.common.collect.ImmutableMap;
14 import com.google.common.collect.ImmutableMap.Builder;
15 import java.lang.invoke.MethodHandles;
16 import java.lang.invoke.VarHandle;
17 import java.util.AbstractMap;
18 import java.util.Collection;
19 import java.util.Map;
20 import java.util.Optional;
21 import java.util.Set;
22 import org.eclipse.jdt.annotation.NonNull;
23 import org.opendaylight.mdsal.binding.dom.codec.impl.KeyedListNodeCodecContext.Unordered;
24 import org.opendaylight.yangtools.concepts.Immutable;
25 import org.opendaylight.yangtools.yang.binding.DataObject;
26 import org.opendaylight.yangtools.yang.binding.Identifiable;
27 import org.opendaylight.yangtools.yang.binding.Identifier;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
29 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
30 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33
34 /**
35  * Lazily-populated Map of binding DTOs. This implementation acts as the main entry point, so that we can decide on the
36  * translation strategy we are going to use. We make that decision based on the first method that touches the mappings
37  * (or materializes a view).
38  *
39  * @param <K> key type
40  * @param <V> value type
41  */
42 final class LazyBindingMap<K extends Identifier<V>, V extends DataObject & Identifiable<K>>
43         extends AbstractMap<K, V> implements Immutable {
44     private static final Logger LOG = LoggerFactory.getLogger(LazyBindingMap.class);
45     private static final String LAZY_CUTOFF_PROPERTY =
46             "org.opendaylight.mdsal.binding.dom.codec.impl.LazyBindingMap.max-eager-elements";
47     private static final int DEFAULT_LAZY_CUTOFF = 1;
48     @VisibleForTesting
49     static final int LAZY_CUTOFF;
50
51     private static final VarHandle STATE;
52
53     static {
54         try {
55             STATE = MethodHandles.lookup().findVarHandle(LazyBindingMap.class, "state",
56                 State.class);
57         } catch (NoSuchFieldException | IllegalAccessException e) {
58             throw new ExceptionInInitializerError(e);
59         }
60
61         final int value = Integer.getInteger(LAZY_CUTOFF_PROPERTY, DEFAULT_LAZY_CUTOFF);
62         if (value < 0) {
63             LOG.info("Lazy population of maps disabled");
64             LAZY_CUTOFF = Integer.MAX_VALUE;
65         } else {
66             LOG.info("Using lazy population for maps larger than {} element(s)", value);
67             LAZY_CUTOFF = value;
68         }
69     }
70
71     private final @NonNull Unordered<K, V> codec;
72     private final @NonNull MapNode mapNode;
73
74     // Used via VarHandle above
75     @SuppressWarnings("unused")
76     private volatile State<K, V> state;
77
78     private LazyBindingMap(final Unordered<K, V> codec, final MapNode mapNode) {
79         this.codec = requireNonNull(codec);
80         this.mapNode = requireNonNull(mapNode);
81     }
82
83     static <K extends Identifier<V>, V extends DataObject & Identifiable<K>> @NonNull Map<K, V> create(
84             final Unordered<K, V> codec, final MapNode mapNode, final int size) {
85         if (size == 1) {
86             // Do not bother with lazy instantiation in case of a singleton
87             final V entry = codec.createBindingProxy(mapNode.getValue().iterator().next());
88             return Map.of(entry.key(), entry);
89         }
90         return size > LAZY_CUTOFF ? new LazyBindingMap<>(codec, mapNode) : eagerMap(codec, mapNode, size);
91     }
92
93     private static <K extends Identifier<V>, V extends DataObject & Identifiable<K>> @NonNull Map<K, V> eagerMap(
94             final Unordered<K, V> codec, final MapNode mapNode, final int size) {
95         final Builder<K, V> builder = ImmutableMap.builderWithExpectedSize(size);
96         for (MapEntryNode node : mapNode.getValue()) {
97             final V entry = codec.createBindingProxy(node);
98             builder.put(entry.key(), entry);
99         }
100         return builder.build();
101     }
102
103     @Override
104     public int size() {
105         return mapNode.size();
106     }
107
108     @Override
109     public V remove(final Object key) {
110         throw uoe();
111     }
112
113     @Override
114     @SuppressWarnings("checkstyle:parameterName")
115     public void putAll(final Map<? extends K, ? extends V> m) {
116         throw uoe();
117     }
118
119     @Override
120     public void clear() {
121         throw uoe();
122     }
123
124     @Override
125     public boolean containsKey(final Object key) {
126         return lookupState().containsKey(requireNonNull(key));
127     }
128
129     @Override
130     public boolean containsValue(final Object value) {
131         /*
132          * This implementation relies on the relationship specified by Identifiable/Identifier and its use in binding
133          * objects. The key is a wrapper object composed of a subset (or all) properties in the value, i.e. we have
134          * a partial index.
135          *
136          * Instead of performing an O(N) search, we extract the key from the value, look the for the corresponding
137          * mapping. If we find a mapping we check if the mapped value equals the the value being looked up.
138          *
139          * Note we prefer throwing ClassCastException/NullPointerException when presented with null or an object which
140          * cannot possibly be contained in this map.
141          */
142         final V cast = codec.getBindingClass().cast(requireNonNull(value));
143         final V found = get(cast.key());
144         return found != null && cast.equals(found);
145     }
146
147     @Override
148     public V get(final Object key) {
149         return lookupState().get(requireNonNull(key));
150     }
151
152     @Override
153     public Set<K> keySet() {
154         return iterState().keySet();
155     }
156
157     @Override
158     public Collection<V> values() {
159         return iterState().values();
160     }
161
162     @Override
163     public Set<Entry<K, V>> entrySet() {
164         return iterState().entrySet();
165     }
166
167     @NonNull V createValue(final MapEntryNode node) {
168         return codec.createBindingProxy(node);
169     }
170
171     Optional<V> lookupValue(final @NonNull Object key) {
172         final NodeIdentifierWithPredicates childId = codec.serialize((Identifier<?>) key);
173         return mapNode.getChild(childId).map(codec::createBindingProxy);
174     }
175
176     @NonNull MapNode mapNode() {
177         return mapNode;
178     }
179
180     private @NonNull State<K, V> lookupState() {
181         final State<K, V> local;
182         return (local = (State<K, V>) STATE.getAcquire(this)) != null ? local : loadLookup();
183     }
184
185     private @NonNull State<K, V> iterState() {
186         final State<K, V> local;
187         return (local = (State<K, V>) STATE.getAcquire(this)) != null ? local : loadIter();
188     }
189
190     @SuppressWarnings("unchecked")
191     private @NonNull State<K, V> loadLookup() {
192         final State<K, V> ret = new LazyBindingMapLookupState<>(this);
193         final Object witness;
194         return (witness = STATE.compareAndExchangeRelease(this, null, ret)) == null ? ret : (State<K, V>) witness;
195     }
196
197     @SuppressWarnings("unchecked")
198     private @NonNull State<K, V> loadIter() {
199         final State<K, V> ret = new LazyBindingMapIterState<>(this);
200         final Object witness;
201         return (witness = STATE.compareAndExchangeRelease(this, null, ret)) == null ? ret : (State<K, V>) witness;
202     }
203
204     private static UnsupportedOperationException uoe() {
205         return new UnsupportedOperationException("Modification is not supported");
206     }
207
208     abstract static class State<K extends Identifier<V>, V extends DataObject & Identifiable<K>> {
209         abstract boolean containsKey(@NonNull Object key);
210
211         abstract V get(@NonNull Object key);
212
213         abstract @NonNull Set<K> keySet();
214
215         abstract @NonNull Collection<V> values();
216
217         abstract @NonNull Set<Entry<K, V>> entrySet();
218     }
219 }