BUG-1041 cli proposal #1
[controller.git] / opendaylight / netconf / netconf-cli / src / main / java / org / opendaylight / controller / netconf / cli / reader / custom / FilterReader.java
diff --git a/opendaylight/netconf/netconf-cli/src/main/java/org/opendaylight/controller/netconf/cli/reader/custom/FilterReader.java b/opendaylight/netconf/netconf-cli/src/main/java/org/opendaylight/controller/netconf/cli/reader/custom/FilterReader.java
new file mode 100644 (file)
index 0000000..7b37f69
--- /dev/null
@@ -0,0 +1,218 @@
+/*
+ * 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.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.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.data.impl.ImmutableCompositeNode;
+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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 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 FilterReader extends AbstractReader<DataSchemaNode> {
+
+    private static final Logger LOG = LoggerFactory.getLogger(FilterReader.class);
+
+    public static final String SEPARATOR = "/";
+
+    private final Map<String, QName> mappedModules;
+    private final Map<URI, QName> mappedModulesNamespace;
+
+    public FilterReader(final ConsoleIO console, final SchemaContext remoteSchemaContext) {
+        super(console, remoteSchemaContext);
+
+        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
+
+    public static final QName FILTER_TYPE_QNAME = QName.create("urn:ietf:params:xml:ns:netconf:base:1.0", "2011-06-01",
+            "type");
+    public static final String FILTER_TYPE_VALUE_DEFAULT = "subtree";
+
+    @Override
+    protected List<Node<?>> readWithContext(final DataSchemaNode schemaNode) throws IOException, ReadingException {
+        boolean redSuccessfuly = false;
+        Node<?> newNode = null;
+        do {
+            console.writeLn("Filter " + schemaNode.getQName().getLocalName());
+            console.writeLn("Submit path of the data to retrieve. Use TAB for autocomplete");
+
+            final String rawValue = console.read();
+
+            // FIXME skip should be somewhere in abstractReader
+            if (isSkipInput(rawValue) || Strings.isNullOrEmpty(rawValue)) {
+                return Collections.emptyList();
+            }
+
+            final List<QName> filterPartsQNames = Lists.newArrayList();
+
+            try {
+                for (final String part : rawValue.split(SEPARATOR)) {
+                    final QName qName = IOUtil.qNameFromKeyString(part, mappedModules);
+                    filterPartsQNames.add(qName);
+                }
+
+                Node<?> previous = null;
+
+                for (final QName qName : Lists.reverse(filterPartsQNames)) {
+                    previous = new CompositeNodeTOImpl(qName, null,
+                            previous == null ? Collections.<Node<?>> emptyList()
+                                    : Collections.<Node<?>> singletonList(previous));
+                }
+
+                final Map<QName, String> attributes = Collections.singletonMap(FILTER_TYPE_QNAME,
+                        FILTER_TYPE_VALUE_DEFAULT);
+                newNode = previous == null ? null : ImmutableCompositeNode.create(schemaNode.getQName(), attributes,
+                        Collections.<Node<?>> singletonList(previous));
+                redSuccessfuly = true;
+            } catch (final ReadingException e) {
+                final String message = "Specified filter path isn't correct.";
+                LOG.error(message, e);
+                console.writeLn(message);
+            }
+        } while (!redSuccessfuly);
+        return Collections.<Node<?>> singletonList(newNode);
+    }
+
+    @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;
+
+        // TODO add skip to filter completer, better soulution would be to add
+        // SKIP completer before context completer if possible
+
+        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<DataNodeContainer> currentNode = getCurrentNode(remoteSchemaContext, buffer);
+            if (currentNode.isPresent()) {
+
+                final Collection<String> transformed = Collections2.transform(currentNode.get().getChildNodes(),
+                        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<DataNodeContainer> getCurrentNode(DataNodeContainer parent, final String buffer) {
+            for (final String part : buffer.split(SEPARATOR)) {
+                if (!IOUtil.isQName(part)) {
+                    return Optional.of(parent);
+                }
+
+                QName qName;
+                try {
+                    qName = IOUtil.qNameFromKeyString(part, mappedModules);
+                } catch (final ReadingException e) {
+                    return Optional.of(parent);
+                }
+
+                final DataSchemaNode dataChildByName = parent.getDataChildByName(qName);
+                if (dataChildByName instanceof DataNodeContainer) {
+                    parent = (DataNodeContainer) dataChildByName;
+                } else {
+                    return Optional.absent();
+                }
+            }
+            return Optional.of(parent);
+        }
+    }
+
+}