Add lazily-instantiated maps
[mdsal.git] / binding / mdsal-binding-dom-codec / src / main / java / org / opendaylight / mdsal / binding / dom / codec / impl / CodecDataObject.java
1 /*
2  * Copyright (c) 2019 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 com.google.common.base.Verify.verify;
11 import static java.util.Objects.requireNonNull;
12
13 import com.google.common.base.MoreObjects;
14 import com.google.common.base.MoreObjects.ToStringHelper;
15 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
16 import java.lang.invoke.MethodHandles;
17 import java.lang.invoke.VarHandle;
18 import java.util.Optional;
19 import org.eclipse.jdt.annotation.NonNull;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.opendaylight.yangtools.yang.binding.DataObject;
22 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
23 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
24 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
25
26 /**
27  * A base class for {@link DataObject}s backed by {@link DataObjectCodecContext}. While this class is public, it not
28  * part of API surface and is an implementation detail. The only reason for it being public is that it needs to be
29  * accessible by code generated at runtime.
30  *
31  * @param <T> DataObject type
32  */
33 public abstract class CodecDataObject<T extends DataObject> implements DataObject {
34     // An object representing a null value in a member field.
35     private static final @NonNull Object NULL_VALUE = new Object();
36
37     private static final VarHandle CACHED_HASH_CODE;
38
39     static {
40         try {
41             CACHED_HASH_CODE = MethodHandles.lookup().findVarHandle(CodecDataObject.class, "cachedHashcode",
42                 Integer.class);
43         } catch (NoSuchFieldException | IllegalAccessException e) {
44             throw new ExceptionInInitializerError(e);
45         }
46     }
47
48     private final @NonNull DataObjectCodecContext<T, ?> context;
49     @SuppressWarnings("rawtypes")
50     private final @NonNull NormalizedNodeContainer data;
51
52     // Accessed via a VarHandle
53     @SuppressWarnings("unused")
54     // FIXME: consider using a primitive int-based cache (with 0 being uninit)
55     private volatile Integer cachedHashcode;
56
57     protected CodecDataObject(final DataObjectCodecContext<T, ?> context, final NormalizedNodeContainer<?, ?, ?> data) {
58         this.data = requireNonNull(data, "Data must not be null");
59         this.context = requireNonNull(context, "Context must not be null");
60     }
61
62     @Override
63     public final int hashCode() {
64         final Integer cached = (Integer) CACHED_HASH_CODE.getAcquire(this);
65         return cached != null ? cached : loadHashCode();
66     }
67
68     @Override
69     @SuppressFBWarnings(value = "EQ_UNUSUAL", justification = "State is examined indirectly enough to confuse SpotBugs")
70     public final boolean equals(final Object obj) {
71         if (obj == this) {
72             return true;
73         }
74         final Class<? extends DataObject> iface = implementedInterface();
75         if (!iface.isInstance(obj)) {
76             return false;
77         }
78         @SuppressWarnings("unchecked")
79         final T other = (T) iface.cast(obj);
80         // Note: we do not want to compare NormalizedNode data here, as we may be looking at different instantiations
81         //       of the same grouping -- in which case normalized node will not compare as equal.
82         return codecAugmentedEquals(other);
83     }
84
85     @Override
86     public final String toString() {
87         return codecAugmentedFillToString(MoreObjects.toStringHelper(implementedInterface()).omitNullValues())
88                 .toString();
89     }
90
91     protected final Object codecMember(final VarHandle handle, final String localName) {
92         final Object cached = handle.getAcquire(this);
93         return cached != null ? unmaskNull(cached) : loadMember(handle, context.getLeafChild(localName));
94     }
95
96     protected final Object codecMember(final VarHandle handle, final Class<? extends DataObject> bindingClass) {
97         final Object cached = handle.getAcquire(this);
98         return cached != null ? unmaskNull(cached) : loadMember(handle, context.streamChild(bindingClass));
99     }
100
101     protected final Object codecMember(final VarHandle handle, final NodeContextSupplier supplier) {
102         final Object cached = handle.getAcquire(this);
103         return cached != null ? unmaskNull(cached) : loadMember(handle, supplier.get());
104     }
105
106     protected final @NonNull Object codecKey(final VarHandle handle) {
107         final Object cached = handle.getAcquire(this);
108         return cached != null ? cached : loadKey(handle);
109     }
110
111     protected abstract int codecHashCode();
112
113     protected abstract boolean codecEquals(T other);
114
115     protected abstract ToStringHelper codecFillToString(ToStringHelper helper);
116
117     final @NonNull DataObjectCodecContext<T, ?> codecContext() {
118         return context;
119     }
120
121     @SuppressWarnings("rawtypes")
122     final @NonNull NormalizedNodeContainer codecData() {
123         return data;
124     }
125
126     // Non-final to allow specialization in AugmentableCodecDataObject
127     int codecAugmentedHashCode() {
128         return codecHashCode();
129     }
130
131     // Non-final to allow specialization in AugmentableCodecDataObject
132     boolean codecAugmentedEquals(final T other) {
133         return codecEquals(other);
134     }
135
136     // Non-final to allow specialization in AugmentableCodecDataObject
137     ToStringHelper codecAugmentedFillToString(final ToStringHelper helper) {
138         return codecFillToString(helper);
139     }
140
141     // Helper split out of codecMember to aid its inlining
142     private Object loadMember(final VarHandle handle, final NodeCodecContext childCtx) {
143         @SuppressWarnings("unchecked")
144         final Optional<NormalizedNode<?, ?>> child = data.getChild(childCtx.getDomPathArgument());
145
146         // We do not want to use Optional.map() here because we do not want to invoke defaultObject() when we have
147         // normal value because defaultObject() may end up throwing an exception intentionally.
148         final Object obj = child.isPresent() ? childCtx.deserializeObject(child.get()) : childCtx.defaultObject();
149         final Object witness = handle.compareAndExchangeRelease(this, null, maskNull(obj));
150         return witness == null ? obj : unmaskNull(witness);
151     }
152
153     // Helper split out of codecKey to aid its inlining
154     private @NonNull Object loadKey(final VarHandle handle) {
155         verify(data instanceof MapEntryNode, "Unsupported value %s", data);
156         verify(context instanceof KeyedListNodeCodecContext, "Unexpected context %s", context);
157         final Object obj = ((KeyedListNodeCodecContext<?, ?>) context)
158                 .deserialize(((MapEntryNode) data).getIdentifier());
159         // key is known to be non-null, no need to mask it
160         final Object witness = handle.compareAndExchangeRelease(this, null, obj);
161         return witness == null ? obj : witness;
162     }
163
164     // Helper split out of hashCode() to aid its inlining
165     private int loadHashCode() {
166         final int result = codecAugmentedHashCode();
167         final Object witness = CACHED_HASH_CODE.compareAndExchangeRelease(this, null, Integer.valueOf(result));
168         return witness == null ? result : (Integer) witness;
169     }
170
171     private static @NonNull Object maskNull(final @Nullable Object unmasked) {
172         return unmasked == null ? NULL_VALUE : unmasked;
173     }
174
175     private static @Nullable Object unmaskNull(final Object masked) {
176         return masked == NULL_VALUE ? null : masked;
177     }
178 }