Implemented class for PI match key

Used as table entry ID in P4Runtime devices

Change-Id: I9f35503f118fa6e6a23b59aa6b716273a24ece0a
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMatchKey.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMatchKey.java
new file mode 100644
index 0000000..c95afb7
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiMatchKey.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2017-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.net.pi.runtime;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.StringJoiner;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Representation of all field matches of an entry of a match+action table of a protocol-independent pipeline.
+ */
+@Beta
+public final class PiMatchKey {
+
+    private final ImmutableMap<PiHeaderFieldId, PiFieldMatch> fieldMatches;
+
+    private PiMatchKey(ImmutableMap<PiHeaderFieldId, PiFieldMatch> fieldMatches) {
+        this.fieldMatches = fieldMatches;
+    }
+
+    /**
+     * Returns the collection of field matches of this match key.
+     *
+     * @return collection of field matches
+     */
+    public Collection<PiFieldMatch> fieldMatches() {
+        return fieldMatches.values();
+    }
+
+    /**
+     * If present, returns the field match associated with the given header field identifier.
+     *
+     * @param fieldId field identifier
+     * @return optional field match
+     */
+    public Optional<PiFieldMatch> fieldMatch(PiHeaderFieldId fieldId) {
+        return Optional.ofNullable(fieldMatches.get(fieldId));
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof PiMatchKey)) {
+            return false;
+        }
+        PiMatchKey that = (PiMatchKey) o;
+        return Objects.equal(fieldMatches, that.fieldMatches);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(fieldMatches);
+    }
+
+    @Override
+    public String toString() {
+        StringJoiner stringFieldMatches = new StringJoiner(", ", "{", "}");
+        this.fieldMatches.values().forEach(f -> stringFieldMatches.add(f.toString()));
+        return stringFieldMatches.toString();
+    }
+
+    /**
+     * Returns a new builder of match keys.
+     *
+     * @return match key builder
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Builder of match keys.
+     */
+    public static final class Builder {
+
+        private final ImmutableMap.Builder<PiHeaderFieldId, PiFieldMatch> fieldMatchesBuilder = ImmutableMap.builder();
+
+        private Builder() {
+            // hides constructor.
+        }
+
+        /**
+         * Adds one field match to this match key.
+         *
+         * @param fieldMatch field match
+         * @return this
+         */
+        public Builder addFieldMatch(PiFieldMatch fieldMatch) {
+            this.fieldMatchesBuilder.put(fieldMatch.fieldId(), fieldMatch);
+            return this;
+        }
+
+        /**
+         * Adds many field matches to this match key.
+         *
+         * @param fieldMatches collection of field matches
+         * @return this
+         */
+        public Builder addFieldMatches(Collection<PiFieldMatch> fieldMatches) {
+            fieldMatches.forEach(this::addFieldMatch);
+            return this;
+        }
+
+        /**
+         * Creates a new match key.
+         *
+         * @return match key
+         */
+        public PiMatchKey build() {
+            ImmutableMap<PiHeaderFieldId, PiFieldMatch> fieldMatches = fieldMatchesBuilder.build();
+            checkArgument(fieldMatches.size() > 0, "Field matches cannot be empty");
+            return new PiMatchKey(fieldMatches);
+        }
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
index 63e478d..40b2f05 100644
--- a/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
+++ b/core/api/src/main/java/org/onosproject/net/pi/runtime/PiTableEntry.java
@@ -19,11 +19,7 @@
 import com.google.common.annotations.Beta;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Objects;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Maps;
 
-import java.util.Collection;
-import java.util.Map;
 import java.util.Optional;
 
 import static com.google.common.base.Preconditions.checkArgument;
@@ -39,16 +35,16 @@
     private static final double NO_TIMEOUT = -1;
 
     private final PiTableId tableId;
-    private final Map<PiHeaderFieldId, PiFieldMatch> fieldMatches;
+    private final PiMatchKey matchKey;
     private final PiTableAction tableAction;
     private final long cookie;
     private final int priority;
     private final double timeout;
 
-    private PiTableEntry(PiTableId tableId, Map<PiHeaderFieldId, PiFieldMatch> fieldMatches,
+    private PiTableEntry(PiTableId tableId, PiMatchKey matchKey,
                          PiTableAction tableAction, long cookie, int priority, double timeout) {
         this.tableId = tableId;
-        this.fieldMatches = ImmutableMap.copyOf(fieldMatches);
+        this.matchKey = matchKey;
         this.tableAction = tableAction;
         this.cookie = cookie;
         this.priority = priority;
@@ -65,22 +61,12 @@
     }
 
     /**
-     * Returns an immutable view of the field matches of this table entry.
+     * Returns the match key of this table entry.
      *
-     * @return collection of field matches
+     * @return match key
      */
-    public Collection<PiFieldMatch> fieldMatches() {
-        return fieldMatches.values();
-    }
-
-    /**
-     * If present, returns the field match associated with the given header field identifier.
-     *
-     * @param fieldId field identifier
-     * @return optional field match
-     */
-    public Optional<PiFieldMatch> fieldMatch(PiHeaderFieldId fieldId) {
-        return Optional.ofNullable(fieldMatches.get(fieldId));
+    public PiMatchKey matchKey() {
+        return matchKey;
     }
 
     /**
@@ -133,20 +119,20 @@
         return priority == that.priority &&
                 Double.compare(that.timeout, timeout) == 0 &&
                 Objects.equal(tableId, that.tableId) &&
-                Objects.equal(fieldMatches, that.fieldMatches) &&
+                Objects.equal(matchKey, that.matchKey) &&
                 Objects.equal(tableAction, that.tableAction);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hashCode(tableId, fieldMatches, tableAction, priority, timeout);
+        return Objects.hashCode(tableId, matchKey, tableAction, priority, timeout);
     }
 
     @Override
     public String toString() {
         return MoreObjects.toStringHelper(this)
                 .add("tableId", tableId)
-                .add("fieldMatches", fieldMatches)
+                .add("matchKey", matchKey)
                 .add("tableAction", tableAction)
                 .add("priority", priority == NO_PRIORITY ? "N/A" : String.valueOf(priority))
                 .add("timeout", timeout == NO_TIMEOUT ? "PERMANENT" : String.valueOf(timeout))
@@ -165,7 +151,7 @@
     public static final class Builder {
 
         private PiTableId tableId;
-        private Map<PiHeaderFieldId, PiFieldMatch> fieldMatches = Maps.newHashMap();
+        private PiMatchKey matchKey;
         private PiTableAction tableAction;
         private long cookie = 0;
         private int priority = NO_PRIORITY;
@@ -198,24 +184,13 @@
         }
 
         /**
-         * Adds one field match to this table entry.
+         * Sets the match key of this table entry.
          *
-         * @param fieldMatch field match
+         * @param matchKey match key
          * @return this
          */
-        public Builder withFieldMatch(PiFieldMatch fieldMatch) {
-            this.fieldMatches.put(fieldMatch.fieldId(), fieldMatch);
-            return this;
-        }
-
-        /**
-         * Adds many field matches to this table entry.
-         *
-         * @param fieldMatches collection of field matches
-         * @return this
-         */
-        public Builder withFieldMatches(Collection<PiFieldMatch> fieldMatches) {
-            fieldMatches.forEach(f -> this.fieldMatches.put(f.fieldId(), f));
+        public Builder withMatchKey(PiMatchKey matchKey) {
+            this.matchKey = matchKey;
             return this;
         }
 
@@ -262,7 +237,7 @@
         public PiTableEntry build() {
             checkNotNull(tableId);
             checkNotNull(tableAction);
-            return new PiTableEntry(tableId, fieldMatches, tableAction, cookie, priority, timeout);
+            return new PiTableEntry(tableId, matchKey, tableAction, cookie, priority, timeout);
         }
     }
 }
diff --git a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiTableEntryTest.java b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiTableEntryTest.java
index 0246227..84211e0 100644
--- a/core/api/src/test/java/org/onosproject/net/pi/runtime/PiTableEntryTest.java
+++ b/core/api/src/test/java/org/onosproject/net/pi/runtime/PiTableEntryTest.java
@@ -93,7 +93,9 @@
         fieldMatches.put(piHeaderFieldId, piFieldMatch);
         final PiTableEntry piTableEntry = PiTableEntry.builder()
                 .forTable(piTableId)
-                .withFieldMatches(fieldMatches.values())
+                .withMatchKey(PiMatchKey.builder()
+                                      .addFieldMatches(fieldMatches.values())
+                                      .build())
                 .withAction(piAction)
                 .withCookie(cookie)
                 .withPriority(priority)
@@ -105,7 +107,7 @@
         assertThat(piTableEntry.priority().get(), is(priority));
         assertThat(piTableEntry.timeout().get(), is(timeout));
         assertThat("Incorrect match param value",
-                CollectionUtils.isEqualCollection(piTableEntry.fieldMatches(), fieldMatches.values()));
+                   CollectionUtils.isEqualCollection(piTableEntry.matchKey().fieldMatches(), fieldMatches.values()));
         assertThat(piTableEntry.action(), is(piAction));
     }
 }
diff --git a/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslator.java b/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslator.java
index 8b28b5e..17dce70 100644
--- a/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslator.java
+++ b/core/net/src/main/java/org/onosproject/net/pi/impl/PiFlowRuleTranslator.java
@@ -41,6 +41,7 @@
 import org.onosproject.net.pi.runtime.PiFieldMatch;
 import org.onosproject.net.pi.runtime.PiHeaderFieldId;
 import org.onosproject.net.pi.runtime.PiLpmFieldMatch;
+import org.onosproject.net.pi.runtime.PiMatchKey;
 import org.onosproject.net.pi.runtime.PiRangeFieldMatch;
 import org.onosproject.net.pi.runtime.PiTableAction;
 import org.onosproject.net.pi.runtime.PiTableEntry;
@@ -135,7 +136,9 @@
         tableEntryBuilder
                 .forTable(piTableId)
                 .withPriority(rule.priority())
-                .withFieldMatches(fieldMatches)
+                .withMatchKey(PiMatchKey.builder()
+                                      .addFieldMatches(fieldMatches)
+                                      .build())
                 .withAction(piAction);
 
         if (!rule.isPermanent()) {
diff --git a/core/net/src/test/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorTest.java b/core/net/src/test/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorTest.java
index 894865e..d865c8e 100644
--- a/core/net/src/test/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorTest.java
+++ b/core/net/src/test/java/org/onosproject/net/pi/impl/PiFlowRuleTranslatorTest.java
@@ -124,16 +124,16 @@
 
         int numMatchParams = pipeconf.pipelineModel().table(TABLE0).get().matchFields().size();
         // parse values stored in entry1
-        PiTernaryFieldMatch inPortParam = (PiTernaryFieldMatch) entry1.fieldMatch(IN_PORT_ID).get();
-        PiTernaryFieldMatch ethDstParam = (PiTernaryFieldMatch) entry1.fieldMatch(ETH_DST_ID).get();
-        PiTernaryFieldMatch ethSrcParam = (PiTernaryFieldMatch) entry1.fieldMatch(ETH_SRC_ID).get();
-        PiTernaryFieldMatch ethTypeParam = (PiTernaryFieldMatch) entry1.fieldMatch(ETH_TYPE_ID).get();
+        PiTernaryFieldMatch inPortParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(IN_PORT_ID).get();
+        PiTernaryFieldMatch ethDstParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(ETH_DST_ID).get();
+        PiTernaryFieldMatch ethSrcParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(ETH_SRC_ID).get();
+        PiTernaryFieldMatch ethTypeParam = (PiTernaryFieldMatch) entry1.matchKey().fieldMatch(ETH_TYPE_ID).get();
         Optional<Double> expectedTimeout = pipeconf.pipelineModel().table(TABLE0).get().supportsAging()
                 ? Optional.of((double) rule1.timeout()) : Optional.empty();
 
         // check that the number of parameters in the entry is the same as the number of table keys
         assertThat("Incorrect number of match parameters",
-                   entry1.fieldMatches().size(), is(equalTo(numMatchParams)));
+                   entry1.matchKey().fieldMatches().size(), is(equalTo(numMatchParams)));
 
         // check that values stored in entry are the same used for the flow rule
         assertThat("Incorrect inPort match param value",
diff --git a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java
index 68e1808..5f0ac5f 100644
--- a/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java
+++ b/protocols/p4runtime/ctl/src/main/java/org/onosproject/p4runtime/ctl/TableEntryEncoder.java
@@ -28,6 +28,7 @@
 import org.onosproject.net.pi.runtime.PiFieldMatch;
 import org.onosproject.net.pi.runtime.PiHeaderFieldId;
 import org.onosproject.net.pi.runtime.PiLpmFieldMatch;
+import org.onosproject.net.pi.runtime.PiMatchKey;
 import org.onosproject.net.pi.runtime.PiRangeFieldMatch;
 import org.onosproject.net.pi.runtime.PiTableAction;
 import org.onosproject.net.pi.runtime.PiTableEntry;
@@ -161,7 +162,7 @@
         tableEntryMsgBuilder.setAction(encodePiTableAction(piTableEntry.action(), browser));
 
         // Field matches.
-        for (PiFieldMatch piFieldMatch : piTableEntry.fieldMatches()) {
+        for (PiFieldMatch piFieldMatch : piTableEntry.matchKey().fieldMatches()) {
             tableEntryMsgBuilder.addMatch(encodePiFieldMatch(piFieldMatch, tableInfo, browser));
         }
 
@@ -190,10 +191,12 @@
         // Timeout.
         // FIXME: how to decode table entry messages with timeout, given that the timeout value is lost after encoding?
 
-        // Field matches.
+        // Match key for field matches.
+        PiMatchKey.Builder piMatchKeyBuilder = PiMatchKey.builder();
         for (FieldMatch fieldMatchMsg : tableEntryMsg.getMatchList()) {
-            piTableEntryBuilder.withFieldMatch(decodeFieldMatchMsg(fieldMatchMsg, tableInfo, browser));
+            piMatchKeyBuilder.addFieldMatch(decodeFieldMatchMsg(fieldMatchMsg, tableInfo, browser));
         }
+        piTableEntryBuilder.withMatchKey(piMatchKeyBuilder.build());
 
         return piTableEntryBuilder.build();
     }
diff --git a/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/TableEntryEncoderTest.java b/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/TableEntryEncoderTest.java
index e4f00dc..a33430a 100644
--- a/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/TableEntryEncoderTest.java
+++ b/protocols/p4runtime/ctl/src/test/java/org/onosproject/p4runtime/ctl/TableEntryEncoderTest.java
@@ -29,6 +29,7 @@
 import org.onosproject.net.pi.runtime.PiActionParam;
 import org.onosproject.net.pi.runtime.PiActionParamId;
 import org.onosproject.net.pi.runtime.PiHeaderFieldId;
+import org.onosproject.net.pi.runtime.PiMatchKey;
 import org.onosproject.net.pi.runtime.PiTableEntry;
 import org.onosproject.net.pi.runtime.PiTableId;
 import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
@@ -45,6 +46,7 @@
 import static org.hamcrest.Matchers.is;
 import static org.onlab.util.ImmutableByteSequence.copyFrom;
 import static org.onlab.util.ImmutableByteSequence.fit;
+import static org.onlab.util.ImmutableByteSequence.ofOnes;
 import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType.BMV2_JSON;
 import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType.P4_INFO_TEXT;
 import static org.onosproject.p4runtime.ctl.TableEntryEncoder.decode;
@@ -94,15 +96,17 @@
     private final PiTableEntry piTableEntry = PiTableEntry
             .builder()
             .forTable(tableId)
-            .withFieldMatch(new PiTernaryFieldMatch(ethDstAddrFieldId, ethAddr, ImmutableByteSequence.ofOnes(6)))
-            .withFieldMatch(new PiTernaryFieldMatch(ethSrcAddrFieldId, ethAddr, ImmutableByteSequence.ofOnes(6)))
-            .withFieldMatch(new PiTernaryFieldMatch(inPortFieldId, portValue, ImmutableByteSequence.ofOnes(2)))
-            .withFieldMatch(new PiTernaryFieldMatch(ethTypeFieldId, portValue, ImmutableByteSequence.ofOnes(2)))
+            .withMatchKey(PiMatchKey.builder()
+                                  .addFieldMatch(new PiTernaryFieldMatch(ethDstAddrFieldId, ethAddr, ofOnes(6)))
+                                  .addFieldMatch(new PiTernaryFieldMatch(ethSrcAddrFieldId, ethAddr, ofOnes(6)))
+                                  .addFieldMatch(new PiTernaryFieldMatch(inPortFieldId, portValue, ofOnes(2)))
+                                  .addFieldMatch(new PiTernaryFieldMatch(ethTypeFieldId, portValue, ofOnes(2)))
+                                  .build())
             .withAction(PiAction
-                    .builder()
-                    .withId(outActionId)
-                    .withParameter(new PiActionParam(portParamId, portValue))
-                    .build())
+                                .builder()
+                                .withId(outActionId)
+                                .withParameter(new PiActionParam(portParamId, portValue))
+                                .build())
             .withPriority(1)
             .withCookie(2)
             .build();