Disconnect IdentifiableItemCodec from IllegalArgumentCodec
[mdsal.git] / binding / mdsal-binding-dom-codec / src / main / java / org / opendaylight / mdsal / binding / dom / codec / impl / IdentifiableItemCodec.java
index 57b45a2d7199e1efec7f590dbb64cfa710165eaa..0b89bff24fda6e2ea4e277388b5c999d72c8c9a2 100644 (file)
  */
 package org.opendaylight.mdsal.binding.dom.codec.impl;
 
+import static java.util.Objects.requireNonNull;
+
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import java.lang.invoke.MethodHandle;
 import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
 import java.lang.reflect.Constructor;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedHashMap;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import org.opendaylight.yangtools.concepts.Codec;
+import java.util.Set;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.mdsal.binding.spec.naming.BindingMapping;
+import org.opendaylight.yangtools.util.ImmutableOffsetMap;
+import org.opendaylight.yangtools.util.ImmutableOffsetMapTemplate;
+import org.opendaylight.yangtools.yang.binding.Identifiable;
 import org.opendaylight.yangtools.yang.binding.Identifier;
 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier.IdentifiableItem;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
-import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.stmt.KeyEffectiveStatement;
+import org.opendaylight.yangtools.yang.model.api.stmt.ListEffectiveStatement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-final class IdentifiableItemCodec implements Codec<NodeIdentifierWithPredicates, IdentifiableItem<?, ?>> {
-    private final Map<QName, ValueContext> keyValueContexts;
-    private final List<QName> keysInBindingOrder;
-    private final ListSchemaNode schema;
-    private final Class<?> identifiable;
-    private final MethodHandle ctorInvoker;
-    private final MethodHandle ctor;
+/**
+ * Codec support for extracting the {@link Identifiable#key()} method return from a MapEntryNode.
+ */
+// FIXME: sealed class when we have JDK17+
+abstract class IdentifiableItemCodec {
+    private static final class SingleKey extends IdentifiableItemCodec {
+        private static final MethodType CTOR_TYPE = MethodType.methodType(Identifier.class, Object.class);
 
-    IdentifiableItemCodec(final ListSchemaNode schema, final Class<? extends Identifier<?>> keyClass,
-            final Class<?> identifiable, final Map<QName, ValueContext> keyValueContexts) {
-        this.schema = schema;
-        this.identifiable = identifiable;
+        private final ValueContext keyContext;
+        private final MethodHandle ctor;
+        private final QName keyName;
 
-        try {
-            ctor = MethodHandles.publicLookup().unreflectConstructor(getConstructor(keyClass));
-        } catch (IllegalAccessException e) {
-            throw new IllegalArgumentException("Missing constructor in class " + keyClass, e);
+        SingleKey(final ListEffectiveStatement schema, final Class<? extends Identifier<?>> keyClass,
+                final Class<?> identifiable, final QName keyName, final ValueContext keyContext) {
+            super(schema, keyClass, identifiable);
+            this.keyContext = requireNonNull(keyContext);
+            this.keyName = requireNonNull(keyName);
+            ctor = getConstructor(keyClass, 1).asType(CTOR_TYPE);
         }
-        final MethodHandle inv = MethodHandles.spreadInvoker(ctor.type(), 0);
-        this.ctorInvoker = inv.asType(inv.type().changeReturnType(Identifier.class));
-
-        /*
-         * We need to re-index to make sure we instantiate nodes in the order in which
-         * they are defined.
-         */
-        final Map<QName, ValueContext> keys = new LinkedHashMap<>();
-        for (final QName qname : schema.getKeyDefinition()) {
-            keys.put(qname, keyValueContexts.get(qname));
+
+        @Override
+        Identifier<?> deserializeIdentifierImpl(final NodeIdentifierWithPredicates nip) throws Throwable {
+            return (Identifier<?>) ctor.invokeExact(keyContext.deserialize(nip.getValue(keyName)));
         }
-        this.keyValueContexts = ImmutableMap.copyOf(keys);
-
-        /*
-         * When instantiating binding objects we need to specify constructor arguments
-         * in alphabetic order. We play a couple of tricks here to optimize CPU/memory
-         * trade-offs.
-         *
-         * We do not have to perform a sort if the source collection has less than two
-         * elements.
-
-         * We always perform an ImmutableList.copyOf(), as that will turn into a no-op
-         * if the source is already immutable. It will also produce optimized implementations
-         * for empty and singleton collections.
-         *
-         * BUG-2755: remove this if order is made declaration-order-dependent
-         */
-        final List<QName> unsortedKeys = schema.getKeyDefinition();
-        final List<QName> sortedKeys;
-        if (unsortedKeys.size() > 1) {
-            final List<QName> tmp = new ArrayList<>(unsortedKeys);
-            Collections.sort(tmp, (q1, q2) -> q1.getLocalName().compareToIgnoreCase(q2.getLocalName()));
-            sortedKeys = tmp;
-        } else {
-            sortedKeys = unsortedKeys;
+
+        @Override
+        NodeIdentifierWithPredicates serializeIdentifier(final QName qname, final Identifier<?> key) {
+            return NodeIdentifierWithPredicates.of(qname, keyName, keyContext.getAndSerialize(key));
         }
+    }
+
+    private static final class MultiKey extends IdentifiableItemCodec {
+        private final ImmutableOffsetMapTemplate<QName> predicateTemplate;
+        private final ImmutableOffsetMap<QName, ValueContext> keyValueContexts;
+        private final ImmutableList<QName> keysInBindingOrder;
+        private final MethodHandle ctor;
+
+        MultiKey(final ListEffectiveStatement schema, final Class<? extends Identifier<?>> keyClass,
+                final Class<?> identifiable, final Map<QName, ValueContext> keyValueContexts) {
+            super(schema, keyClass, identifiable);
+
+            final MethodHandle tmpCtor = getConstructor(keyClass, keyValueContexts.size());
+            final MethodHandle inv = MethodHandles.spreadInvoker(tmpCtor.type(), 0);
+            ctor = inv.asType(inv.type().changeReturnType(Identifier.class)).bindTo(tmpCtor);
+
+            /*
+             * We need to re-index to make sure we instantiate nodes in the order in which they are defined. We will
+             * also need to instantiate values in the same order.
+             */
+            final Set<QName> keyDef = schema.findFirstEffectiveSubstatementArgument(KeyEffectiveStatement.class)
+                .orElseThrow();
+            predicateTemplate = ImmutableOffsetMapTemplate.ordered(keyDef);
+            this.keyValueContexts = predicateTemplate.instantiateTransformed(keyValueContexts, (key, value) -> value);
 
-        this.keysInBindingOrder = ImmutableList.copyOf(sortedKeys);
+            /*
+             * When instantiating binding objects we need to specify constructor arguments in alphabetic order. If the
+             * order matches definition order, we try to reuse the key definition.
+             *
+             * BUG-2755: remove this if order is made declaration-order-dependent
+             */
+            final List<QName> tmp = new ArrayList<>(keyDef);
+            // This is not terribly efficient but gets the job done
+            tmp.sort(Comparator.comparing(qname -> BindingMapping.getPropertyName(qname.getLocalName())));
+            keysInBindingOrder = ImmutableList.copyOf(tmp.equals(List.copyOf(keyDef)) ? keyDef : tmp);
+        }
+
+        @Override
+        Identifier<?> deserializeIdentifierImpl(final NodeIdentifierWithPredicates nip) throws Throwable {
+            final Object[] bindingValues = new Object[keysInBindingOrder.size()];
+            int offset = 0;
+            for (final QName key : keysInBindingOrder) {
+                bindingValues[offset++] = keyValueContexts.get(key).deserialize(nip.getValue(key));
+            }
+
+            return (Identifier<?>) ctor.invokeExact(bindingValues);
+        }
+
+        @Override
+        NodeIdentifierWithPredicates serializeIdentifier(final QName qname, final Identifier<?> key) {
+            final Object[] values = new Object[keyValueContexts.size()];
+            int offset = 0;
+            for (final ValueContext valueCtx : keyValueContexts.values()) {
+                values[offset++] = valueCtx.getAndSerialize(key);
+            }
+
+            return NodeIdentifierWithPredicates.of(qname, predicateTemplate.instantiateWithValues(values));
+        }
     }
 
-    @Override
-    @SuppressWarnings("checkstyle:illegalCatch")
-    public IdentifiableItem<?, ?> deserialize(final NodeIdentifierWithPredicates input) {
-        final Object[] bindingValues = new Object[keysInBindingOrder.size()];
-        int offset = 0;
+    private static final Logger LOG = LoggerFactory.getLogger(IdentifiableItemCodec.class);
 
-        for (final QName key : keysInBindingOrder) {
-            final Object yangValue = input.getKeyValues().get(key);
-            bindingValues[offset++] = keyValueContexts.get(key).deserialize(yangValue);
+    private final Class<?> identifiable;
+    private final QName qname;
+
+    IdentifiableItemCodec(final ListEffectiveStatement schema, final Class<? extends Identifier<?>> keyClass,
+            final Class<?> identifiable) {
+        this.identifiable = requireNonNull(identifiable);
+        qname = schema.argument();
+    }
+
+    static IdentifiableItemCodec of(final ListEffectiveStatement schema,
+            final Class<? extends Identifier<?>> keyClass, final Class<?> identifiable,
+                    final Map<QName, ValueContext> keyValueContexts) {
+        switch (keyValueContexts.size()) {
+            case 0:
+                throw new IllegalArgumentException("Key " + keyClass + " of " + identifiable + " has no components");
+            case 1:
+                final Entry<QName, ValueContext> entry = keyValueContexts.entrySet().iterator().next();
+                return new SingleKey(schema, keyClass, identifiable, entry.getKey(), entry.getValue());
+            default:
+                return new MultiKey(schema, keyClass, identifiable, keyValueContexts);
         }
+    }
 
-        final Identifier<?> identifier;
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    final @NonNull IdentifiableItem<?, ?> domToBinding(final NodeIdentifierWithPredicates input) {
+        return IdentifiableItem.of((Class) identifiable, (Identifier) deserializeIdentifier(requireNonNull(input)));
+    }
+
+    final @NonNull NodeIdentifierWithPredicates bindingToDom(final IdentifiableItem<?, ?> input) {
+        return serializeIdentifier(qname, input.getKey());
+    }
+
+    @SuppressWarnings("checkstyle:illegalCatch")
+    final @NonNull Identifier<?> deserializeIdentifier(final @NonNull NodeIdentifierWithPredicates input) {
         try {
-            identifier = (Identifier<?>) ctorInvoker.invokeExact(ctor, bindingValues);
+            return deserializeIdentifierImpl(input);
         } catch (Throwable e) {
             Throwables.throwIfUnchecked(e);
-            throw new RuntimeException(e);
+            throw new IllegalStateException("Failed to deserialize " + input, e);
         }
-
-        @SuppressWarnings({ "rawtypes", "unchecked" })
-        final IdentifiableItem identifiableItem = new IdentifiableItem(identifiable, identifier);
-        return identifiableItem;
     }
 
-    @Override
-    public NodeIdentifierWithPredicates serialize(final IdentifiableItem<?, ?> input) {
-        final Object value = input.getKey();
+    @SuppressWarnings("checkstyle:illegalThrows")
+    abstract @NonNull Identifier<?> deserializeIdentifierImpl(@NonNull NodeIdentifierWithPredicates nip)
+            throws Throwable;
 
-        final Map<QName, Object> values = new LinkedHashMap<>();
-        for (final Entry<QName, ValueContext> valueCtx : keyValueContexts.entrySet()) {
-            values.put(valueCtx.getKey(), valueCtx.getValue().getAndSerialize(value));
-        }
-        return new NodeIdentifierWithPredicates(schema.getQName(), values);
-    }
+    abstract @NonNull NodeIdentifierWithPredicates serializeIdentifier(QName qname, Identifier<?> key);
+
+    static MethodHandle getConstructor(final Class<? extends Identifier<?>> clazz, final int nrArgs) {
+        for (final Constructor<?> ctor : clazz.getConstructors()) {
+            // Check argument count
+            if (ctor.getParameterCount() != nrArgs) {
+                LOG.debug("Skipping {} due to argument count mismatch", ctor);
+                continue;
+            }
+
+            // Do not consider deprecated constructors
+            if (isDeprecated(ctor)) {
+                LOG.debug("Skipping deprecated constructor {}", ctor);
+                continue;
+            }
+
+            // Do not consider copy constructors
+            if (clazz.equals(ctor.getParameterTypes()[0])) {
+                LOG.debug("Skipping copy constructor {}", ctor);
+                continue;
+            }
 
-    @SuppressWarnings("unchecked")
-    private static Constructor<? extends Identifier<?>> getConstructor(final Class<? extends Identifier<?>> clazz) {
-        for (@SuppressWarnings("rawtypes") final Constructor constr : clazz.getConstructors()) {
-            final Class<?>[] parameters = constr.getParameterTypes();
-            if (!clazz.equals(parameters[0])) {
-                // It is not copy constructor;
-                return constr;
+            try {
+                return MethodHandles.publicLookup().unreflectConstructor(ctor);
+            } catch (IllegalAccessException e) {
+                throw new IllegalStateException("Cannot access constructor " + ctor + " in class " + clazz, e);
             }
         }
-        throw new IllegalArgumentException("Supplied class " + clazz + "does not have required constructor.");
+        throw new IllegalArgumentException("Supplied class " + clazz + " does not have required constructor.");
+    }
+
+    // This could be inlined, but then it throws off Eclipse analysis, which thinks the return is always non-null
+    private static boolean isDeprecated(final Constructor<?> ctor) {
+        return ctor.getAnnotation(Deprecated.class) != null;
     }
 }