diff options
Diffstat (limited to 'infra/translate-utils/src/main/java/io')
23 files changed, 2821 insertions, 0 deletions
diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/AbstractSubtreeManagerRegistryBuilderBuilder.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/AbstractSubtreeManagerRegistryBuilderBuilder.java new file mode 100644 index 000000000..23a66337f --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/AbstractSubtreeManagerRegistryBuilderBuilder.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; +import io.fd.honeycomb.v3po.translate.ModifiableSubtreeManagerRegistryBuilder; +import io.fd.honeycomb.v3po.translate.SubtreeManager; +import io.fd.honeycomb.v3po.translate.SubtreeManagerRegistryBuilder; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nonnull; +import org.jgrapht.experimental.dag.DirectedAcyclicGraph; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; + +public abstract class AbstractSubtreeManagerRegistryBuilderBuilder<S extends SubtreeManager<? extends DataObject>, R> + implements ModifiableSubtreeManagerRegistryBuilder<S>, SubtreeManagerRegistryBuilder<R>, AutoCloseable { + + // Using directed acyclic graph to represent the ordering relationships between writers + private final DirectedAcyclicGraph<InstanceIdentifier<?>, Order> + handlersRelations = new DirectedAcyclicGraph<>((sourceVertex, targetVertex) -> new Order()); + private final Map<InstanceIdentifier<?>, S> handlersMap = new HashMap<>(); + + /** + * Add handler without any special relationship to any other type. + */ + @Override + public AbstractSubtreeManagerRegistryBuilderBuilder<S, R> add(@Nonnull final S handler) { + // Make IID wildcarded just in case + // + the way InstanceIdentifier.create + equals work for Identifiable items is unexpected, meaning updates would + // not be matched to writers in registry + final InstanceIdentifier<?> targetType = RWUtils.makeIidWildcarded(handler.getManagedDataObjectType()); + checkWriterNotPresentYet(targetType); + handlersRelations.addVertex(targetType); + handlersMap.put(targetType, handler); + return this; + } + + /** + * Add handler without any special relationship to any other type. + */ + @Override + public AbstractSubtreeManagerRegistryBuilderBuilder<S, R> subtreeAdd(@Nonnull final Set<InstanceIdentifier<?>> handledChildren, + @Nonnull final S handler) { + add(getSubtreeHandler(handledChildren, handler)); + return this; + } + + private void checkWriterNotPresentYet(final InstanceIdentifier<?> targetType) { + Preconditions.checkArgument(!handlersMap.containsKey(targetType), + "Writer for type: %s already present: %s", targetType, handlersMap.get(targetType)); + } + + /** + * Add handler with relationship: to be executed before handler handling relatedType. + */ + @Override + public AbstractSubtreeManagerRegistryBuilderBuilder<S, R> addBefore(@Nonnull final S handler, + @Nonnull final InstanceIdentifier<?> relatedType) { + final InstanceIdentifier<?> targetType = RWUtils.makeIidWildcarded(handler.getManagedDataObjectType()); + final InstanceIdentifier<?> wildcardedRelatedType = RWUtils.makeIidWildcarded(relatedType); + checkWriterNotPresentYet(targetType); + handlersRelations.addVertex(targetType); + handlersRelations.addVertex(wildcardedRelatedType); + addEdge(targetType, wildcardedRelatedType); + handlersMap.put(targetType, handler); + return this; + } + + @Override + public AbstractSubtreeManagerRegistryBuilderBuilder<S, R> addBefore(@Nonnull final S handler, + @Nonnull final Collection<InstanceIdentifier<?>> relatedTypes) { + final InstanceIdentifier<?> targetType = RWUtils.makeIidWildcarded(handler.getManagedDataObjectType()); + checkWriterNotPresentYet(targetType); + handlersRelations.addVertex(targetType); + relatedTypes.stream() + .map(RWUtils::makeIidWildcarded) + .forEach(handlersRelations::addVertex); + relatedTypes.stream() + .map(RWUtils::makeIidWildcarded) + .forEach(type -> addEdge(targetType, type)); + handlersMap.put(targetType, handler); + return this; + } + + @Override + public AbstractSubtreeManagerRegistryBuilderBuilder<S, R> subtreeAddBefore( + @Nonnull final Set<InstanceIdentifier<?>> handledChildren, + @Nonnull final S handler, + @Nonnull final InstanceIdentifier<?> relatedType) { + return addBefore(getSubtreeHandler(handledChildren, handler), relatedType); + } + + @Override + public AbstractSubtreeManagerRegistryBuilderBuilder<S, R> subtreeAddBefore( + @Nonnull final Set<InstanceIdentifier<?>> handledChildren, + @Nonnull final S handler, + @Nonnull final Collection<InstanceIdentifier<?>> relatedTypes) { + return addBefore(getSubtreeHandler(handledChildren, handler), relatedTypes); + } + + protected abstract S getSubtreeHandler(@Nonnull final Set<InstanceIdentifier<?>> handledChildren, + @Nonnull final S handler); + + /** + * Add handler with relationship: to be executed after handler handling relatedType. + */ + @Override + public AbstractSubtreeManagerRegistryBuilderBuilder<S, R> addAfter(@Nonnull final S handler, + @Nonnull final InstanceIdentifier<?> relatedType) { + final InstanceIdentifier<?> targetType = RWUtils.makeIidWildcarded(handler.getManagedDataObjectType()); + final InstanceIdentifier<?> wildcardedRelatedType = RWUtils.makeIidWildcarded(relatedType); + checkWriterNotPresentYet(targetType); + handlersRelations.addVertex(targetType); + handlersRelations.addVertex(wildcardedRelatedType); + // set edge to indicate before relationship, just reversed + addEdge(wildcardedRelatedType, targetType); + handlersMap.put(targetType, handler); + return this; + } + + @Override + public AbstractSubtreeManagerRegistryBuilderBuilder<S, R> addAfter(@Nonnull final S handler, + @Nonnull final Collection<InstanceIdentifier<?>> relatedTypes) { + final InstanceIdentifier<?> targetType = RWUtils.makeIidWildcarded(handler.getManagedDataObjectType()); + checkWriterNotPresentYet(targetType); + handlersRelations.addVertex(targetType); + relatedTypes.stream() + .map(RWUtils::makeIidWildcarded) + .forEach(handlersRelations::addVertex); + // set edge to indicate before relationship, just reversed + relatedTypes.stream() + .map(RWUtils::makeIidWildcarded) + .forEach(type -> addEdge(type, targetType)); + handlersMap.put(targetType, handler); + return this; + } + + @Override + public AbstractSubtreeManagerRegistryBuilderBuilder<S, R> subtreeAddAfter( + @Nonnull final Set<InstanceIdentifier<?>> handledChildren, + @Nonnull final S handler, + @Nonnull final InstanceIdentifier<?> relatedType) { + return addAfter(getSubtreeHandler(handledChildren, handler), relatedType); + } + + @Override + public AbstractSubtreeManagerRegistryBuilderBuilder<S, R> subtreeAddAfter( + @Nonnull final Set<InstanceIdentifier<?>> handledChildren, + @Nonnull final S handler, + @Nonnull final Collection<InstanceIdentifier<?>> relatedTypes) { + return addAfter(getSubtreeHandler(handledChildren, handler), relatedTypes); + } + + + private void addEdge(final InstanceIdentifier<?> targetType, + final InstanceIdentifier<?> relatedType) { + try { + handlersRelations.addDagEdge(targetType, relatedType); + } catch (DirectedAcyclicGraph.CycleFoundException e) { + throw new IllegalArgumentException(String.format( + "Unable to add writer with relation: %s -> %s. Loop detected", targetType, relatedType), e); + } + } + + protected ImmutableMap<InstanceIdentifier<?>, S> getMappedHandlers() { + final ImmutableMap.Builder<InstanceIdentifier<?>, S> builder = ImmutableMap.builder(); + // Iterate writer types according to their relationships from graph + handlersRelations.iterator() + .forEachRemaining(writerType -> { + // There might be types stored just for relationship sake, no real writer, ignoring those + if (handlersMap.containsKey(writerType)) { + builder.put(writerType, handlersMap.get(writerType)); + } + }); + + // TODO we could optimize subtree handlers, if there is a dedicated handler for a node managed by a subtree + // handler, recreate the subtree handler with a subset of handled child nodes + // This way it is not necessary to change the configuration of subtree writer, just to add a dedicated child + // writer. This will be needed if we ever switch to annotations for reader/writer hierarchy initialization + + return builder.build(); + } + + @Override + public void close() throws Exception { + handlersMap.clear(); + // Wrap sets into another set to avoid concurrent modification ex in graph + handlersRelations.removeAllEdges(Sets.newHashSet(handlersRelations.edgeSet())); + handlersRelations.removeAllVertices(Sets.newHashSet(handlersRelations.vertexSet())); + } + + // Represents edges in graph + private class Order {} +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/JsonUtils.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/JsonUtils.java new file mode 100644 index 000000000..d9798d07d --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/JsonUtils.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util; + +import com.google.common.base.Charsets; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream; +import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory; +import org.opendaylight.yangtools.yang.data.impl.schema.Builders; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.DataContainerNodeAttrBuilder; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.opendaylight.yangtools.yang.model.api.SchemaPath; + +public final class JsonUtils { + + private JsonUtils() {} + + /** + * Serialize normalized node root structure into provided output stream + * + * @throws IOException if serialized data cannot be written into provided output stream + */ + public static void writeJsonRoot(@Nonnull final NormalizedNode<?, ?> rootData, + @Nonnull final SchemaContext schemaContext, + @Nonnull final OutputStream outputStream) throws IOException { + final JsonWriter + jsonWriter = createJsonWriter(outputStream, true); + final NormalizedNodeStreamWriter streamWriter = JSONNormalizedNodeStreamWriter + .createNestedWriter(JSONCodecFactory.create(schemaContext), SchemaPath.ROOT, null, jsonWriter); + final NormalizedNodeWriter normalizedNodeWriter = + NormalizedNodeWriter.forStreamWriter(streamWriter, true); + jsonWriter.beginObject(); + writeChildren(normalizedNodeWriter,(ContainerNode) rootData); + jsonWriter.endObject(); + jsonWriter.flush(); + } + + /** + * Read json serialized normalized node root structure and parse them into normalized nodes + * + * @return artificial normalized node holding all the top level nodes from provided stream as children. In case + * the stream is empty, empty artificial normalized node is returned + * + * @throws IllegalArgumentException if content in the provided input stream is not restore-able + */ + public static ContainerNode readJsonRoot(@Nonnull final SchemaContext schemaContext, + @Nonnull final InputStream stream) { + final DataContainerNodeAttrBuilder<YangInstanceIdentifier.NodeIdentifier, ContainerNode> builder = + Builders.containerBuilder().withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(schemaContext.getQName())); + final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(builder); + + final JsonParserStream jsonParser = JsonParserStream.create(writer, schemaContext); + final JsonReader reader = new JsonReader(new InputStreamReader(stream, Charsets.UTF_8)); + jsonParser.parse(reader); + + return builder.build(); + } + + private static void writeChildren(final NormalizedNodeWriter nnWriter, final ContainerNode data) throws IOException { + for(final DataContainerChild<? extends YangInstanceIdentifier.PathArgument, ?> child : data.getValue()) { + nnWriter.write(child); + } + } + + private static JsonWriter createJsonWriter(final OutputStream entityStream, boolean prettyPrint) { + if (prettyPrint) { + return JsonWriterFactory.createJsonWriter(new OutputStreamWriter(entityStream, Charsets.UTF_8), 2); + } else { + return JsonWriterFactory.createJsonWriter(new OutputStreamWriter(entityStream, Charsets.UTF_8)); + } + } +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/RWUtils.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/RWUtils.java new file mode 100644 index 000000000..2a565d9f2 --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/RWUtils.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util; + +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import io.fd.honeycomb.v3po.translate.SubtreeManager; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.yang.binding.Augmentation; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.Identifiable; +import org.opendaylight.yangtools.yang.binding.Identifier; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; + +public final class RWUtils { + + private RWUtils() {} + + /** + * Collector expecting only a single resulting item from a stream + */ + public static<T> Collector<T,?,T> singleItemCollector() { + return Collectors.collectingAndThen( + Collectors.toList(), + list -> { + if (list.size() != 1) { + throw new IllegalStateException("Unexpected size of list: " + list + ". Single item expected"); + } + return list.get(0); + } + ); + } + + /** + * Find next item in ID after provided type + */ + @Nonnull + public static InstanceIdentifier.PathArgument getNextId(@Nonnull final InstanceIdentifier<? extends DataObject> id, + @Nonnull final InstanceIdentifier<? extends DataObject> type) { + // TODO this is inefficient(maybe, depending on actual Iterable type) + final Iterable<InstanceIdentifier.PathArgument> pathArguments = id.getPathArguments(); + final int i = Iterables.indexOf(pathArguments, new Predicate<InstanceIdentifier.PathArgument>() { + @Override + public boolean apply(final InstanceIdentifier.PathArgument input) { + return input.getType().isAssignableFrom(type.getTargetType()); + } + }); + Preconditions.checkArgument(i >= 0, "Unable to find %s type in %s", type.getTargetType(), id); + return Iterables.get(pathArguments, i + 1); + } + + /** + * Replace last item in ID with a provided IdentifiableItem of the same type + */ + @SuppressWarnings("unchecked") + @Nonnull + public static <D extends DataObject & Identifiable<K>, K extends Identifier<D>> InstanceIdentifier<D> replaceLastInId( + @Nonnull final InstanceIdentifier<D> id, final InstanceIdentifier.IdentifiableItem<D, K> currentBdItem) { + + final Iterable<InstanceIdentifier.PathArgument> pathArguments = id.getPathArguments(); + final Iterable<InstanceIdentifier.PathArgument> withoutCurrent = + Iterables.limit(pathArguments, Iterables.size(pathArguments) - 1); + final Iterable<InstanceIdentifier.PathArgument> concat = + Iterables.concat(withoutCurrent, Collections.singleton(currentBdItem)); + return (InstanceIdentifier<D>) InstanceIdentifier.create(concat); + } + + /** + * Create IdentifiableItem from target type of provided ID with provided key + */ + @Nonnull + public static <D extends DataObject & Identifiable<K>, K extends Identifier<D>> InstanceIdentifier.IdentifiableItem<D, K> getCurrentIdItem( + @Nonnull final InstanceIdentifier<D> id, final K key) { + return new InstanceIdentifier.IdentifiableItem<>(id.getTargetType(), key); + } + + /** + * Trim InstanceIdentifier at indexOf(type) + */ + @SuppressWarnings("unchecked") + @Nonnull + public static <D extends DataObject> InstanceIdentifier<D> cutId(@Nonnull final InstanceIdentifier<? extends DataObject> id, + @Nonnull final InstanceIdentifier<D> type) { + final Iterable<InstanceIdentifier.PathArgument> pathArguments = id.getPathArguments(); + final int i = Iterables.indexOf(pathArguments, new Predicate<InstanceIdentifier.PathArgument>() { + @Override + public boolean apply(final InstanceIdentifier.PathArgument input) { + return input.getType().equals(type.getTargetType()); + } + }); + Preconditions.checkArgument(i >= 0, "ID %s does not contain %s", id, type); + return (InstanceIdentifier<D>) InstanceIdentifier.create(Iterables.limit(pathArguments, i + 1)); + } + + /** + * Create an ordered map from a collection, checking for duplicity in the process. + */ + @Nonnull + public static <K, V> Map<K, V> uniqueLinkedIndex(@Nonnull final Collection<V> values, @Nonnull final Function<? super V, K> keyFunction) { + final Map<K, V> objectObjectLinkedHashMap = Maps.newLinkedHashMap(); + for (V value : values) { + final K key = keyFunction.apply(value); + Preconditions.checkArgument(objectObjectLinkedHashMap.put(key, value) == null, + "Duplicate key detected : %s", key); + } + return objectObjectLinkedHashMap; + } + + public static final Function<SubtreeManager<? extends DataObject>, Class<? extends DataObject>> + MANAGER_CLASS_FUNCTION = new Function<SubtreeManager<? extends DataObject>, Class<? extends DataObject>>() { + @Override + public Class<? extends DataObject> apply(final SubtreeManager<? extends DataObject> input) { + return input.getManagedDataObjectType().getTargetType(); + } + }; + + public static final Function<SubtreeManager<? extends Augmentation<?>>, Class<? extends DataObject>> + MANAGER_CLASS_AUG_FUNCTION = new Function<SubtreeManager<? extends Augmentation<?>>, Class<? extends DataObject>>() { + + @Override + @SuppressWarnings("unchecked") + public Class<? extends DataObject> apply(final SubtreeManager<? extends Augmentation<?>> input) { + final Class<? extends Augmentation<?>> targetType = input.getManagedDataObjectType().getTargetType(); + Preconditions.checkArgument(DataObject.class.isAssignableFrom(targetType)); + return (Class<? extends DataObject>) targetType; + } + }; + + /** + * Transform a keyed instance identifier into a wildcarded one. + * <p/> + * ! This has to be called also for wildcarded List instance identifiers + * due to weird behavior of equals in InstanceIdentifier ! + */ + @SuppressWarnings("unchecked") + public static <D extends DataObject> InstanceIdentifier<D> makeIidWildcarded(final InstanceIdentifier<D> id) { + final List<InstanceIdentifier.PathArgument> transformedPathArguments = + StreamSupport.stream(id.getPathArguments().spliterator(), false) + .map(RWUtils::cleanPathArgumentFromKeys) + .collect(Collectors.toList()); + return (InstanceIdentifier<D>) InstanceIdentifier.create(transformedPathArguments); + } + + /** + * Transform a keyed instance identifier into a wildcarded one, keeping keys except the last item. + */ + @SuppressWarnings("unchecked") + public static <D extends DataObject> InstanceIdentifier<D> makeIidLastWildcarded(final InstanceIdentifier<D> id) { + final InstanceIdentifier.Item<D> wildcardedItem = new InstanceIdentifier.Item<>(id.getTargetType()); + final Iterable<InstanceIdentifier.PathArgument> pathArguments = id.getPathArguments(); + return (InstanceIdentifier<D>) InstanceIdentifier.create( + Iterables.concat( + Iterables.limit(pathArguments, Iterables.size(pathArguments) - 1), + Collections.singleton(wildcardedItem))); + } + + private static InstanceIdentifier.PathArgument cleanPathArgumentFromKeys(final InstanceIdentifier.PathArgument pathArgument) { + return pathArgument instanceof InstanceIdentifier.IdentifiableItem<?, ?> + ? new InstanceIdentifier.Item<>(pathArgument.getType()) + : pathArgument; + } +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/ReflectionUtils.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/ReflectionUtils.java new file mode 100644 index 000000000..728c4f80d --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/ReflectionUtils.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util; + +import com.google.common.base.Optional; +import java.lang.reflect.Method; +import java.util.List; +import javax.annotation.Nonnull; + +/** + * Reflection based utilities + */ +public final class ReflectionUtils { + + private ReflectionUtils() {} + + /** + * Find a specific method using reflection + * + * @param managedType Class object to find method in + * @param prefix Method name prefix used when finding the method. Case does not matter. + * @param paramTypes List of input argument types + * @param retType Return type + * + * @return Found method or Optional.absent() if there's no such method + */ + @Nonnull + public static Optional<Method> findMethodReflex(@Nonnull final Class<?> managedType, + @Nonnull final String prefix, + @Nonnull final List<Class<?>> paramTypes, + @Nonnull final Class<?> retType) { + for (Method method : managedType.getMethods()) { + if (isMethodMatch(prefix, paramTypes, retType, method)) { + return Optional.of(method); + } + } + + return Optional.absent(); + } + + private static boolean isMethodMatch(final @Nonnull String prefix, + final @Nonnull List<Class<?>> paramTypes, + final @Nonnull Class<?> retType, final Method method) { + if (!method.getName().toLowerCase().startsWith(prefix.toLowerCase())) { + return false; + } + + final Class<?>[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length != paramTypes.size()) { + return false; + } + + for (int i = 0; i < parameterTypes.length; i++) { + if (!parameterTypes[i].isAssignableFrom(paramTypes.get(i))) { + return false; + } + } + + if (!method.getReturnType().equals(retType)) { + return false; + } + + return true; + } +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/TransactionMappingContext.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/TransactionMappingContext.java new file mode 100644 index 000000000..6abc3b1eb --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/TransactionMappingContext.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util; + +import com.google.common.base.Optional; +import com.google.common.util.concurrent.CheckedFuture; +import io.fd.honeycomb.v3po.translate.MappingContext; +import javax.annotation.Nonnull; +import org.opendaylight.controller.md.sal.binding.api.ReadWriteTransaction; +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException; +import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; + +/** + * Binding Transaction backed mapping context. + */ +public class TransactionMappingContext implements MappingContext { + + private final ReadWriteTransaction readWriteTransaction; + + // TODO make async + + public TransactionMappingContext(final ReadWriteTransaction readWriteTransaction) { + this.readWriteTransaction = readWriteTransaction; + } + + @Override + public <T extends DataObject> Optional<T> read(@Nonnull final InstanceIdentifier<T> currentId) { + try { + return readWriteTransaction.read(LogicalDatastoreType.OPERATIONAL, currentId).checkedGet(); + } catch (ReadFailedException e) { + throw new IllegalStateException("Unable to perform read", e); + } + } + + @Override + public void delete(final InstanceIdentifier<?> path) { + readWriteTransaction.delete(LogicalDatastoreType.OPERATIONAL, path); + } + + @Override + public <T extends DataObject> void merge(final InstanceIdentifier<T> path, T data) { + readWriteTransaction.merge(LogicalDatastoreType.OPERATIONAL, path, data, true); + } + + @Override + public <T extends DataObject> void put(final InstanceIdentifier<T> path, T data) { + readWriteTransaction.put(LogicalDatastoreType.OPERATIONAL, path, data, true); + } + + public CheckedFuture<Void, TransactionCommitFailedException> submit() { + return readWriteTransaction.submit(); + } + + @Override + public void close() { + readWriteTransaction.cancel(); + } +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/AbstractGenericReader.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/AbstractGenericReader.java new file mode 100644 index 000000000..9bfbc2450 --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/AbstractGenericReader.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.read; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.annotations.Beta; +import com.google.common.base.Optional; +import io.fd.honeycomb.v3po.translate.read.ReadContext; +import io.fd.honeycomb.v3po.translate.read.ReadFailedException; +import io.fd.honeycomb.v3po.translate.read.Reader; +import io.fd.honeycomb.v3po.translate.util.RWUtils; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.concepts.Builder; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Beta +public abstract class AbstractGenericReader<D extends DataObject, B extends Builder<D>> implements Reader<D, B> { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractGenericReader.class); + + private final InstanceIdentifier<D> instanceIdentifier; + + protected AbstractGenericReader(final InstanceIdentifier<D> managedDataObjectType) { + this.instanceIdentifier = RWUtils.makeIidWildcarded(managedDataObjectType); + } + + @Nonnull + @Override + public final InstanceIdentifier<D> getManagedDataObjectType() { + return instanceIdentifier; + } + + /** + * @param id {@link InstanceIdentifier} pointing to current node. In case of keyed list, key must be present. + * + */ + protected Optional<D> readCurrent(@Nonnull final InstanceIdentifier<D> id, + @Nonnull final ReadContext ctx) throws ReadFailedException { + LOG.debug("{}: Reading current: {}", this, id); + final B builder = getBuilder(id); + // Cache empty value to determine if anything has changed later TODO cache in a field + final D emptyValue = builder.build(); + + LOG.trace("{}: Reading current attributes", this); + readCurrentAttributes(id, builder, ctx); + + // Need to check whether anything was filled in to determine if data is present or not. + final D built = builder.build(); + final Optional<D> read = built.equals(emptyValue) + ? Optional.absent() + : Optional.of(built); + + LOG.debug("{}: Current node read successfully. Result: {}", this, read); + return read; + } + + @Nonnull + @Override + @SuppressWarnings("unchecked") + public Optional<? extends DataObject> read(@Nonnull final InstanceIdentifier<? extends DataObject> id, + @Nonnull final ReadContext ctx) + throws ReadFailedException { + LOG.trace("{}: Reading : {}", this, id); + checkArgument(id.getTargetType().equals(getManagedDataObjectType().getTargetType())); + return readCurrent((InstanceIdentifier<D>) id, ctx); + } + + @Override + public String toString() { + return String.format("Reader[%s]", getManagedDataObjectType().getTargetType().getSimpleName()); + } +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/BindingBrokerReader.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/BindingBrokerReader.java new file mode 100644 index 000000000..68aa3956e --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/BindingBrokerReader.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.read; + +import com.google.common.base.Optional; +import com.google.common.util.concurrent.CheckedFuture; +import io.fd.honeycomb.v3po.translate.read.ReadContext; +import io.fd.honeycomb.v3po.translate.read.ReadFailedException; +import io.fd.honeycomb.v3po.translate.read.Reader; +import javax.annotation.Nonnull; +import org.opendaylight.controller.md.sal.binding.api.DataBroker; +import org.opendaylight.controller.md.sal.binding.api.ReadOnlyTransaction; +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.yangtools.concepts.Builder; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; + +/** + * Simple DataBroker backed reader allowing to delegate reads to different brokers. + */ +public final class BindingBrokerReader<D extends DataObject, B extends Builder<D>> + implements Reader<D, B>, AutoCloseable { + + private final InstanceIdentifier<D> instanceIdentifier; + private final DataBroker dataBroker; + private final LogicalDatastoreType datastoreType; + private final ReflexiveReaderCustomizer<D, B> reflexiveReaderCustomizer; + + public BindingBrokerReader(final InstanceIdentifier<D> instanceIdentifier, + final DataBroker dataBroker, + final LogicalDatastoreType datastoreType, + final Class<B> builderClass) { + this.reflexiveReaderCustomizer = new ReflexiveReaderCustomizer<>(instanceIdentifier.getTargetType(), builderClass); + this.instanceIdentifier = instanceIdentifier; + this.dataBroker = dataBroker; + this.datastoreType = datastoreType; + } + + @Nonnull + @Override + public Optional<? extends DataObject> read(@Nonnull final InstanceIdentifier<? extends DataObject> id, + @Nonnull final ReadContext ctx) throws ReadFailedException { + try (final ReadOnlyTransaction readOnlyTransaction = dataBroker.newReadOnlyTransaction()) { + final CheckedFuture<? extends Optional<? extends DataObject>, org.opendaylight.controller.md.sal.common.api.data.ReadFailedException> + read = readOnlyTransaction.read(datastoreType, id); + try { + return read.checkedGet(); + } catch (org.opendaylight.controller.md.sal.common.api.data.ReadFailedException e) { + throw new ReadFailedException(id, e); + } + } + } + + @Override + public void merge(@Nonnull final Builder<? extends DataObject> parentBuilder, @Nonnull final D readValue) { + reflexiveReaderCustomizer.merge(parentBuilder, readValue); + } + + @Nonnull + @Override + public B getBuilder(final InstanceIdentifier<D> id) { + return reflexiveReaderCustomizer.getBuilder(id); + } + + @Override + public void readCurrentAttributes(@Nonnull final InstanceIdentifier<D> id, + @Nonnull final B builder, + @Nonnull final ReadContext ctx) throws ReadFailedException { + throw new UnsupportedOperationException("Not supported"); + } + + @Nonnull + @Override + public InstanceIdentifier<D> getManagedDataObjectType() { + return instanceIdentifier; + } + + @Override + public void close() throws Exception { + // Noop + } +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/KeepaliveReaderWrapper.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/KeepaliveReaderWrapper.java new file mode 100644 index 000000000..d782bcc7f --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/KeepaliveReaderWrapper.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.read; + +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import io.fd.honeycomb.v3po.translate.MappingContext; +import io.fd.honeycomb.v3po.translate.ModificationCache; +import io.fd.honeycomb.v3po.translate.read.ReadContext; +import io.fd.honeycomb.v3po.translate.read.ReadFailedException; +import io.fd.honeycomb.v3po.translate.read.Reader; +import java.io.Closeable; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.concepts.Builder; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Reader wrapper that periodically invokes a read to determine whether reads are still fully functional. + * In case a specific error occurs, Keep-alive failure listener gets notified. + */ +public final class KeepaliveReaderWrapper<D extends DataObject, B extends Builder<D>> implements Reader<D, B>, Runnable, Closeable { + + private static final Logger LOG = LoggerFactory.getLogger(KeepaliveReaderWrapper.class); + + private static final NoopReadContext CTX = new NoopReadContext(); + + private final Reader<D, B> delegate; + private final Class<? extends Exception> exceptionType; + private final KeepaliveFailureListener failureListener; + private final ScheduledFuture<?> scheduledFuture; + + /** + * Create new Keepalive wrapper + * + * @param delegate underlying reader performing actual reads + * @param executor scheduled executor service to schedule keepalive calls + * @param exception type of exception used to differentiate keepalive exception from other exceptions + * @param delayInSeconds number of seconds to wait between keepalive calls + * @param failureListener listener to be called whenever a keepalive failure is detected + */ + public KeepaliveReaderWrapper(@Nonnull final Reader<D, B> delegate, + @Nonnull final ScheduledExecutorService executor, + @Nonnull final Class<? extends Exception> exception, + @Nonnegative final int delayInSeconds, + @Nonnull final KeepaliveFailureListener failureListener) { + this.delegate = delegate; + this.exceptionType = exception; + this.failureListener = failureListener; + Preconditions.checkArgument(delayInSeconds > 0, "Delay cannot be < 0"); + LOG.debug("Starting keep-alive execution on top of: {} with delay of: {} seconds", delegate, delayInSeconds); + scheduledFuture = executor.scheduleWithFixedDelay(this, delayInSeconds, delayInSeconds, TimeUnit.SECONDS); + } + + @Nonnull + public Optional<? extends DataObject> read(@Nonnull final InstanceIdentifier id, + @Nonnull final ReadContext ctx) throws ReadFailedException { + return delegate.read(id, ctx); + } + + public void readCurrentAttributes(@Nonnull final InstanceIdentifier<D> id, + @Nonnull final B builder, + @Nonnull final ReadContext ctx) throws ReadFailedException { + delegate.readCurrentAttributes(id, builder, ctx); + } + + @Nonnull + public B getBuilder(final InstanceIdentifier<D> id) { + return delegate.getBuilder(id); + } + + public void merge(@Nonnull final Builder<? extends DataObject> parentBuilder, + @Nonnull final D readValue) { + delegate.merge(parentBuilder, readValue); + } + + @Nonnull + @Override + public InstanceIdentifier<D> getManagedDataObjectType() { + return delegate.getManagedDataObjectType(); + } + + @Override + public void run() { + LOG.trace("Invoking keepalive"); + try { + final Optional<? extends DataObject> read = read(delegate.getManagedDataObjectType(), CTX); + LOG.debug("Keepalive executed successfully with data: {}", read); + } catch (Exception e) { + if (exceptionType.isAssignableFrom(e.getClass())) { + LOG.warn("Keepalive failed. Notifying listener", e); + failureListener.onKeepaliveFailure(); + } + LOG.warn("Keepalive failed unexpectedly", e); + throw new IllegalArgumentException("Unexpected failure during keep-alive execution", e); + } + } + + @Override + public void close() { + // Do not interrupt, it's not our executor + scheduledFuture.cancel(false); + } + + /** + * Listener that gets called whenever keepalive fails as expected + */ + public interface KeepaliveFailureListener { + + void onKeepaliveFailure(); + } + + private static final class NoopMappingContext implements MappingContext { + @Override + public <T extends DataObject> Optional<T> read(@Nonnull final InstanceIdentifier<T> currentId) { + return Optional.absent(); + } + + @Override + public void delete(final InstanceIdentifier<?> path) {} + + @Override + public <T extends DataObject> void merge(final InstanceIdentifier<T> path, final T data) {} + + @Override + public <T extends DataObject> void put(final InstanceIdentifier<T> path, final T data) {} + + @Override + public void close() {} + } + + private static class NoopReadContext implements ReadContext { + + private final ModificationCache modificationCache = new ModificationCache(); + + @Nonnull + @Override + public ModificationCache getModificationCache() { + return modificationCache; + } + + @Nonnull + @Override + public MappingContext getMappingContext() { + return new NoopMappingContext(); + } + + @Override + public void close() { + + } + } +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/NoopReaderCustomizer.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/NoopReaderCustomizer.java new file mode 100644 index 000000000..a4de9febb --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/NoopReaderCustomizer.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.read; + +import io.fd.honeycomb.v3po.translate.read.ReadContext; +import io.fd.honeycomb.v3po.translate.read.ReadFailedException; +import io.fd.honeycomb.v3po.translate.spi.read.ReaderCustomizer; +import org.opendaylight.yangtools.concepts.Builder; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; + +public abstract class NoopReaderCustomizer<C extends DataObject, B extends Builder<C>> implements + ReaderCustomizer<C, B> { + + @Override + public void readCurrentAttributes(InstanceIdentifier<C> id, final B builder, final ReadContext context) throws + ReadFailedException { + // Noop + } +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/ReflexiveListReaderCustomizer.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/ReflexiveListReaderCustomizer.java new file mode 100644 index 000000000..8ad323cc3 --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/ReflexiveListReaderCustomizer.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.read; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Optional; +import io.fd.honeycomb.v3po.translate.spi.read.ListReaderCustomizer; +import io.fd.honeycomb.v3po.translate.util.ReflectionUtils; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.concepts.Builder; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.Identifiable; +import org.opendaylight.yangtools.yang.binding.Identifier; + +/** + * Might be slow ! + */ +public abstract class ReflexiveListReaderCustomizer<C extends DataObject & Identifiable<K>, K extends Identifier<C>, B extends Builder<C>> + extends ReflexiveReaderCustomizer<C, B> + implements ListReaderCustomizer<C, K, B> { + + + public ReflexiveListReaderCustomizer(final Class<C> typeClass, final Class<B> builderClass) { + super(typeClass, builderClass); + } + + @Override + public void merge(@Nonnull final Builder<? extends DataObject> parentBuilder, @Nonnull final C readValue) { + merge(parentBuilder, Collections.singletonList(readValue)); + } + + @Override + public void merge(@Nonnull final Builder<? extends DataObject> parentBuilder, @Nonnull final List<C> readData) { + final Optional<Method> method = + ReflectionUtils.findMethodReflex(parentBuilder.getClass(), "set" + getTypeClass().getSimpleName(), + Collections.singletonList(List.class), parentBuilder.getClass()); + + checkArgument(method.isPresent(), "Unable to set %s to %s", readData, parentBuilder); + + try { + method.get().invoke(parentBuilder, readData); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalArgumentException("Unable to set " + readData + " to " + parentBuilder, e); + } + } +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/ReflexiveReader.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/ReflexiveReader.java new file mode 100644 index 000000000..2b2d9300b --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/ReflexiveReader.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.read; + +import io.fd.honeycomb.v3po.translate.read.ReadContext; +import io.fd.honeycomb.v3po.translate.read.ReadFailedException; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.concepts.Builder; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; + +/** + * Reader that performs no read operation on its own, just fills in the hierarchy. + * <p/> + * Might be slow due to reflection ! + */ +public class ReflexiveReader<C extends DataObject, B extends Builder<C>> extends AbstractGenericReader<C, B> { + + private final ReflexiveReaderCustomizer<C, B> customizer; + + public ReflexiveReader(final InstanceIdentifier<C> identifier, final Class<B> builderClass) { + super(identifier); + this.customizer = new ReflexiveReaderCustomizer<>(identifier.getTargetType(), builderClass); + } + + @Override + public void readCurrentAttributes(@Nonnull final InstanceIdentifier<C> id, @Nonnull final B builder, + @Nonnull final ReadContext ctx) + throws ReadFailedException { + customizer.readCurrentAttributes(id, builder, ctx); + } + + @Nonnull + @Override + public B getBuilder(final InstanceIdentifier<C> id) { + return customizer.getBuilder(id); + } + + @Override + public void merge(@Nonnull final Builder<? extends DataObject> parentBuilder, @Nonnull final C readValue) { + customizer.merge(parentBuilder, readValue); + } +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/ReflexiveReaderCustomizer.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/ReflexiveReaderCustomizer.java new file mode 100644 index 000000000..a6b9bf08e --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/ReflexiveReaderCustomizer.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.read; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Optional; +import com.google.common.collect.Lists; +import io.fd.honeycomb.v3po.translate.util.ReflectionUtils; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.concepts.Builder; +import org.opendaylight.yangtools.yang.binding.Augmentation; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; + +/** + * Might be slow ! + */ +class ReflexiveReaderCustomizer<C extends DataObject, B extends Builder<C>> extends NoopReaderCustomizer<C, B> { + + private final Class<C> typeClass; + private final Class<B> builderClass; + + public ReflexiveReaderCustomizer(final Class<C> typeClass, final Class<B> builderClass) { + this.typeClass = typeClass; + this.builderClass = builderClass; + } + + protected Class<C> getTypeClass() { + return typeClass; + } + + protected Class<B> getBuilderClass() { + return builderClass; + } + + @Nonnull + @Override + public B getBuilder(@Nonnull InstanceIdentifier<C> id) { + try { + return builderClass.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new IllegalStateException("Unable to instantiate " + builderClass, e); + } + } + + @Override + public void merge(@Nonnull final Builder<? extends DataObject> parentBuilder, @Nonnull final C readValue) { + if (Augmentation.class.isAssignableFrom(typeClass)) { + mergeAugmentation(parentBuilder, (Class<? extends Augmentation<?>>) typeClass, readValue); + } else { + mergeRegular(parentBuilder, readValue); + } + } + + private static void mergeRegular(@Nonnull final Builder<? extends DataObject> parentBuilder, + @Nonnull final DataObject readValue) { + final Optional<Method> method = + ReflectionUtils.findMethodReflex(parentBuilder.getClass(), "set", + Collections.singletonList(readValue.getClass()), parentBuilder.getClass()); + + checkArgument(method.isPresent(), "Unable to set %s to %s", readValue, parentBuilder); + + try { + method.get().invoke(parentBuilder, readValue); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalArgumentException("Unable to set " + readValue + " to " + parentBuilder, e); + } + } + + private static void mergeAugmentation(@Nonnull final Builder<? extends DataObject> parentBuilder, + @Nonnull final Class<? extends Augmentation<?>> typeClass, + @Nonnull final DataObject readValue) { + final Optional<Method> method = + ReflectionUtils.findMethodReflex(parentBuilder.getClass(), "addAugmentation", + Lists.newArrayList(Class.class, Augmentation.class), parentBuilder.getClass()); + + checkArgument(method.isPresent(), "Not possible to add augmentations to builder: %s", parentBuilder); + try { + method.get().invoke(parentBuilder, typeClass, readValue); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalArgumentException("Unable to set " + readValue + " to " + parentBuilder, e); + } + } + +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/registry/CompositeReader.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/registry/CompositeReader.java new file mode 100644 index 000000000..aa9b2dc92 --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/registry/CompositeReader.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.read.registry; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import io.fd.honeycomb.v3po.translate.read.ListReader; +import io.fd.honeycomb.v3po.translate.read.ReadContext; +import io.fd.honeycomb.v3po.translate.read.ReadFailedException; +import io.fd.honeycomb.v3po.translate.read.Reader; +import io.fd.honeycomb.v3po.translate.util.RWUtils; +import io.fd.honeycomb.v3po.translate.util.read.AbstractGenericReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.concepts.Builder; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.Identifiable; +import org.opendaylight.yangtools.yang.binding.Identifier; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class CompositeReader<D extends DataObject, B extends Builder<D>> extends AbstractGenericReader<D, B> { + + private static final Logger LOG = LoggerFactory.getLogger(CompositeReader.class); + + private final Reader<D, B> delegate; + private final ImmutableMap<Class<?>, Reader<? extends DataObject, ? extends Builder<?>>> childReaders; + + private CompositeReader(final Reader<D, B> reader, + final ImmutableMap<Class<?>, Reader<? extends DataObject, ? extends Builder<?>>> childReaders) { + super(reader.getManagedDataObjectType()); + this.delegate = reader; + this.childReaders = childReaders; + } + + @VisibleForTesting + ImmutableMap<Class<?>, Reader<? extends DataObject, ? extends Builder<?>>> getChildReaders() { + return childReaders; + } + + @SuppressWarnings("unchecked") + public static <D extends DataObject> InstanceIdentifier<D> appendTypeToId( + final InstanceIdentifier<? extends DataObject> parentId, final InstanceIdentifier<D> type) { + final InstanceIdentifier.PathArgument t = new InstanceIdentifier.Item<>(type.getTargetType()); + return (InstanceIdentifier<D>) InstanceIdentifier.create(Iterables.concat( + parentId.getPathArguments(), Collections.singleton(t))); + } + + @Nonnull + @Override + public Optional<? extends DataObject> read(@Nonnull final InstanceIdentifier<? extends DataObject> id, + @Nonnull final ReadContext ctx) throws ReadFailedException { + if (shouldReadCurrent(id)) { + LOG.trace("{}: Reading current: {}", this, id); + return readCurrent((InstanceIdentifier<D>) id, ctx); + } else if (shouldDelegateToChild(id)) { + LOG.trace("{}: Reading child: {}", this, id); + return readSubtree(id, ctx); + } else { + // Fallback + LOG.trace("{}: Delegating read: {}", this, id); + return delegate.read(id, ctx); + } + } + + private boolean shouldReadCurrent(@Nonnull final InstanceIdentifier<? extends DataObject> id) { + return id.getTargetType().equals(getManagedDataObjectType().getTargetType()); + } + + private boolean shouldDelegateToChild(@Nonnull final InstanceIdentifier<? extends DataObject> id) { + return childReaders.containsKey(RWUtils.getNextId(id, getManagedDataObjectType()).getType()); + } + + private Optional<? extends DataObject> readSubtree(final InstanceIdentifier<? extends DataObject> id, + final ReadContext ctx) throws ReadFailedException { + final InstanceIdentifier.PathArgument nextId = RWUtils.getNextId(id, getManagedDataObjectType()); + final Reader<?, ? extends Builder<?>> nextReader = childReaders.get(nextId.getType()); + checkArgument(nextReader != null, "Unable to read: %s. No delegate present, available readers at next level: %s", + id, childReaders.keySet()); + return nextReader.read(id, ctx); + } + + @SuppressWarnings("unchecked") + private void readChildren(final InstanceIdentifier<D> id, @Nonnull final ReadContext ctx, final B builder) + throws ReadFailedException { + LOG.debug("{}: Reading children: {}", this, childReaders.keySet()); + for (Reader child : childReaders.values()) { + final InstanceIdentifier childId = appendTypeToId(id, child.getManagedDataObjectType()); + + LOG.debug("{}: Reading child from: {}", this, child); + if (child instanceof ListReader) { + final List<? extends DataObject> list = ((ListReader) child).readList(childId, ctx); + ((ListReader) child).merge(builder, list); + } else { + final Optional<? extends DataObject> read = child.read(childId, ctx); + if (read.isPresent()) { + child.merge(builder, read.get()); + } + } + } + } + + @Override + public void readCurrentAttributes(@Nonnull final InstanceIdentifier<D> id, @Nonnull final B builder, + @Nonnull final ReadContext ctx) + throws ReadFailedException { + delegate.readCurrentAttributes(id, builder, ctx); + readChildren(id, ctx, builder); + } + + @Nonnull + @Override + public B getBuilder(final InstanceIdentifier<D> id) { + return delegate.getBuilder(id); + } + + @Override + public void merge(@Nonnull final Builder<? extends DataObject> parentBuilder, @Nonnull final D readValue) { + delegate.merge(parentBuilder, readValue); + } + + /** + * Wrap a Reader as a Composite Reader. + */ + static <D extends DataObject, B extends Builder<D>> Reader<D, B> createForReader( + @Nonnull final Reader<D, B> reader, + @Nonnull final ImmutableMap<Class<?>, Reader<?, ? extends Builder<?>>> childReaders) { + + return (reader instanceof ListReader) + ? new CompositeListReader<>((ListReader) reader, childReaders) + : new CompositeReader<>(reader, childReaders); + } + + private static class CompositeListReader<D extends DataObject & Identifiable<K>, B extends Builder<D>, K extends Identifier<D>> + extends CompositeReader<D, B> + implements ListReader<D, K, B> { + + private final ListReader<D, K, B> delegate; + + private CompositeListReader(final ListReader<D, K, B> reader, + final ImmutableMap<Class<?>, Reader<? extends DataObject, ? extends Builder<?>>> childReaders) { + super(reader, childReaders); + this.delegate = reader; + } + + @Nonnull + @Override + public List<D> readList(@Nonnull final InstanceIdentifier<D> id, @Nonnull final ReadContext ctx) + throws ReadFailedException { + LOG.trace("{}: Reading all list entries", this); + final List<K> allIds = delegate.getAllIds(id, ctx); + LOG.debug("{}: Reading list entries for: {}", this, allIds); + + // Override read list in order to perform readCurrent + readChildren here + final ArrayList<D> allEntries = new ArrayList<>(allIds.size()); + for (K key : allIds) { + final InstanceIdentifier.IdentifiableItem<D, K> currentBdItem = RWUtils.getCurrentIdItem(id, key); + final InstanceIdentifier<D> keyedId = RWUtils.replaceLastInId(id, currentBdItem); + final Optional<D> read = readCurrent(keyedId, ctx); + if (read.isPresent()) { + final DataObject singleItem = read.get(); + checkArgument(getManagedDataObjectType().getTargetType().isAssignableFrom(singleItem.getClass())); + allEntries.add(getManagedDataObjectType().getTargetType().cast(singleItem)); + } + } + return allEntries; + } + + @Override + public void merge(@Nonnull final Builder<? extends DataObject> builder, @Nonnull final List<D> readData) { + delegate.merge(builder, readData); + } + + @Override + public List<K> getAllIds(@Nonnull final InstanceIdentifier<D> id, + @Nonnull final ReadContext ctx) throws ReadFailedException { + return delegate.getAllIds(id, ctx); + } + } + +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/registry/CompositeReaderRegistry.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/registry/CompositeReaderRegistry.java new file mode 100644 index 000000000..a9f606ae2 --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/registry/CompositeReaderRegistry.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.read.registry; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; +import com.google.common.collect.Iterables; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Multimap; +import io.fd.honeycomb.v3po.translate.read.ListReader; +import io.fd.honeycomb.v3po.translate.read.ReadContext; +import io.fd.honeycomb.v3po.translate.read.ReadFailedException; +import io.fd.honeycomb.v3po.translate.read.Reader; +import io.fd.honeycomb.v3po.translate.read.registry.ReaderRegistry; +import io.fd.honeycomb.v3po.translate.util.RWUtils; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.concepts.Builder; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Simple reader registry able to perform and aggregated read (ROOT read) on top of all provided readers. Also able to + * delegate a specific read to one of the delegate readers. + * <p/> + * This could serve as a utility to hold & hide all available readers in upper layers. + */ +public final class CompositeReaderRegistry implements ReaderRegistry { + + private static final Logger LOG = LoggerFactory.getLogger(CompositeReaderRegistry.class); + + private final Map<Class<? extends DataObject>, Reader<? extends DataObject, ? extends Builder<?>>> rootReaders; + + /** + * Create new {@link CompositeReaderRegistry}. + * + * @param rootReaders List of delegate readers + */ + public CompositeReaderRegistry(@Nonnull final List<Reader<? extends DataObject, ? extends Builder<?>>> rootReaders) { + this.rootReaders = RWUtils.uniqueLinkedIndex(checkNotNull(rootReaders), RWUtils.MANAGER_CLASS_FUNCTION); + } + + @VisibleForTesting + Map<Class<? extends DataObject>, Reader<? extends DataObject, ? extends Builder<?>>> getRootReaders() { + return rootReaders; + } + + @Override + @Nonnull + public Multimap<InstanceIdentifier<? extends DataObject>, ? extends DataObject> readAll( + @Nonnull final ReadContext ctx) throws ReadFailedException { + + LOG.debug("Reading from all delegates: {}", this); + LOG.trace("Reading from all delegates: {}", rootReaders.values()); + + final Multimap<InstanceIdentifier<? extends DataObject>, DataObject> objects = LinkedListMultimap.create(); + for (Reader<? extends DataObject, ? extends Builder<?>> rootReader : rootReaders.values()) { + LOG.debug("Reading from delegate: {}", rootReader); + + if (rootReader instanceof ListReader) { + final List<? extends DataObject> listEntries = + ((ListReader) rootReader).readList(rootReader.getManagedDataObjectType(), ctx); + if (!listEntries.isEmpty()) { + objects.putAll(rootReader.getManagedDataObjectType(), listEntries); + } + } else { + final Optional<? extends DataObject> read = rootReader.read(rootReader.getManagedDataObjectType(), ctx); + if (read.isPresent()) { + objects.putAll(rootReader.getManagedDataObjectType(), Collections.singletonList(read.get())); + } + } + } + + return objects; + } + + @Nonnull + @Override + public Optional<? extends DataObject> read(@Nonnull final InstanceIdentifier<? extends DataObject> id, + @Nonnull final ReadContext ctx) + throws ReadFailedException { + final InstanceIdentifier.PathArgument first = checkNotNull( + Iterables.getFirst(id.getPathArguments(), null), "Empty id"); + final Reader<? extends DataObject, ? extends Builder<?>> reader = rootReaders.get(first.getType()); + checkNotNull(reader, + "Unable to read %s. Missing reader. Current readers for: %s", id, rootReaders.keySet()); + LOG.debug("Reading from delegate: {}", reader); + return reader.read(id, ctx); + } + + @Override + public String toString() { + return getClass().getSimpleName() + + rootReaders.keySet().stream().map(Class::getSimpleName).collect(Collectors.toList()); + } +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/registry/CompositeReaderRegistryBuilder.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/registry/CompositeReaderRegistryBuilder.java new file mode 100644 index 000000000..3adda713d --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/registry/CompositeReaderRegistryBuilder.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.read.registry; + +import com.google.common.collect.ImmutableMap; +import io.fd.honeycomb.v3po.translate.read.Reader; +import io.fd.honeycomb.v3po.translate.read.registry.ModifiableReaderRegistryBuilder; +import io.fd.honeycomb.v3po.translate.read.registry.ReaderRegistry; +import io.fd.honeycomb.v3po.translate.read.registry.ReaderRegistryBuilder; +import io.fd.honeycomb.v3po.translate.util.AbstractSubtreeManagerRegistryBuilderBuilder; +import io.fd.honeycomb.v3po.translate.util.read.ReflexiveReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.NotThreadSafe; +import org.opendaylight.yangtools.concepts.Builder; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@NotThreadSafe +public final class CompositeReaderRegistryBuilder + extends AbstractSubtreeManagerRegistryBuilderBuilder<Reader<? extends DataObject, ? extends Builder<?>>, ReaderRegistry> + implements ModifiableReaderRegistryBuilder, ReaderRegistryBuilder { + + private static final Logger LOG = LoggerFactory.getLogger(CompositeReaderRegistryBuilder.class); + + @Override + protected Reader<? extends DataObject, ? extends Builder<?>> getSubtreeHandler(@Nonnull final Set<InstanceIdentifier<?>> handledChildren, + @Nonnull final Reader<? extends DataObject, ? extends Builder<?>> reader) { + return SubtreeReader.createForReader(handledChildren, reader); + } + + @Override + public <D extends DataObject> void addStructuralReader(@Nonnull InstanceIdentifier<D> id, + @Nonnull Class<? extends Builder<D>> builderType) { + add(new ReflexiveReader<>(id, builderType)); + } + + /** + * Create {@link CompositeReaderRegistry} with Readers ordered according to submitted relationships. + * <p/> + * Note: The ordering only applies between nodes on the same level, inter-level and inter-subtree relationships are + * ignored. + */ + @Override + public ReaderRegistry build() { + ImmutableMap<InstanceIdentifier<?>, Reader<? extends DataObject, ? extends Builder<?>>> mappedReaders = + getMappedHandlers(); + LOG.debug("Building Reader registry with Readers: {}", + mappedReaders.keySet().stream() + .map(InstanceIdentifier::getTargetType) + .map(Class::getSimpleName) + .collect(Collectors.joining(", "))); + + LOG.trace("Building Reader registry with Readers: {}", mappedReaders); + final List<InstanceIdentifier<?>> readerOrder = new ArrayList<>(mappedReaders.keySet()); + + // Wrap readers into composite readers recursively, collect roots and create registry + final TypeHierarchy typeHierarchy = TypeHierarchy.create(mappedReaders.keySet()); + final List<Reader<? extends DataObject, ? extends Builder<?>>> orderedRootReaders = + typeHierarchy.getRoots().stream() + .map(rootId -> toCompositeReader(rootId, mappedReaders, typeHierarchy)) + .collect(Collectors.toList()); + + // We are violating the ordering from mappedReaders, since we are forming a composite structure + // but at least order root writers + orderedRootReaders.sort((reader1, reader2) -> readerOrder.indexOf(reader1.getManagedDataObjectType()) + - readerOrder.indexOf(reader2.getManagedDataObjectType())); + + return new CompositeReaderRegistry(orderedRootReaders); + } + + private Reader<? extends DataObject, ? extends Builder<?>> toCompositeReader( + final InstanceIdentifier<?> instanceIdentifier, + final ImmutableMap<InstanceIdentifier<?>, Reader<? extends DataObject, ? extends Builder<?>>> mappedReaders, + final TypeHierarchy typeHierarchy) { + + // Order child readers according to the mappedReadersCollection + final ImmutableMap.Builder<Class<?>, Reader<?, ? extends Builder<?>>> childReadersMapB = ImmutableMap.builder(); + for (InstanceIdentifier<?> childId : mappedReaders.keySet()) { + if (typeHierarchy.getDirectChildren(instanceIdentifier).contains(childId)) { + childReadersMapB.put(childId.getTargetType(), toCompositeReader(childId, mappedReaders, typeHierarchy)); + } + } + + final ImmutableMap<Class<?>, Reader<?, ? extends Builder<?>>> childReadersMap = childReadersMapB.build(); + return childReadersMap.isEmpty() + ? mappedReaders.get(instanceIdentifier) + : CompositeReader.createForReader(mappedReaders.get(instanceIdentifier), childReadersMap); + } +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/registry/SubtreeReader.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/registry/SubtreeReader.java new file mode 100644 index 000000000..50a20656e --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/registry/SubtreeReader.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.read.registry; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import io.fd.honeycomb.v3po.translate.read.ListReader; +import io.fd.honeycomb.v3po.translate.read.ReadContext; +import io.fd.honeycomb.v3po.translate.read.ReadFailedException; +import io.fd.honeycomb.v3po.translate.read.Reader; +import io.fd.honeycomb.v3po.translate.util.RWUtils; +import io.fd.honeycomb.v3po.translate.util.ReflectionUtils; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.opendaylight.yangtools.concepts.Builder; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.Identifiable; +import org.opendaylight.yangtools.yang.binding.Identifier; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Simple Reader delegate for subtree Readers (Readers handling also children nodes) providing a list of all the + * children nodes being handled. + */ +class SubtreeReader<D extends DataObject, B extends Builder<D>> implements Reader<D, B> { + + private static final Logger LOG = LoggerFactory.getLogger(SubtreeReader.class); + + private final Reader<D, B> delegate; + private final Set<InstanceIdentifier<?>> handledChildTypes = new HashSet<>(); + + private SubtreeReader(final Reader<D, B> delegate, Set<InstanceIdentifier<?>> handledTypes) { + this.delegate = delegate; + for (InstanceIdentifier<?> handledType : handledTypes) { + // Iid has to start with Reader's handled root type + checkArgument(delegate.getManagedDataObjectType().getTargetType().equals( + handledType.getPathArguments().iterator().next().getType()), + "Handled node from subtree has to be identified by an instance identifier starting from: %s." + + "Instance identifier was: %s", getManagedDataObjectType().getTargetType(), handledType); + checkArgument(Iterables.size(handledType.getPathArguments()) > 1, + "Handled node from subtree identifier too short: %s", handledType); + handledChildTypes.add(InstanceIdentifier.create(Iterables.concat( + getManagedDataObjectType().getPathArguments(), Iterables.skip(handledType.getPathArguments(), 1)))); + } + } + + /** + * Return set of types also handled by this Reader. All of the types are children of the type managed by this Reader + * excluding the type of this Reader. + */ + Set<InstanceIdentifier<?>> getHandledChildTypes() { + return handledChildTypes; + } + + @Override + @Nonnull + public Optional<? extends DataObject> read( + @Nonnull final InstanceIdentifier<? extends DataObject> id, + @Nonnull final ReadContext ctx) throws ReadFailedException { + final InstanceIdentifier<?> wildcarded = RWUtils.makeIidWildcarded(id); + + // Reading entire subtree and filtering if is current reader responsible + if (getHandledChildTypes().contains(wildcarded)) { + LOG.debug("{}: Subtree node managed by this writer requested: {}. Reading current and filtering", this, id); + // If there's no dedicated reader, use read current + final InstanceIdentifier<D> currentId = RWUtils.cutId(id, getManagedDataObjectType()); + final Optional<? extends DataObject> current = delegate.read(currentId, ctx); + // then perform post-reading filtering (return only requested sub-node) + final Optional<? extends DataObject> readSubtree = current.isPresent() + ? filterSubtree(current.get(), id, getManagedDataObjectType().getTargetType()) + : current; + + LOG.debug("{}: Subtree: {} read successfully. Result: {}", this, id, readSubtree); + return readSubtree; + + // Fallback solution, try delegate, maybe it can read the ID + } else { + return delegate.read(id, ctx); + } + } + + @Override + public void readCurrentAttributes(@Nonnull final InstanceIdentifier<D> id, @Nonnull final B builder, + @Nonnull final ReadContext ctx) + throws ReadFailedException { + delegate.readCurrentAttributes(id, builder, ctx); + } + + @Nonnull + @Override + public B getBuilder(final InstanceIdentifier<D> id) { + return delegate.getBuilder(id); + } + + @Override + public void merge(@Nonnull final Builder<? extends DataObject> parentBuilder, @Nonnull final D readValue) { + delegate.merge(parentBuilder, readValue); + } + + @Nonnull + private static Optional<? extends DataObject> filterSubtree(@Nonnull final DataObject parent, + @Nonnull final InstanceIdentifier<? extends DataObject> absolutPath, + @Nonnull final Class<?> managedType) { + final InstanceIdentifier.PathArgument nextId = + RWUtils.getNextId(absolutPath, InstanceIdentifier.create(parent.getClass())); + + final Optional<? extends DataObject> nextParent = findNextParent(parent, nextId, managedType); + + if (Iterables.getLast(absolutPath.getPathArguments()).equals(nextId)) { + return nextParent; // we found the dataObject identified by absolutePath + } else if (nextParent.isPresent()) { + return filterSubtree(nextParent.get(), absolutPath, nextId.getType()); + } else { + return nextParent; // we can't go further, return Optional.absent() + } + } + + private static Optional<? extends DataObject> findNextParent(@Nonnull final DataObject parent, + @Nonnull final InstanceIdentifier.PathArgument nextId, + @Nonnull final Class<?> managedType) { + // TODO is there a better way than reflection ? e.g. convert into NN and filter out with a utility + Optional<Method> method = ReflectionUtils.findMethodReflex(managedType, "get", + Collections.emptyList(), nextId.getType()); + + if (method.isPresent()) { + return Optional.fromNullable(filterSingle(parent, nextId, method.get())); + } else { + // List child nodes + method = ReflectionUtils.findMethodReflex(managedType, + "get" + nextId.getType().getSimpleName(), Collections.emptyList(), List.class); + + if (method.isPresent()) { + return filterList(parent, nextId, method.get()); + } else { + throw new IllegalStateException( + "Unable to filter " + nextId + " from " + parent + " getters not found using reflexion"); + } + } + } + + @SuppressWarnings("unchecked") + private static Optional<? extends DataObject> filterList(final DataObject parent, + final InstanceIdentifier.PathArgument nextId, + final Method method) { + final List<? extends DataObject> invoke = (List<? extends DataObject>) invoke(method, nextId, parent); + + checkArgument(nextId instanceof InstanceIdentifier.IdentifiableItem<?, ?>, + "Unable to perform wildcarded read for %s", nextId); + final Identifier key = ((InstanceIdentifier.IdentifiableItem) nextId).getKey(); + // TODO replace with stream().filter().findFirst() when we switch to using java's Optional instead of Guava's + // because now we would have to do awkward Optional transformation since findFirstReturns guava's optional + return Iterables.tryFind(invoke, new Predicate<DataObject>() { + + @Override + public boolean apply(@Nullable final DataObject input) { + final Optional<Method> keyGetter = ReflectionUtils.findMethodReflex(nextId.getType(), "get", + Collections.emptyList(), key.getClass()); + final Object actualKey; + actualKey = invoke(keyGetter.get(), nextId, input); + return key.equals(actualKey); + } + }); + } + + private static DataObject filterSingle(final DataObject parent, + final InstanceIdentifier.PathArgument nextId, final Method method) { + return nextId.getType().cast(invoke(method, nextId, parent)); + } + + private static Object invoke(final Method method, + final InstanceIdentifier.PathArgument nextId, final DataObject parent) { + try { + return method.invoke(parent); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalArgumentException("Unable to get " + nextId + " from " + parent, e); + } + } + + @Override + @Nonnull + public InstanceIdentifier<D> getManagedDataObjectType() { + return delegate.getManagedDataObjectType(); + } + + /** + * Wrap a Reader as a subtree Reader. + */ + static <D extends DataObject, B extends Builder<D>> Reader<D, B> createForReader(@Nonnull final Set<InstanceIdentifier<?>> handledChildren, + @Nonnull final Reader<D, B> reader) { + return (reader instanceof ListReader) + ? new SubtreeListReader<>((ListReader) reader, handledChildren) + : new SubtreeReader<>(reader, handledChildren); + } + + private static final class SubtreeListReader<D extends DataObject & Identifiable<K>, B extends Builder<D>, K extends Identifier<D>> + extends SubtreeReader<D, B> implements ListReader<D, K, B> { + + private final ListReader<D, K, B> delegate; + + private SubtreeListReader(final ListReader<D, K, B> delegate, + final Set<InstanceIdentifier<?>> handledTypes) { + super(delegate, handledTypes); + this.delegate = delegate; + } + + @Nonnull + @Override + public List<D> readList(@Nonnull final InstanceIdentifier<D> id, @Nonnull final ReadContext ctx) + throws ReadFailedException { + return delegate.readList(id, ctx); + } + + @Override + public void merge(@Nonnull final Builder<? extends DataObject> builder, @Nonnull final List<D> readData) { + delegate.merge(builder, readData); + } + + @Override + public List<K> getAllIds(@Nonnull final InstanceIdentifier<D> id, + @Nonnull final ReadContext ctx) throws ReadFailedException { + return delegate.getAllIds(id, ctx); + } + } + +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/registry/TypeHierarchy.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/registry/TypeHierarchy.java new file mode 100644 index 000000000..005e3bc8d --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/read/registry/TypeHierarchy.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.read.registry; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.collect.Iterables; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import org.jgrapht.experimental.dag.DirectedAcyclicGraph; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; + +final class TypeHierarchy { + private final DirectedAcyclicGraph<InstanceIdentifier<?>, Parent> hierarchy; + + private TypeHierarchy(@Nonnull final DirectedAcyclicGraph<InstanceIdentifier<?>, Parent> hierarchy) { + this.hierarchy = hierarchy; + } + + Set<InstanceIdentifier<?>> getAllChildren(InstanceIdentifier<?> id) { + final HashSet<InstanceIdentifier<?>> instanceIdentifiers = new HashSet<>(); + for (InstanceIdentifier<?> childId : getDirectChildren(id)) { + instanceIdentifiers.add(childId); + instanceIdentifiers.addAll(getAllChildren(childId)); + } + return instanceIdentifiers; + } + + Set<InstanceIdentifier<?>> getDirectChildren(InstanceIdentifier<?> id) { + checkArgument(hierarchy.vertexSet().contains(id), + "Unknown reader: %s. Known readers: %s", id, hierarchy.vertexSet()); + + return hierarchy.outgoingEdgesOf(id).stream() + .map(hierarchy::getEdgeTarget) + .collect(Collectors.toSet()); + } + + Set<InstanceIdentifier<?>> getRoots() { + return hierarchy.vertexSet().stream() + .filter(vertex -> hierarchy.incomingEdgesOf(vertex).size() == 0) + .collect(Collectors.toSet()); + } + + /** + * Create reader hierarchy from a flat set of instance identifiers. + * + * @param allIds Set of unkeyed instance identifiers + */ + static TypeHierarchy create(@Nonnull Set<InstanceIdentifier<?>> allIds) { + final DirectedAcyclicGraph<InstanceIdentifier<?>, Parent> + readersHierarchy = new DirectedAcyclicGraph<>((sourceVertex, targetVertex) -> new Parent()); + + for (InstanceIdentifier<?> allId : allIds) { + checkArgument(!Iterables.isEmpty(allId.getPathArguments()), "Empty ID detected"); + + if (Iterables.size(allId.getPathArguments()) == 1) { + readersHierarchy.addVertex(allId); + } + + List<InstanceIdentifier.PathArgument> pathArgs = new LinkedList<>(); + pathArgs.add(allId.getPathArguments().iterator().next()); + + for (InstanceIdentifier.PathArgument pathArgument : Iterables.skip(allId.getPathArguments(), 1)) { + final InstanceIdentifier<?> previous = InstanceIdentifier.create(pathArgs); + pathArgs.add(pathArgument); + final InstanceIdentifier<?> current = InstanceIdentifier.create(pathArgs); + + readersHierarchy.addVertex(previous); + readersHierarchy.addVertex(current); + + try { + readersHierarchy.addDagEdge(previous, current); + } catch (DirectedAcyclicGraph.CycleFoundException e) { + throw new IllegalArgumentException("Loop in hierarchy detected", e); + } + } + } + + return new TypeHierarchy(readersHierarchy); + } + + private static final class Parent{} +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/AbstractGenericWriter.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/AbstractGenericWriter.java new file mode 100644 index 000000000..44b36edae --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/AbstractGenericWriter.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.write; + +import static com.google.common.base.Preconditions.checkArgument; + +import io.fd.honeycomb.v3po.translate.util.RWUtils; +import io.fd.honeycomb.v3po.translate.write.WriteContext; +import io.fd.honeycomb.v3po.translate.write.WriteFailedException; +import io.fd.honeycomb.v3po.translate.write.Writer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractGenericWriter<D extends DataObject> implements Writer<D> { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractGenericWriter.class); + + private final InstanceIdentifier<D> instanceIdentifier; + + protected AbstractGenericWriter(final InstanceIdentifier<D> type) { + this.instanceIdentifier = RWUtils.makeIidWildcarded(type); + } + + protected void writeCurrent(final InstanceIdentifier<D> id, final D data, final WriteContext ctx) + throws WriteFailedException { + LOG.debug("{}: Writing current: {} data: {}", this, id, data); + writeCurrentAttributes(id, data, ctx); + LOG.debug("{}: Current node written successfully", this); + } + + protected void updateCurrent(final InstanceIdentifier<D> id, final D dataBefore, final D dataAfter, + final WriteContext ctx) throws WriteFailedException { + LOG.debug("{}: Updating current: {} dataBefore: {}, datAfter: {}", this, id, dataBefore, dataAfter); + + if (dataBefore.equals(dataAfter)) { + LOG.debug("{}: Skipping current(no update): {}", this, id); + // No change, ignore + return; + } + updateCurrentAttributes(id, dataBefore, dataAfter, ctx); + LOG.debug("{}: Current node updated successfully", this); + } + + protected void deleteCurrent(final InstanceIdentifier<D> id, final D dataBefore, final WriteContext ctx) + throws WriteFailedException { + LOG.debug("{}: Deleting current: {} dataBefore: {}", this, id, dataBefore); + deleteCurrentAttributes(id, dataBefore, ctx); + } + + @SuppressWarnings("unchecked") + @Override + public void update(@Nonnull final InstanceIdentifier<? extends DataObject> id, + @Nullable final DataObject dataBefore, + @Nullable final DataObject dataAfter, + @Nonnull final WriteContext ctx) throws WriteFailedException { + LOG.debug("{}: Updating : {}", this, id); + LOG.trace("{}: Updating : {}, from: {} to: {}", this, id, dataBefore, dataAfter); + + checkArgument(idPointsToCurrent(id), "Cannot handle data: %s. Only: %s can be handled by writer: %s", + id, getManagedDataObjectType(), this); + + if (isWrite(dataBefore, dataAfter)) { + writeCurrent((InstanceIdentifier<D>) id, castToManaged(dataAfter), ctx); + } else if (isDelete(dataBefore, dataAfter)) { + deleteCurrent((InstanceIdentifier<D>) id, castToManaged(dataBefore), ctx); + } else { + checkArgument(dataBefore != null && dataAfter != null, "No data to process"); + updateCurrent((InstanceIdentifier<D>) id, castToManaged(dataBefore), castToManaged(dataAfter), ctx); + } + } + + private void checkDataType(@Nonnull final DataObject dataAfter) { + checkArgument(getManagedDataObjectType().getTargetType().isAssignableFrom(dataAfter.getClass())); + } + + private D castToManaged(final DataObject data) { + checkDataType(data); + return getManagedDataObjectType().getTargetType().cast(data); + } + + private static boolean isWrite(final DataObject dataBefore, + final DataObject dataAfter) { + return dataBefore == null && dataAfter != null; + } + + private static boolean isDelete(final DataObject dataBefore, + final DataObject dataAfter) { + return dataAfter == null && dataBefore != null; + } + + private boolean idPointsToCurrent(final @Nonnull InstanceIdentifier<? extends DataObject> id) { + return id.getTargetType().equals(getManagedDataObjectType().getTargetType()); + } + + protected abstract void writeCurrentAttributes(@Nonnull final InstanceIdentifier<D> id, + @Nonnull final D data, + @Nonnull final WriteContext ctx) throws WriteFailedException; + + protected abstract void deleteCurrentAttributes(@Nonnull final InstanceIdentifier<D> id, + @Nonnull final D dataBefore, + @Nonnull final WriteContext ctx) throws WriteFailedException; + + protected abstract void updateCurrentAttributes(@Nonnull final InstanceIdentifier<D> id, + @Nonnull final D dataBefore, + @Nonnull final D dataAfter, + @Nonnull final WriteContext ctx) throws WriteFailedException; + + @Nonnull + @Override + public InstanceIdentifier<D> getManagedDataObjectType() { + return instanceIdentifier; + } + + + @Override + public String toString() { + return String.format("Writer[%s]", getManagedDataObjectType().getTargetType().getSimpleName()); + } +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/NoopWriterRegistry.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/NoopWriterRegistry.java new file mode 100644 index 000000000..7c45fcd82 --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/NoopWriterRegistry.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.write; + +import io.fd.honeycomb.v3po.translate.TranslationException; +import io.fd.honeycomb.v3po.translate.write.WriteContext; +import io.fd.honeycomb.v3po.translate.write.registry.WriterRegistry; +import javax.annotation.Nonnull; + +/** + * Empty registry that does not perform any changes. Can be used in data layer, if we want to disable passing data to + * translation layer. + */ +public class NoopWriterRegistry implements WriterRegistry, AutoCloseable { + + @Override + public void update(@Nonnull final DataObjectUpdates updates, + @Nonnull final WriteContext ctx) throws TranslationException { + // NOOP + } + + @Override + public void close() throws Exception { + // NOOP + } +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/TransactionWriteContext.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/TransactionWriteContext.java new file mode 100644 index 000000000..47498f594 --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/TransactionWriteContext.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.write; + +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.base.Optional; +import com.google.common.util.concurrent.CheckedFuture; +import io.fd.honeycomb.v3po.translate.MappingContext; +import io.fd.honeycomb.v3po.translate.ModificationCache; +import io.fd.honeycomb.v3po.translate.write.WriteContext; +import java.util.Map; +import javax.annotation.Nonnull; +import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; +import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException; +import org.opendaylight.controller.md.sal.dom.api.DOMDataReadOnlyTransaction; +import org.opendaylight.yangtools.binding.data.codec.api.BindingNormalizedNodeSerializer; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; + +/** + * Transaction based WriteContext + */ +public final class TransactionWriteContext implements WriteContext { + + private final DOMDataReadOnlyTransaction beforeTx; + private final DOMDataReadOnlyTransaction afterTx; + private final ModificationCache ctx; + private final BindingNormalizedNodeSerializer serializer; + private final MappingContext mappingContext; + + public TransactionWriteContext(final BindingNormalizedNodeSerializer serializer, + final DOMDataReadOnlyTransaction beforeTx, + final DOMDataReadOnlyTransaction afterTx, + final MappingContext mappingContext) { + this.serializer = serializer; + // TODO do we have a BA transaction adapter ? If so, use it here and don't pass serializer + this.beforeTx = beforeTx; + this.afterTx = afterTx; + this.mappingContext = mappingContext; + this.ctx = new ModificationCache(); + } + + // TODO make this asynchronous + + @Override + public <T extends DataObject> Optional<T> readBefore(@Nonnull final InstanceIdentifier<T> currentId) { + return read(currentId, beforeTx); + } + + @Override + public <T extends DataObject> Optional<T> readAfter(@Nonnull final InstanceIdentifier<T> currentId) { + return read(currentId, afterTx); + } + + + private <T extends DataObject> Optional<T> read(final InstanceIdentifier<T> currentId, + final DOMDataReadOnlyTransaction tx) { + final YangInstanceIdentifier path = serializer.toYangInstanceIdentifier(currentId); + + final CheckedFuture<Optional<NormalizedNode<?, ?>>, ReadFailedException> read = + tx.read(LogicalDatastoreType.CONFIGURATION, path); + + try { + // TODO once the APIs are asynchronous use just Futures.transform + final Optional<NormalizedNode<?, ?>> optional = read.checkedGet(); + + if (!optional.isPresent()) { + return Optional.absent(); + } + + final NormalizedNode<?, ?> data = optional.get(); + final Map.Entry<InstanceIdentifier<?>, DataObject> entry = serializer.fromNormalizedNode(path, data); + + final Class<T> targetType = currentId.getTargetType(); + checkState(targetType.isAssignableFrom(entry.getValue().getClass()), + "Unexpected data object type, should be: %s, but was: %s", targetType, entry.getValue().getClass()); + return Optional.of(targetType.cast(entry.getValue())); + } catch (ReadFailedException e) { + throw new IllegalStateException("Unable to perform read", e); + } + } + + @Nonnull + @Override + public ModificationCache getModificationCache() { + return ctx; + } + + @Nonnull + @Override + public MappingContext getMappingContext() { + return mappingContext; + } + + /** + * Does not close the transactions + */ + @Override + public void close() { + ctx.close(); + beforeTx.close(); + afterTx.close(); + } +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/registry/FlatWriterRegistry.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/registry/FlatWriterRegistry.java new file mode 100644 index 000000000..7433de813 --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/registry/FlatWriterRegistry.java @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.write.registry; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Optional; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; +import io.fd.honeycomb.v3po.translate.TranslationException; +import io.fd.honeycomb.v3po.translate.util.RWUtils; +import io.fd.honeycomb.v3po.translate.write.DataObjectUpdate; +import io.fd.honeycomb.v3po.translate.write.WriteContext; +import io.fd.honeycomb.v3po.translate.write.WriteFailedException; +import io.fd.honeycomb.v3po.translate.write.Writer; +import io.fd.honeycomb.v3po.translate.write.registry.WriterRegistry; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Flat writer registry, delegating updates to writers in the order writers were submitted. + */ +@ThreadSafe +final class FlatWriterRegistry implements WriterRegistry { + + private static final Logger LOG = LoggerFactory.getLogger(FlatWriterRegistry.class); + + // All types handled by writers directly or as children + private final ImmutableSet<InstanceIdentifier<?>> handledTypes; + + private final Set<InstanceIdentifier<?>> writersOrderReversed; + private final Set<InstanceIdentifier<?>> writersOrder; + private final Map<InstanceIdentifier<?>, Writer<?>> writers; + + /** + * Create flat registry instance. + * + * @param writers immutable, ordered map of writers to use to process updates. Order of the writers has to be + * one in which create and update operations should be handled. Deletes will be handled in reversed + * order. All deletes are handled before handling all the updates. + */ + FlatWriterRegistry(@Nonnull final ImmutableMap<InstanceIdentifier<?>, Writer<?>> writers) { + this.writers = writers; + this.writersOrderReversed = Sets.newLinkedHashSet(Lists.reverse(Lists.newArrayList(writers.keySet()))); + this.writersOrder = writers.keySet(); + this.handledTypes = getAllHandledTypes(writers); + } + + private static ImmutableSet<InstanceIdentifier<?>> getAllHandledTypes( + @Nonnull final ImmutableMap<InstanceIdentifier<?>, Writer<?>> writers) { + final ImmutableSet.Builder<InstanceIdentifier<?>> handledTypesBuilder = ImmutableSet.builder(); + for (Map.Entry<InstanceIdentifier<?>, Writer<?>> writerEntry : writers.entrySet()) { + final InstanceIdentifier<?> writerType = writerEntry.getKey(); + final Writer<?> writer = writerEntry.getValue(); + handledTypesBuilder.add(writerType); + if (writer instanceof SubtreeWriter) { + handledTypesBuilder.addAll(((SubtreeWriter<?>) writer).getHandledChildTypes()); + } + } + return handledTypesBuilder.build(); + } + + @Override + public void update(@Nonnull final DataObjectUpdates updates, + @Nonnull final WriteContext ctx) throws TranslationException { + if (updates.isEmpty()) { + return; + } + + // Optimization + if (updates.containsOnlySingleType()) { + // First process delete + singleUpdate(updates.getDeletes(), ctx); + // Next is update + singleUpdate(updates.getUpdates(), ctx); + } else { + // First process deletes + bulkUpdate(updates.getDeletes(), ctx, true, writersOrderReversed); + // Next are updates + bulkUpdate(updates.getUpdates(), ctx, true, writersOrder); + } + + LOG.debug("Update successful for types: {}", updates.getTypeIntersection()); + LOG.trace("Update successful for: {}", updates); + } + + private void singleUpdate(@Nonnull final Multimap<InstanceIdentifier<?>, ? extends DataObjectUpdate> updates, + @Nonnull final WriteContext ctx) throws WriteFailedException { + if (updates.isEmpty()) { + return; + } + + final InstanceIdentifier<?> singleType = updates.keySet().iterator().next(); + LOG.debug("Performing single type update for: {}", singleType); + Collection<? extends DataObjectUpdate> singleTypeUpdates = updates.get(singleType); + Writer<?> writer = getWriter(singleType); + + if (writer == null) { + // This node must be handled by a subtree writer, find it and call it or else fail + checkArgument(handledTypes.contains(singleType), "Unable to process update. Missing writers for: %s", + singleType); + writer = getSubtreeWriterResponsible(singleType); + singleTypeUpdates = getParentDataObjectUpdate(ctx, updates, writer); + } + + LOG.trace("Performing single type update with writer: {}", writer); + for (DataObjectUpdate singleUpdate : singleTypeUpdates) { + writer.update(singleUpdate.getId(), singleUpdate.getDataBefore(), singleUpdate.getDataAfter(), ctx); + } + } + + private Writer<?> getSubtreeWriterResponsible(final InstanceIdentifier<?> singleType) { + final Writer<?> writer;// This is slow ( minor TODO-perf ) + writer = writers.values().stream() + .filter(w -> w instanceof SubtreeWriter) + .filter(w -> ((SubtreeWriter<?>) w).getHandledChildTypes().contains(singleType)) + .findFirst() + .get(); + return writer; + } + + private Collection<DataObjectUpdate> getParentDataObjectUpdate(final WriteContext ctx, + final Multimap<InstanceIdentifier<?>, ? extends DataObjectUpdate> updates, + final Writer<?> writer) { + // Now read data for subtree reader root, but first keyed ID is needed and that ID can be cut from updates + InstanceIdentifier<?> firstAffectedChildId = ((SubtreeWriter<?>) writer).getHandledChildTypes().stream() + .filter(updates::containsKey) + .map(unkeyedId -> updates.get(unkeyedId)) + .flatMap(doUpdates -> doUpdates.stream()) + .map(DataObjectUpdate::getId) + .findFirst() + .get(); + + final InstanceIdentifier<?> parentKeyedId = + RWUtils.cutId(firstAffectedChildId, writer.getManagedDataObjectType()); + + final Optional<? extends DataObject> parentBefore = ctx.readBefore(parentKeyedId); + final Optional<? extends DataObject> parentAfter = ctx.readAfter(parentKeyedId); + return Collections.singleton( + DataObjectUpdate.create(parentKeyedId, parentBefore.orNull(), parentAfter.orNull())); + } + + private void bulkUpdate(@Nonnull final Multimap<InstanceIdentifier<?>, ? extends DataObjectUpdate> updates, + @Nonnull final WriteContext ctx, + final boolean attemptRevert, + @Nonnull final Set<InstanceIdentifier<?>> writersOrder) throws BulkUpdateException { + if (updates.isEmpty()) { + return; + } + + LOG.debug("Performing bulk update with revert attempt: {} for: {}", attemptRevert, updates.keySet()); + + // Check that all updates can be handled + checkAllTypesCanBeHandled(updates); + + // Capture all changes successfully processed in case revert is needed + final Set<InstanceIdentifier<?>> processedNodes = new HashSet<>(); + + // Iterate over all writers and call update if there are any related updates + for (InstanceIdentifier<?> writerType : writersOrder) { + Collection<? extends DataObjectUpdate> writersData = updates.get(writerType); + final Writer<?> writer = getWriter(writerType); + + if (writersData.isEmpty()) { + // If there are no data for current writer, but it is a SubtreeWriter and there are updates to + // its children, still invoke it with its root data + if (writer instanceof SubtreeWriter<?> && isAffected(((SubtreeWriter<?>) writer), updates)) { + // Provide parent data for SubtreeWriter for further processing + writersData = getParentDataObjectUpdate(ctx, updates, writer); + } else { + // Skipping unaffected writer + // Alternative to this would be modification sort according to the order of writers + continue; + } + } + + LOG.debug("Performing update for: {}", writerType); + LOG.trace("Performing update with writer: {}", writer); + + for (DataObjectUpdate singleUpdate : writersData) { + try { + writer.update(singleUpdate.getId(), singleUpdate.getDataBefore(), singleUpdate.getDataAfter(), ctx); + processedNodes.add(singleUpdate.getId()); + LOG.trace("Update successful for type: {}", writerType); + LOG.debug("Update successful for: {}", singleUpdate); + } catch (Exception e) { + LOG.error("Error while processing data change of: {} (updates={})", writerType, writersData, e); + + final Reverter reverter = attemptRevert + ? new ReverterImpl(processedNodes, updates, writersOrder, ctx) + : () -> {}; // NOOP reverter + + // Find out which changes left unprocessed + final Set<InstanceIdentifier<?>> unprocessedChanges = updates.values().stream() + .map(DataObjectUpdate::getId) + .filter(id -> !processedNodes.contains(id)) + .collect(Collectors.toSet()); + throw new BulkUpdateException(unprocessedChanges, reverter, e); + } + } + } + } + + private void checkAllTypesCanBeHandled( + @Nonnull final Multimap<InstanceIdentifier<?>, ? extends DataObjectUpdate> updates) { + if (!handledTypes.containsAll(updates.keySet())) { + final Sets.SetView<InstanceIdentifier<?>> missingWriters = Sets.difference(updates.keySet(), handledTypes); + LOG.warn("Unable to process update. Missing writers for: {}", missingWriters); + throw new IllegalArgumentException("Unable to process update. Missing writers for: " + missingWriters); + } + } + + /** + * Check whether {@link SubtreeWriter} is affected by the updates. + * + * @return true if there are any updates to SubtreeWriter's child nodes (those marked by SubtreeWriter + * as being taken care of) + * */ + private static boolean isAffected(final SubtreeWriter<?> writer, + final Multimap<InstanceIdentifier<?>, ? extends DataObjectUpdate> updates) { + return !Sets.intersection(writer.getHandledChildTypes(), updates.keySet()).isEmpty(); + } + + @Nullable + private Writer<?> getWriter(@Nonnull final InstanceIdentifier<?> singleType) { + return writers.get(singleType); + } + + // FIXME unit test + private final class ReverterImpl implements Reverter { + + private final Collection<InstanceIdentifier<?>> processedNodes; + private final Multimap<InstanceIdentifier<?>, ? extends DataObjectUpdate> updates; + private final Set<InstanceIdentifier<?>> revertDeleteOrder; + private final WriteContext ctx; + + ReverterImpl(final Collection<InstanceIdentifier<?>> processedNodes, + final Multimap<InstanceIdentifier<?>, ? extends DataObjectUpdate> updates, + final Set<InstanceIdentifier<?>> writersOrderOriginal, + final WriteContext ctx) { + this.processedNodes = processedNodes; + this.updates = updates; + // Use opposite ordering when executing revert + this.revertDeleteOrder = writersOrderOriginal == FlatWriterRegistry.this.writersOrder + ? FlatWriterRegistry.this.writersOrderReversed + : FlatWriterRegistry.this.writersOrder; + this.ctx = ctx; + } + + @Override + public void revert() throws RevertFailedException { + Multimap<InstanceIdentifier<?>, DataObjectUpdate> updatesToRevert = + filterAndRevertProcessed(updates, processedNodes); + + LOG.info("Attempting revert for changes: {}", updatesToRevert); + try { + // Perform reversed bulk update without revert attempt + bulkUpdate(updatesToRevert, ctx, true, revertDeleteOrder); + LOG.info("Revert successful"); + } catch (BulkUpdateException e) { + LOG.error("Revert failed", e); + throw new RevertFailedException(e.getFailedIds(), e); + } + } + + /** + * Create new updates map, but only keep already processed changes. Switching before and after data for each + * update. + */ + private Multimap<InstanceIdentifier<?>, DataObjectUpdate> filterAndRevertProcessed( + final Multimap<InstanceIdentifier<?>, ? extends DataObjectUpdate> updates, + final Collection<InstanceIdentifier<?>> processedNodes) { + final Multimap<InstanceIdentifier<?>, DataObjectUpdate> filtered = HashMultimap.create(); + for (InstanceIdentifier<?> processedNode : processedNodes) { + final InstanceIdentifier<?> wildcardedIid = RWUtils.makeIidWildcarded(processedNode); + if (updates.containsKey(wildcardedIid)) { + updates.get(wildcardedIid).stream() + .filter(dataObjectUpdate -> processedNode.contains(dataObjectUpdate.getId())) + .forEach(dataObjectUpdate -> filtered.put(processedNode, dataObjectUpdate.reverse())); + } + } + return filtered; + } + } + +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/registry/FlatWriterRegistryBuilder.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/registry/FlatWriterRegistryBuilder.java new file mode 100644 index 000000000..bfac2eedd --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/registry/FlatWriterRegistryBuilder.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.write.registry; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import io.fd.honeycomb.v3po.translate.util.AbstractSubtreeManagerRegistryBuilderBuilder; +import io.fd.honeycomb.v3po.translate.write.Writer; +import io.fd.honeycomb.v3po.translate.write.registry.ModifiableWriterRegistryBuilder; +import io.fd.honeycomb.v3po.translate.write.registry.WriterRegistry; +import io.fd.honeycomb.v3po.translate.write.registry.WriterRegistryBuilder; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.NotThreadSafe; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Builder for {@link FlatWriterRegistry} allowing users to specify inter-writer relationships. + */ +@NotThreadSafe +public final class FlatWriterRegistryBuilder + extends AbstractSubtreeManagerRegistryBuilderBuilder<Writer<? extends DataObject>, WriterRegistry> + implements ModifiableWriterRegistryBuilder, WriterRegistryBuilder { + + private static final Logger LOG = LoggerFactory.getLogger(FlatWriterRegistryBuilder.class); + + @Override + protected Writer<? extends DataObject> getSubtreeHandler(final @Nonnull Set<InstanceIdentifier<?>> handledChildren, + final @Nonnull Writer<? extends DataObject> writer) { + return SubtreeWriter.createForWriter(handledChildren, writer); + } + + /** + * Create FlatWriterRegistry with writers ordered according to submitted relationships. + */ + @Override + public WriterRegistry build() { + final ImmutableMap<InstanceIdentifier<?>, Writer<?>> mappedWriters = getMappedHandlers(); + LOG.debug("Building writer registry with writers: {}", + mappedWriters.keySet().stream() + .map(InstanceIdentifier::getTargetType) + .map(Class::getSimpleName) + .collect(Collectors.joining(", "))); + LOG.trace("Building writer registry with writers: {}", mappedWriters); + return new FlatWriterRegistry(mappedWriters); + } + + @VisibleForTesting + @Override + protected ImmutableMap<InstanceIdentifier<?>, Writer<? extends DataObject>> getMappedHandlers() { + return super.getMappedHandlers(); + } +} diff --git a/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/registry/SubtreeWriter.java b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/registry/SubtreeWriter.java new file mode 100644 index 000000000..e395b29da --- /dev/null +++ b/infra/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/registry/SubtreeWriter.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.translate.util.write.registry; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.collect.Iterables; +import io.fd.honeycomb.v3po.translate.write.WriteContext; +import io.fd.honeycomb.v3po.translate.write.WriteFailedException; +import io.fd.honeycomb.v3po.translate.write.Writer; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.opendaylight.yangtools.yang.binding.DataObject; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; + +/** + * Simple writer delegate for subtree writers (writers handling also children nodes) providing a list of all the + * children nodes being handled. + */ +final class SubtreeWriter<D extends DataObject> implements Writer<D> { + + private final Writer<D> delegate; + private final Set<InstanceIdentifier<?>> handledChildTypes = new HashSet<>(); + + private SubtreeWriter(final Writer<D> delegate, Set<InstanceIdentifier<?>> handledTypes) { + this.delegate = delegate; + for (InstanceIdentifier<?> handledType : handledTypes) { + // Iid has to start with writer's handled root type + checkArgument(delegate.getManagedDataObjectType().getTargetType().equals( + handledType.getPathArguments().iterator().next().getType()), + "Handled node from subtree has to be identified by an instance identifier starting from: %s." + + "Instance identifier was: %s", getManagedDataObjectType().getTargetType(), handledType); + checkArgument(Iterables.size(handledType.getPathArguments()) > 1, + "Handled node from subtree identifier too short: %s", handledType); + handledChildTypes.add(InstanceIdentifier.create(Iterables.concat( + getManagedDataObjectType().getPathArguments(), Iterables.skip(handledType.getPathArguments(), 1)))); + } + } + + /** + * Return set of types also handled by this writer. All of the types are children of the type managed by this + * writer excluding the type of this writer. + */ + Set<InstanceIdentifier<?>> getHandledChildTypes() { + return handledChildTypes; + } + + @Override + public void update( + @Nonnull final InstanceIdentifier<? extends DataObject> id, + @Nullable final DataObject dataBefore, + @Nullable final DataObject dataAfter, @Nonnull final WriteContext ctx) throws WriteFailedException { + delegate.update(id, dataBefore, dataAfter, ctx); + } + + @Override + @Nonnull + public InstanceIdentifier<D> getManagedDataObjectType() { + return delegate.getManagedDataObjectType(); + } + + /** + * Wrap a writer as a subtree writer. + */ + static Writer<?> createForWriter(@Nonnull final Set<InstanceIdentifier<?>> handledChildren, + @Nonnull final Writer<? extends DataObject> writer) { + return new SubtreeWriter<>(writer, handledChildren); + } +} |