Major refactoring of the BMv2 protocol module (onos1.6 cherry-pick)

- Created 3 separate sub-modules: API (doesn't depend on
    Thrift), CTL (depends on Thrift), THRIFT-API (to generate Thrift
    sources)
- Implemented 2 new services (for device configuration swapping and
    table entry management) needed to distribute BMv2-specific state
    among ONOS instances.
- Implemented a BMv2 controller (previously other modules where
    using separately a Thrift client and a server)
- Added a default BMv2 JSON configuration (default.json) and interpreter
    to be used for devices that connect for the first time to ONOS.
    This allows for basic services to work (i.e. LLDP link discovery,
    ARP proxy. etc.).
- Changed behavior of the flow rule translator and extension selector,
    now it allows extension to specify only some of the match parameters
    (before extension selectors were expected to describe the whole
    match key, i.e. all fields)
- Various renaming to better represent the API
- Various java doc fixes / improvements

Change-Id: Ida4b5e546b0def97c3552a6c05f7bce76fd32c28
diff --git a/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2TableDumpParser.java b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2TableDumpParser.java
new file mode 100644
index 0000000..04e37e5
--- /dev/null
+++ b/protocols/bmv2/ctl/src/main/java/org/onosproject/bmv2/ctl/Bmv2TableDumpParser.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * 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 org.onosproject.bmv2.ctl;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.CacheStats;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Lists;
+import org.apache.commons.lang3.tuple.Pair;
+import org.onlab.util.HexString;
+import org.onlab.util.ImmutableByteSequence;
+import org.onlab.util.SharedScheduledExecutors;
+import org.onosproject.bmv2.api.context.Bmv2ActionModel;
+import org.onosproject.bmv2.api.context.Bmv2Configuration;
+import org.onosproject.bmv2.api.runtime.Bmv2Action;
+import org.onosproject.bmv2.api.runtime.Bmv2ExactMatchParam;
+import org.onosproject.bmv2.api.runtime.Bmv2LpmMatchParam;
+import org.onosproject.bmv2.api.runtime.Bmv2MatchKey;
+import org.onosproject.bmv2.api.runtime.Bmv2ParsedTableEntry;
+import org.onosproject.bmv2.api.runtime.Bmv2TernaryMatchParam;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.bmv2.api.utils.Bmv2TranslatorUtils.fitByteSequence;
+import static org.onosproject.bmv2.api.utils.Bmv2TranslatorUtils.ByteSequenceFitException;
+
+/**
+ * BMv2 table dump parser.
+ */
+public final class Bmv2TableDumpParser {
+
+    // Examples of a BMv2 table dump can be found in Bmv2TableDumpParserTest
+
+    // 1: entry id, 2: match string, 3: action string
+    private static final String ENTRY_PATTERN_REGEX = "(\\d+): (.*) => (.*)";
+    // 1: match values, 2: masks
+    private static final String MATCH_TERNARY_PATTERN_REGEX = "([0-9a-fA-F ]+) &&& ([0-9a-fA-F ]+)";
+    // 1: match values, 2: masks
+    private static final String MATCH_LPM_PATTERN_REGEX = "([0-9a-fA-F ]+) / ([0-9a-fA-F ]+)";
+    // 1: match values
+    private static final String MATCH_EXACT_PATTERN_REGEX = "([0-9a-fA-F ]+)";
+    // 1: action name, 2: action params
+    private static final String ACTION_PATTERN_REGEX = "(.+) - ?([0-9a-fA-F ,]*)";
+
+    private static final Pattern ENTRY_PATTERN = Pattern.compile(ENTRY_PATTERN_REGEX);
+    private static final Pattern MATCH_TERNARY_PATTERN = Pattern.compile(MATCH_TERNARY_PATTERN_REGEX);
+    private static final Pattern MATCH_LPM_PATTERN = Pattern.compile(MATCH_LPM_PATTERN_REGEX);
+    private static final Pattern MATCH_EXACT_PATTERN = Pattern.compile(MATCH_EXACT_PATTERN_REGEX);
+    private static final Pattern ACTION_PATTERN = Pattern.compile(ACTION_PATTERN_REGEX);
+
+    // Cache to avoid re-parsing known lines.
+    // The assumption here is that entries are not updated too frequently, so that the entry id doesn't change often.
+    // Otherwise, we should cache only the match and action strings...
+    private static final LoadingCache<Pair<String, Bmv2Configuration>, Optional<Bmv2ParsedTableEntry>> ENTRY_CACHE =
+            CacheBuilder.newBuilder()
+            .expireAfterAccess(60, TimeUnit.SECONDS)
+            .recordStats()
+            .build(new CacheLoader<Pair<String, Bmv2Configuration>, Optional<Bmv2ParsedTableEntry>>() {
+                @Override
+                public Optional<Bmv2ParsedTableEntry> load(Pair<String, Bmv2Configuration> key) throws Exception {
+                    // Very expensive call.
+                    return Optional.ofNullable(parseLine(key.getLeft(), key.getRight()));
+                }
+            });
+
+    private static final Logger log = LoggerFactory.getLogger(Bmv2TableDumpParser.class);
+
+    private static final long STATS_LOG_FREQUENCY = 3; // minutes
+
+    static {
+        SharedScheduledExecutors.getSingleThreadExecutor().scheduleAtFixedRate(
+                () -> reportStats(), 0, STATS_LOG_FREQUENCY, TimeUnit.MINUTES);
+    }
+
+    private Bmv2TableDumpParser() {
+        // Ban constructor.
+    }
+
+    /**
+     * Parse the given BMv2 table dump.
+     *
+     * @param tableDump a string value
+     * @return a list of {@link Bmv2ParsedTableEntry}
+     */
+    public static List<Bmv2ParsedTableEntry> parse(String tableDump, Bmv2Configuration configuration) {
+        checkNotNull(tableDump, "tableDump cannot be null");
+        // Parse all lines
+        List<Bmv2ParsedTableEntry> result = Arrays.stream(tableDump.split("\n"))
+                .map(line -> Pair.of(line, configuration))
+                .map(Bmv2TableDumpParser::loadFromCache)
+                .filter(Optional::isPresent)
+                .map(Optional::get)
+                .collect(Collectors.toList());
+        return result;
+    }
+
+    private static Optional<Bmv2ParsedTableEntry> loadFromCache(Pair<String, Bmv2Configuration> key) {
+        try {
+            return ENTRY_CACHE.get(key);
+        } catch (ExecutionException e) {
+            Throwable t = e.getCause();
+            if (t instanceof Bmv2TableDumpParserException) {
+                Bmv2TableDumpParserException parserException = (Bmv2TableDumpParserException) t;
+                log.warn("{}", parserException.getMessage());
+            } else {
+                log.error("Exception while parsing table dump line", e);
+            }
+            return Optional.empty();
+        }
+    }
+
+    private static void reportStats() {
+        CacheStats stats = ENTRY_CACHE.stats();
+        log.info("Cache stats: requestCount={}, hitRate={}, exceptionsCount={}, avgLoadPenalty={}",
+                 stats.requestCount(), stats.hitRate(), stats.loadExceptionCount(), stats.averageLoadPenalty());
+    }
+
+    private static Bmv2ParsedTableEntry parseLine(String line, Bmv2Configuration configuration)
+            throws Bmv2TableDumpParserException {
+        Matcher matcher = ENTRY_PATTERN.matcher(line);
+        if (matcher.find()) {
+            long entryId = parseEntryId(matcher, 1);
+            String matchString = parseMatchString(matcher, 2);
+            String actionString = parseActionString(matcher, 3);
+            Bmv2MatchKey matchKey = parseMatchKey(matchString);
+            Bmv2Action action = parseAction(actionString, configuration);
+            return new Bmv2ParsedTableEntry(entryId, matchKey, action);
+        } else {
+            // Not a table entry
+            return null;
+        }
+    }
+
+    private static Long parseEntryId(Matcher matcher, int groupIdx) throws Bmv2TableDumpParserException {
+        String str = matcher.group(groupIdx);
+        if (str == null) {
+            throw new Bmv2TableDumpParserException("Unable to find entry ID: " + matcher.group());
+        }
+        long entryId;
+        try {
+            entryId = Long.valueOf(str.trim());
+        } catch (NumberFormatException e) {
+            throw new Bmv2TableDumpParserException("Unable to parse entry id for string: " + matcher.group());
+        }
+        return entryId;
+    }
+
+    private static String parseMatchString(Matcher matcher, int groupIdx) throws Bmv2TableDumpParserException {
+        String str = matcher.group(groupIdx);
+        if (str == null) {
+            throw new Bmv2TableDumpParserException("Unable to find match string: " + matcher.group());
+        }
+        return str.trim();
+    }
+
+    private static String parseActionString(Matcher matcher, int groupIdx) throws Bmv2TableDumpParserException {
+        String str = matcher.group(groupIdx);
+        if (str == null) {
+            throw new Bmv2TableDumpParserException("Unable to find action string: " + matcher.group());
+        }
+        return str.trim();
+    }
+
+    private static Bmv2MatchKey parseMatchKey(String str) throws Bmv2TableDumpParserException {
+
+        Bmv2MatchKey.Builder builder = Bmv2MatchKey.builder();
+
+        // Try with ternary...
+        Matcher matcher = MATCH_TERNARY_PATTERN.matcher(str);
+        if (matcher.find()) {
+            // Ternary Match.
+            List<ImmutableByteSequence> values = parseMatchValues(matcher, 1);
+            List<ImmutableByteSequence> masks = parseMatchMasks(matcher, 2, values);
+            for (int i = 0; i < values.size(); i++) {
+                builder.add(new Bmv2TernaryMatchParam(values.get(i), masks.get(i)));
+            }
+            return builder.build();
+        }
+
+        // FIXME: LPM match parsing broken if table key contains also a ternary match
+        // Also it assumes the lpm parameter is the last one, which is wrong.
+        // Try with LPM...
+        matcher = MATCH_LPM_PATTERN.matcher(str);
+        if (matcher.find()) {
+            // Lpm Match.
+            List<ImmutableByteSequence> values = parseMatchValues(matcher, 1);
+            int prefixLength = parseLpmPrefix(matcher, 2);
+            for (int i = 0; i < values.size() - 1; i++) {
+                builder.add(new Bmv2ExactMatchParam(values.get(i)));
+            }
+            builder.add(new Bmv2LpmMatchParam(values.get(values.size() - 1), prefixLength));
+            return builder.build();
+        }
+
+        // Try with exact...
+        matcher = MATCH_EXACT_PATTERN.matcher(str);
+        if (matcher.find()) {
+            // Exact match.
+            parseMatchValues(matcher, 1)
+                    .stream()
+                    .map(Bmv2ExactMatchParam::new)
+                    .forEach(builder::add);
+            return builder.build();
+        }
+
+        throw new Bmv2TableDumpParserException("Unable to parse match string: " + str);
+    }
+
+    private static List<ImmutableByteSequence> parseMatchValues(Matcher matcher, int groupIdx)
+            throws Bmv2TableDumpParserException {
+        String matchString = matcher.group(groupIdx);
+        if (matchString == null) {
+            throw new Bmv2TableDumpParserException("Unable to find match params for string: " + matcher.group());
+        }
+        List<ImmutableByteSequence> result = Lists.newArrayList();
+        for (String paramString : matchString.split(" ")) {
+            byte[] bytes = HexString.fromHexString(paramString, null);
+            result.add(ImmutableByteSequence.copyFrom(bytes));
+        }
+        return result;
+    }
+
+    private static List<ImmutableByteSequence> parseMatchMasks(Matcher matcher, int groupIdx,
+                                                               List<ImmutableByteSequence> matchParams)
+            throws Bmv2TableDumpParserException {
+        String maskString = matcher.group(groupIdx);
+        if (maskString == null) {
+            throw new Bmv2TableDumpParserException("Unable to find mask for string: " + matcher.group());
+        }
+        List<ImmutableByteSequence> result = Lists.newArrayList();
+        /*
+        Mask here is a hex string with no spaces, hence individual mask params can be derived according
+        to given matchParam sizes.
+         */
+        byte[] maskBytes = HexString.fromHexString(maskString, null);
+        int startPosition = 0;
+        for (ImmutableByteSequence bs : matchParams) {
+            if (startPosition + bs.size() > maskBytes.length) {
+                throw new Bmv2TableDumpParserException("Invalid length for mask in string: " + matcher.group());
+            }
+            ImmutableByteSequence maskParam = ImmutableByteSequence.copyFrom(maskBytes,
+                                                                             startPosition,
+                                                                             startPosition + bs.size() - 1);
+            result.add(maskParam);
+            startPosition += bs.size();
+        }
+        return result;
+    }
+
+    private static int parseLpmPrefix(Matcher matcher, int groupIdx)
+            throws Bmv2TableDumpParserException {
+        String str = matcher.group(groupIdx);
+        if (str == null) {
+            throw new Bmv2TableDumpParserException("Unable to find LPM prefix for string: " + matcher.group());
+        }
+        // For some reason the dumped prefix has 16 bits more than the one programmed
+        try {
+            return Integer.valueOf(str.trim()) - 16;
+        } catch (NumberFormatException e) {
+            throw new Bmv2TableDumpParserException("Unable to parse LPM prefix from string: " + matcher.group());
+        }
+    }
+
+    private static Bmv2Action parseAction(String str, Bmv2Configuration configuration)
+            throws Bmv2TableDumpParserException {
+        Matcher matcher = ACTION_PATTERN.matcher(str);
+        if (matcher.find()) {
+            String actionName = parseActionName(matcher, 1);
+            Bmv2ActionModel actionModel = configuration.action(actionName);
+            if (actionModel == null) {
+                throw new Bmv2TableDumpParserException("Not such an action in configuration: " + actionName);
+            }
+            Bmv2Action.Builder builder = Bmv2Action.builder().withName(actionName);
+            List<ImmutableByteSequence> actionParams = parseActionParams(matcher, 2);
+            if (actionParams.size() != actionModel.runtimeDatas().size()) {
+                throw new Bmv2TableDumpParserException("Invalid number of parameters for action: " + actionName);
+            }
+            for (int i = 0; i < actionModel.runtimeDatas().size(); i++) {
+                try {
+                    // fit param byte-width according to configuration.
+                    builder.addParameter(fitByteSequence(actionParams.get(i),
+                                                         actionModel.runtimeDatas().get(i).bitWidth()));
+                } catch (ByteSequenceFitException e) {
+                    throw new Bmv2TableDumpParserException("Unable to parse action param: " + e.toString());
+                }
+            }
+            return builder.build();
+        }
+        throw new Bmv2TableDumpParserException("Unable to parse action string: " + str.trim());
+    }
+
+    private static String parseActionName(Matcher matcher, int groupIdx) throws Bmv2TableDumpParserException {
+        String actionName = matcher.group(groupIdx);
+        if (actionName == null) {
+            throw new Bmv2TableDumpParserException("Unable to find action name for string: " + matcher.group());
+        }
+        return actionName.trim();
+    }
+
+    private static List<ImmutableByteSequence> parseActionParams(Matcher matcher, int groupIdx)
+            throws Bmv2TableDumpParserException {
+        String paramsString = matcher.group(groupIdx);
+        if (paramsString == null) {
+            throw new Bmv2TableDumpParserException("Unable to find action params for string: " + matcher.group());
+        }
+        if (paramsString.length() == 0) {
+            return Collections.emptyList();
+        }
+        return Arrays.stream(paramsString.split(","))
+                .map(String::trim)
+                .map(s -> HexString.fromHexString(s, null))
+                .map(ImmutableByteSequence::copyFrom)
+                .collect(Collectors.toList());
+    }
+
+    public static class Bmv2TableDumpParserException extends Exception {
+        public Bmv2TableDumpParserException(String msg) {
+            super(msg);
+        }
+    }
+}
\ No newline at end of file