BUG-1041 cli proposal #1
[controller.git] / opendaylight / netconf / netconf-cli / src / main / java / org / opendaylight / controller / netconf / cli / reader / custom / ConfigReader.java
diff --git a/opendaylight/netconf/netconf-cli/src/main/java/org/opendaylight/controller/netconf/cli/reader/custom/ConfigReader.java b/opendaylight/netconf/netconf-cli/src/main/java/org/opendaylight/controller/netconf/cli/reader/custom/ConfigReader.java
new file mode 100644 (file)
index 0000000..95fc098
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.controller.netconf.cli.reader.custom;
+
+import static org.opendaylight.controller.netconf.cli.io.IOUtil.isSkipInput;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import jline.console.completer.Completer;
+import org.opendaylight.controller.netconf.cli.CommandArgHandlerRegistry;
+import org.opendaylight.controller.netconf.cli.io.BaseConsoleContext;
+import org.opendaylight.controller.netconf.cli.io.ConsoleContext;
+import org.opendaylight.controller.netconf.cli.io.ConsoleIO;
+import org.opendaylight.controller.netconf.cli.io.IOUtil;
+import org.opendaylight.controller.netconf.cli.reader.AbstractReader;
+import org.opendaylight.controller.netconf.cli.reader.ReadingException;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.Node;
+import org.opendaylight.yangtools.yang.data.impl.CompositeNodeTOImpl;
+import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
+import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.Module;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+
+/**
+ * Custom reader implementation for filter elements in get/get-config rpcs. This
+ * reader overrides the default anyxml reader and reads filter as a schema path.
+ */
+public class ConfigReader extends AbstractReader<DataSchemaNode> {
+
+    public static final String SEPARATOR = "/";
+
+    private final CommandArgHandlerRegistry commandArgHandlerRegistry;
+    private final Map<String, QName> mappedModules;
+    private final Map<URI, QName> mappedModulesNamespace;
+
+    public ConfigReader(final ConsoleIO console, final SchemaContext remoteSchemaContext,
+            final CommandArgHandlerRegistry commandArgHandlerRegistry) {
+        super(console, remoteSchemaContext);
+        this.commandArgHandlerRegistry = commandArgHandlerRegistry;
+
+        mappedModules = Maps.newHashMap();
+        mappedModulesNamespace = Maps.newHashMap();
+        for (final Module module : remoteSchemaContext.getModules()) {
+            final QName moduleQName = QName.create(module.getNamespace(), module.getRevision(), module.getName());
+            mappedModules.put(moduleQName.getLocalName(), moduleQName);
+            mappedModulesNamespace.put(moduleQName.getNamespace(), moduleQName);
+        }
+    }
+
+    // FIXME refactor + unite common code with FilterReader
+
+    @Override
+    protected List<Node<?>> readWithContext(final DataSchemaNode schemaNode) throws IOException, ReadingException {
+        console.writeLn("Config " + schemaNode.getQName().getLocalName());
+        console.writeLn("Submit path of the data to edit. Use TAB for autocomplete");
+
+        final String rawValue = console.read();
+
+        // FIXME isSkip check should be somewhere in abstractReader
+        if (isSkipInput(rawValue) || Strings.isNullOrEmpty(rawValue)) {
+            return Collections.emptyList();
+        }
+
+        final List<QName> filterPartsQNames = Lists.newArrayList();
+
+        for (final String part : rawValue.split(SEPARATOR)) {
+            final QName qName = IOUtil.qNameFromKeyString(part, mappedModules);
+            filterPartsQNames.add(qName);
+        }
+
+        List<Node<?>> previous = readInnerNode(rawValue);
+
+        for (final QName qName : Lists.reverse(filterPartsQNames).subList(1, filterPartsQNames.size())) {
+            previous = Collections.<Node<?>> singletonList(new CompositeNodeTOImpl(qName, null,
+                    previous == null ? Collections.<Node<?>> emptyList() : previous));
+        }
+
+        final Node<?> newNode = previous == null ? null
+                : new CompositeNodeTOImpl(schemaNode.getQName(), null, previous);
+
+        return Collections.<Node<?>> singletonList(newNode);
+    }
+
+    private List<Node<?>> readInnerNode(final String pathString) throws ReadingException {
+        final Optional<DataSchemaNode> schema = getCurrentNode(getSchemaContext(), pathString);
+        Preconditions.checkState(schema.isPresent(), "Unable to find schema for %s", pathString);
+        return commandArgHandlerRegistry.getGenericReader(getSchemaContext(), true).read(schema.get());
+    }
+
+    @Override
+    protected ConsoleContext getContext(final DataSchemaNode schemaNode) {
+        return new FilterConsoleContext(schemaNode, getSchemaContext());
+    }
+
+    private final class FilterConsoleContext extends BaseConsoleContext<DataSchemaNode> {
+
+        private final SchemaContext remoteSchemaContext;
+
+        public FilterConsoleContext(final DataSchemaNode schemaNode, final SchemaContext remoteSchemaContext) {
+            super(schemaNode);
+            this.remoteSchemaContext = remoteSchemaContext;
+        }
+
+        @Override
+        protected List<Completer> getAdditionalCompleters() {
+            return Collections.<Completer> singletonList(new FilterCompleter(remoteSchemaContext));
+        }
+    }
+
+    private final class FilterCompleter implements Completer {
+
+        private final SchemaContext remoteSchemaContext;
+
+        public FilterCompleter(final SchemaContext remoteSchemaContext) {
+            this.remoteSchemaContext = remoteSchemaContext;
+        }
+
+        @Override
+        public int complete(final String buffer, final int cursor, final List<CharSequence> candidates) {
+            final int idx = buffer.lastIndexOf(SEPARATOR);
+
+            final Optional<DataSchemaNode> currentNode = getCurrentNode(remoteSchemaContext, buffer);
+            if (currentNode.isPresent() && currentNode.get() instanceof DataNodeContainer) {
+                final Collection<DataSchemaNode> childNodes = ((DataNodeContainer) currentNode.get()).getChildNodes();
+                final Collection<String> transformed = Collections2.transform(childNodes,
+                        new Function<DataSchemaNode, String>() {
+                            @Override
+                            public String apply(final DataSchemaNode input) {
+                                return IOUtil.qNameToKeyString(input.getQName(),
+                                        mappedModulesNamespace.get(input.getQName().getNamespace()).getLocalName());
+                            }
+                        });
+
+                fillCandidates(buffer.substring(idx + 1), candidates, transformed);
+            }
+
+            return idx == -1 ? 0 : idx + 1;
+        }
+
+        private void fillCandidates(final String buffer, final List<CharSequence> candidates,
+                final Collection<String> transformed) {
+            final SortedSet<String> strings = new TreeSet<>(transformed);
+
+            if (buffer == null) {
+                candidates.addAll(strings);
+            } else {
+                for (final String match : strings.tailSet(buffer)) {
+                    if (!match.startsWith(buffer)) {
+                        break;
+                    }
+                    candidates.add(match);
+                }
+            }
+
+            if (candidates.size() == 1) {
+                candidates.set(0, candidates.get(0) + SEPARATOR);
+            }
+        }
+
+    }
+
+    private Optional<DataSchemaNode> getCurrentNode(DataSchemaNode parent, final String buffer) {
+        for (final String part : buffer.split(SEPARATOR)) {
+            if (IOUtil.isQName(part) == false) {
+                return Optional.of(parent);
+            }
+
+            final QName qName;
+            try {
+                qName = IOUtil.qNameFromKeyString(part, mappedModules);
+            } catch (final ReadingException e) {
+                return Optional.of(parent);
+            }
+            if (parent instanceof DataNodeContainer) {
+                parent = ((DataNodeContainer) parent).getDataChildByName(qName);
+            } else {
+                // This should check if we are at the end of buffer ?
+                return Optional.of(parent);
+            }
+        }
+        return Optional.of(parent);
+    }
+
+}