/* * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ package org.opendaylight.mdsal.binding.runtime.api; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import com.google.common.annotations.Beta; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import java.util.AbstractMap.SimpleEntry; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.opendaylight.mdsal.binding.model.api.GeneratedType; import org.opendaylight.mdsal.binding.model.api.MethodSignature; import org.opendaylight.mdsal.binding.model.api.ParameterizedType; import org.opendaylight.mdsal.binding.model.api.Type; import org.opendaylight.mdsal.binding.model.api.type.builder.GeneratedTypeBuilder; import org.opendaylight.yangtools.yang.binding.Action; import org.opendaylight.yangtools.yang.binding.Augmentation; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.common.QNameModule; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier; import org.opendaylight.yangtools.yang.model.api.ActionDefinition; import org.opendaylight.yangtools.yang.model.api.AugmentationSchemaNode; import org.opendaylight.yangtools.yang.model.api.AugmentationTarget; import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode; import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode; import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; import org.opendaylight.yangtools.yang.model.api.DerivableSchemaNode; import org.opendaylight.yangtools.yang.model.api.DocumentedNode.WithStatus; import org.opendaylight.yangtools.yang.model.api.SchemaNode; import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute; import org.opendaylight.yangtools.yang.model.util.EffectiveAugmentationSchema; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Runtime Context for Java YANG Binding classes. It provides information derived from the backing effective model, * which is not captured in generated classes (and hence cannot be obtained from {@code BindingReflections}. * *

Some of this information are for example list of all available children for cases * {@link #getChoiceCaseChildren(DataNodeContainer)}, since choices are augmentable and new choices may be introduced * by additional models. Same goes for all possible augmentations. */ @Beta public abstract class AbstractBindingRuntimeContext implements BindingRuntimeContext { private static final Logger LOG = LoggerFactory.getLogger(AbstractBindingRuntimeContext.class); private final LoadingCache> identityClasses = CacheBuilder.newBuilder().weakValues().build( new CacheLoader>() { @Override public Class load(final QName key) { final Optional identityType = getTypes().findIdentity(key); checkArgument(identityType.isPresent(), "Supplied QName %s is not a valid identity", key); try { return loadClass(identityType.get()); } catch (final ClassNotFoundException e) { throw new IllegalArgumentException("Required class " + identityType + "was not found.", e); } } }); @Override public final > AugmentationSchemaNode getAugmentationDefinition(final Class augClass) { return getTypes().findAugmentation(Type.of(augClass)).orElse(null); } @Override public final DataSchemaNode getSchemaDefinition(final Class cls) { checkArgument(!Augmentation.class.isAssignableFrom(cls), "Supplied class must not be an augmentation (%s is)", cls); checkArgument(!Action.class.isAssignableFrom(cls), "Supplied class must not be an action (%s is)", cls); return (DataSchemaNode) getTypes().findSchema(Type.of(cls)).orElse(null); } @Override public final DataSchemaNode findChildSchemaDefinition(final DataNodeContainer parentSchema, final QNameModule parentNamespace, final Class childClass) { final DataSchemaNode origDef = getSchemaDefinition(childClass); if (origDef == null) { // Weird, the child does not have an associated definition return null; } // Direct instantiation or use in same module in which grouping was defined. final QName origName = origDef.getQName(); final DataSchemaNode sameName = parentSchema.dataChildByName(origName); if (sameName != null) { // Check if it is: // - exactly same schema node, or // - instantiated node was added via uses statement and is instantiation of same grouping if (origDef.equals(sameName) || origDef.equals(getRootOriginalIfPossible(sameName))) { return sameName; } // Node has same name, but clearly is different return null; } // We are looking for instantiation via uses in other module final DataSchemaNode potential = parentSchema.dataChildByName(origName.bindTo(parentNamespace)); // We check if it is really instantiated from same definition as class was derived if (potential != null && origDef.equals(getRootOriginalIfPossible(potential))) { return potential; } return null; } private static @Nullable SchemaNode getRootOriginalIfPossible(final SchemaNode data) { SchemaNode previous = null; SchemaNode next = getOriginalIfPossible(data); while (next != null) { previous = next; next = getOriginalIfPossible(next); } return previous; } private static @Nullable SchemaNode getOriginalIfPossible(final SchemaNode node) { return node instanceof DerivableSchemaNode ? ((DerivableSchemaNode) node).getOriginal().orElse(null) : null; } @Override public final ActionDefinition getActionDefinition(final Class> cls) { return (ActionDefinition) getTypes().findSchema(Type.of(cls)).orElse(null); } @Override public final Absolute getActionIdentifier(final Class> cls) { return getTypes().findSchemaNodeIdentifier(Type.of(cls)).orElse(null); } @Override public final Entry getResolvedAugmentationSchema( final DataNodeContainer target, final Class> aug) { final AugmentationSchemaNode origSchema = getAugmentationDefinition(aug); checkArgument(origSchema != null, "Augmentation %s is not known in current schema context", aug); /* * FIXME: Validate augmentation schema lookup * * Currently this algorithm, does not verify if instantiated child nodes * are real one derived from augmentation schema. The problem with * full validation is, if user used copy builders, he may use * augmentation which was generated for different place. * * If this augmentations have same definition, we emit same identifier * with data and it is up to underlying user to validate data. * */ final Set childNames = new HashSet<>(); final Set realChilds = new HashSet<>(); for (final DataSchemaNode child : origSchema.getChildNodes()) { final DataSchemaNode dataChildQNname = target.dataChildByName(child.getQName()); final String childLocalName = child.getQName().getLocalName(); if (dataChildQNname == null) { for (DataSchemaNode dataSchemaNode : target.getChildNodes()) { if (childLocalName.equals(dataSchemaNode.getQName().getLocalName())) { realChilds.add(dataSchemaNode); childNames.add(dataSchemaNode.getQName()); } } } else { realChilds.add(dataChildQNname); childNames.add(child.getQName()); } } final AugmentationIdentifier identifier = AugmentationIdentifier.create(childNames); final AugmentationSchemaNode proxy = new EffectiveAugmentationSchema(origSchema, realChilds); return new SimpleEntry<>(identifier, proxy); } @Override public final Optional getCaseSchemaDefinition(final ChoiceSchemaNode schema, final Class childClass) { final DataSchemaNode origSchema = getSchemaDefinition(childClass); checkArgument(origSchema instanceof CaseSchemaNode, "Supplied schema %s is not case.", origSchema); /* FIXME: Make sure that if there are multiple augmentations of same * named case, with same structure we treat it as equals * this is due property of Binding specification and copy builders * that user may be unaware that he is using incorrect case * which was generated for choice inside grouping. */ return findInstantiatedCase(schema, (CaseSchemaNode) origSchema); } @Override public final Entry getTypeWithSchema(final Class type) { return getTypeWithSchema(getTypes(), Type.of(type)); } private static @NonNull Entry getTypeWithSchema(final BindingRuntimeTypes types, final Type referencedType) { final WithStatus schema = types.findSchema(referencedType).orElseThrow( () -> new NullPointerException("Failed to find schema for type " + referencedType)); final Type definedType = types.findType(schema).orElseThrow( () -> new NullPointerException("Failed to find defined type for " + referencedType + " schema " + schema)); if (definedType instanceof GeneratedTypeBuilder) { return new SimpleEntry<>(((GeneratedTypeBuilder) definedType).build(), schema); } checkArgument(definedType instanceof GeneratedType, "Type %s is not a GeneratedType", referencedType); return new SimpleEntry<>((GeneratedType) definedType, schema); } @Override public final Map> getChoiceCaseChildren(final DataNodeContainer schema) { return getChoiceCaseChildren(getTypes(), schema); } private static @NonNull ImmutableMap> getChoiceCaseChildren(final BindingRuntimeTypes types, final DataNodeContainer schema) { final Map> childToCase = new HashMap<>(); for (final ChoiceSchemaNode choice : Iterables.filter(schema.getChildNodes(), ChoiceSchemaNode.class)) { final ChoiceSchemaNode originalChoice = getOriginalSchema(choice); final Optional optType = types.findType(originalChoice); checkState(optType.isPresent(), "Failed to find generated type for choice %s", originalChoice); final Type choiceType = optType.get(); for (Type caze : types.findCases(choiceType)) { final Entry caseIdentifier = new SimpleEntry<>(choiceType, caze); final HashSet caseChildren = new HashSet<>(); if (caze instanceof GeneratedTypeBuilder) { caze = ((GeneratedTypeBuilder) caze).build(); } collectAllContainerTypes((GeneratedType) caze, caseChildren); for (final Type caseChild : caseChildren) { childToCase.put(caseChild, caseIdentifier); } } } return ImmutableMap.copyOf(childToCase); } @Override public final Set> getCases(final Class choice) { final Collection cazes = getTypes().findCases(Type.of(choice)); final Set> ret = new HashSet<>(cazes.size()); for (final Type caze : cazes) { try { ret.add(loadClass(caze)); } catch (final ClassNotFoundException e) { LOG.warn("Failed to load class for case {}, ignoring it", caze, e); } } return ret; } @Override public final Class getClassForSchema(final SchemaNode childSchema) { final SchemaNode origSchema = getOriginalSchema(childSchema); final Optional clazzType = getTypes().findType(origSchema); checkArgument(clazzType.isPresent(), "Failed to find binding type for %s (original %s)", childSchema, origSchema); try { return loadClass(clazzType.get()); } catch (final ClassNotFoundException e) { throw new IllegalStateException(e); } } @Override public final ImmutableMap getAvailableAugmentationTypes( final DataNodeContainer container) { if (container instanceof AugmentationTarget) { final var augmentations = ((AugmentationTarget) container).getAvailableAugmentations(); if (!augmentations.isEmpty()) { final var identifierToType = new HashMap(); final var types = getTypes(); for (var augment : augmentations) { types.findOriginalAugmentationType(augment).ifPresent(augType -> { identifierToType.put(getAugmentationIdentifier(augment), augType); }); } return ImmutableMap.copyOf(identifierToType); } } return ImmutableMap.of(); } @Override public final Class getIdentityClass(final QName input) { return identityClasses.getUnchecked(input); } private static AugmentationIdentifier getAugmentationIdentifier(final AugmentationSchemaNode augment) { // FIXME: use DataSchemaContextNode.augmentationIdentifierFrom() once it does caching return AugmentationIdentifier.create(augment.getChildNodes().stream().map(DataSchemaNode::getQName) .collect(ImmutableSet.toImmutableSet())); } private static Set collectAllContainerTypes(final GeneratedType type, final Set collection) { for (final MethodSignature definition : type.getMethodDefinitions()) { Type childType = definition.getReturnType(); if (childType instanceof ParameterizedType) { childType = ((ParameterizedType) childType).getActualTypeArguments()[0]; } if (childType instanceof GeneratedType || childType instanceof GeneratedTypeBuilder) { collection.add(childType); } } for (final Type parent : type.getImplements()) { if (parent instanceof GeneratedType) { collectAllContainerTypes((GeneratedType) parent, collection); } } return collection; } private static T getOriginalSchema(final T choice) { @SuppressWarnings("unchecked") final T original = (T) originalNodeOf(choice); if (original != null) { return original; } return choice; } private static @NonNull Optional findInstantiatedCase(final ChoiceSchemaNode instantiatedChoice, final CaseSchemaNode originalDefinition) { CaseSchemaNode potential = instantiatedChoice.findCase(originalDefinition.getQName()).orElse(null); if (originalDefinition.equals(potential)) { return Optional.of(potential); } if (potential != null) { SchemaNode potentialRoot = originalNodeOf(potential); if (originalDefinition.equals(potentialRoot)) { return Optional.of(potential); } } // We try to find case by name, then lookup its root definition // and compare it with original definition // This solves case, if choice was inside grouping // which was used in different module and thus namespaces are // different, but local names are still same. // // Still we need to check equality of definition, because local name is not // sufficient to uniquelly determine equality of cases // for (CaseSchemaNode caze : instantiatedChoice.findCaseNodes(originalDefinition.getQName().getLocalName())) { if (originalDefinition.equals(originalNodeOf(caze))) { return Optional.of(caze); } } return Optional.empty(); } private static @Nullable SchemaNode originalNodeOf(final SchemaNode node) { return node instanceof DerivableSchemaNode ? ((DerivableSchemaNode) node).getOriginal().orElse(null) : null; } }