Extract and publish p4runtime protocol utils in separate artifact

For consumption by third-party apps built via mvn

Change-Id: Ic9c1eccb2519b2952cdb632e1f07ccefbba00396
diff --git a/protocols/p4runtime/utils/BUILD b/protocols/p4runtime/utils/BUILD
new file mode 100644
index 0000000..a521bbb
--- /dev/null
+++ b/protocols/p4runtime/utils/BUILD
@@ -0,0 +1,8 @@
+COMPILE_DEPS = CORE_DEPS + [
+    "//protocols/p4runtime/proto:onos-protocols-p4runtime-proto",
+    "//deps:com_google_protobuf_protobuf_java",
+]
+
+osgi_jar_with_tests(
+    deps = COMPILE_DEPS,
+)
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/AbstractCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/AbstractCodec.java
new file mode 100644
index 0000000..2bb75a3
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/AbstractCodec.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.Message;
+import com.google.protobuf.TextFormat;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import org.onosproject.p4runtime.ctl.utils.PipeconfHelper;
+import org.slf4j.Logger;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Abstract implementation of a general codec that translates pipeconf-related
+ * objects into protobuf messages and vice versa.
+ *
+ * @param <P> object
+ * @param <M> protobuf message class
+ * @param <X> metadata class
+ */
+abstract class AbstractCodec<P, M extends Message, X> {
+
+    protected final Logger log = getLogger(this.getClass());
+
+    protected abstract M encode(P object, X metadata, PiPipeconf pipeconf,
+                                P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException;
+
+    protected abstract P decode(M message, X metadata, PiPipeconf pipeconf,
+                                P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException;
+
+    /**
+     * Returns a protobuf message that is equivalent to the given object for the
+     * given metadata and pipeconf.
+     *
+     * @param object   object
+     * @param metadata metadata
+     * @param pipeconf pipeconf
+     * @return protobuf message
+     * @throws CodecException if the given object cannot be encoded (see
+     *                        exception message)
+     */
+    public M encode(P object, X metadata, PiPipeconf pipeconf)
+            throws CodecException {
+        checkNotNull(object);
+        try {
+            return encode(object, metadata, pipeconf, browserOrFail(pipeconf));
+        } catch (P4InfoBrowser.NotFoundException e) {
+            throw new CodecException(e.getMessage());
+        }
+    }
+
+    /**
+     * Returns a object that is equivalent to the protobuf message for the given
+     * metadata and pipeconf.
+     *
+     * @param message  protobuf message
+     * @param metadata metadata
+     * @param pipeconf pipeconf pipeconf
+     * @return object
+     * @throws CodecException if the given protobuf message cannot be decoded
+     *                        (see exception message)
+     */
+    public P decode(M message, X metadata, PiPipeconf pipeconf)
+            throws CodecException {
+        checkNotNull(message);
+        try {
+            return decode(message, metadata, pipeconf, browserOrFail(pipeconf));
+        } catch (P4InfoBrowser.NotFoundException e) {
+            throw new CodecException(e.getMessage());
+        }
+    }
+
+    /**
+     * Same as {@link #encode(Object, Object, PiPipeconf)} but returns null in
+     * case of exceptions, while the error message is logged.
+     *
+     * @param object   object
+     * @param metadata metadata
+     * @param pipeconf pipeconf
+     * @return protobuf message
+     */
+    private M encodeOrNull(P object, X metadata, PiPipeconf pipeconf) {
+        checkNotNull(object);
+        try {
+            return encode(object, metadata, pipeconf);
+        } catch (CodecException e) {
+            log.error("Unable to encode {}: {} [{}]",
+                      object.getClass().getSimpleName(),
+                      e.getMessage(), object.toString());
+            return null;
+        }
+    }
+
+    /**
+     * Same as {@link #decode(Message, Object, PiPipeconf)} but returns null in
+     * case of exceptions, while the error message is logged.
+     *
+     * @param message  protobuf message
+     * @param metadata metadata
+     * @param pipeconf pipeconf pipeconf
+     * @return object
+     */
+    private P decodeOrNull(M message, X metadata, PiPipeconf pipeconf) {
+        checkNotNull(message);
+        try {
+            return decode(message, metadata, pipeconf);
+        } catch (CodecException e) {
+            log.error("Unable to decode {}: {} [{}]",
+                      message.getClass().getSimpleName(),
+                      e.getMessage(), TextFormat.shortDebugString(message));
+            return null;
+        }
+    }
+
+    /**
+     * Encodes the given list of objects, skipping those that cannot be encoded,
+     * in which case an error message is logged. For this reason, the returned
+     * list might have different size than the returned one.
+     *
+     * @param objects  list of objects
+     * @param metadata metadata
+     * @param pipeconf pipeconf
+     * @return list of protobuf messages
+     */
+    private List<M> encodeAllSkipException(
+            Collection<P> objects, X metadata, PiPipeconf pipeconf) {
+        checkNotNull(objects);
+        if (objects.isEmpty()) {
+            return ImmutableList.of();
+        }
+        return objects.stream()
+                .map(p -> encodeOrNull(p, metadata, pipeconf))
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Decodes the given list of protobuf messages, skipping those that cannot
+     * be decoded, on which case an error message is logged. For this reason,
+     * the returned list might have different size than the returned one.
+     *
+     * @param messages list of protobuf messages
+     * @param metadata metadata
+     * @param pipeconf pipeconf
+     * @return list of objects
+     */
+    private List<P> decodeAllSkipException(
+            Collection<M> messages, X metadata, PiPipeconf pipeconf) {
+        checkNotNull(messages);
+        if (messages.isEmpty()) {
+            return ImmutableList.of();
+        }
+        return messages.stream()
+                .map(m -> decodeOrNull(m, metadata, pipeconf))
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Encodes the given collection of objects. Throws an exception if one or
+     * more of the given objects cannot be encoded. The returned list is
+     * guaranteed to have same size and order as the given one.
+     *
+     * @param objects  list of objects
+     * @param metadata metadata
+     * @param pipeconf pipeconf
+     * @return list of protobuf messages
+     * @throws CodecException if one or more of the given objects cannot be
+     *                        encoded
+     */
+    List<M> encodeAll(Collection<P> objects, X metadata, PiPipeconf pipeconf)
+            throws CodecException {
+        checkNotNull(objects);
+        if (objects.isEmpty()) {
+            return ImmutableList.of();
+        }
+        final List<M> messages = encodeAllSkipException(objects, metadata, pipeconf);
+        if (objects.size() != messages.size()) {
+            throw new CodecException(format(
+                    "Unable to encode %d entities of %d given " +
+                            "(see previous logs for details)",
+                    objects.size() - messages.size(), objects.size()));
+        }
+        return messages;
+    }
+
+    /**
+     * Decodes the given collection of protobuf messages. Throws an exception if
+     * one or more of the given protobuf messages cannot be decoded. The
+     * returned list is guaranteed to have same size and order as the given
+     * one.
+     *
+     * @param messages list of protobuf messages
+     * @param metadata metadata
+     * @param pipeconf pipeconf
+     * @return list of objects
+     * @throws CodecException if one or more of the given protobuf messages
+     *                        cannot be decoded
+     */
+    List<P> decodeAll(Collection<M> messages, X metadata, PiPipeconf pipeconf)
+            throws CodecException {
+        checkNotNull(messages);
+        if (messages.isEmpty()) {
+            return ImmutableList.of();
+        }
+        final List<P> objects = decodeAllSkipException(messages, metadata, pipeconf);
+        if (messages.size() != objects.size()) {
+            throw new CodecException(format(
+                    "Unable to decode %d messages of %d given " +
+                            "(see previous logs for details)",
+                    messages.size() - objects.size(), messages.size()));
+        }
+        return objects;
+    }
+
+    /**
+     * Returns a P4Info browser for the given pipeconf or throws a
+     * CodecException if not possible.
+     *
+     * @param pipeconf pipeconf
+     * @return P4Info browser
+     * @throws CodecException if a P4Info browser cannot be obtained
+     */
+    P4InfoBrowser browserOrFail(PiPipeconf pipeconf) throws CodecException {
+        checkNotNull(pipeconf);
+        final P4InfoBrowser browser = PipeconfHelper.getP4InfoBrowser(pipeconf);
+        if (browser == null) {
+            throw new CodecException(format(
+                    "Unable to get P4InfoBrowser for pipeconf %s", pipeconf.id()));
+        }
+        return browser;
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/AbstractEntityCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/AbstractEntityCodec.java
new file mode 100644
index 0000000..0b39367
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/AbstractEntityCodec.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import com.google.protobuf.Message;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiHandle;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Abstract implementation of a specialized codec that translates PI runtime
+ * entities and their handles into P4Runtime protobuf messages and vice versa.
+ * Supports also encoding to "key" P4Runtime Entity messages used in read and
+ * delete operations.
+ *
+ * @param <P> PI runtime class
+ * @param <H> PI handle class
+ * @param <M> P4Runtime protobuf message class
+ * @param <X> metadata class
+ */
+public abstract class AbstractEntityCodec
+        <P extends PiEntity, H extends PiHandle, M extends Message, X>
+        extends AbstractCodec<P, M, X> {
+
+    protected abstract M encodeKey(H handle, X metadata, PiPipeconf pipeconf,
+                                   P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException;
+
+    protected abstract M encodeKey(P piEntity, X metadata, PiPipeconf pipeconf,
+                                   P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException;
+
+    /**
+     * Returns a P4Runtime protobuf message representing the P4Runtime.Entity
+     * "key" for the given PI handle, metadata and pipeconf.
+     *
+     * @param handle   PI handle instance
+     * @param metadata metadata
+     * @param pipeconf pipeconf
+     * @return P4Runtime protobuf message
+     * @throws CodecException if the given PI entity cannot be encoded (see
+     *                        exception message)
+     */
+    public M encodeKey(H handle, X metadata, PiPipeconf pipeconf)
+            throws CodecException {
+        checkNotNull(handle);
+        try {
+            return encodeKey(handle, metadata, pipeconf, browserOrFail(pipeconf));
+        } catch (P4InfoBrowser.NotFoundException e) {
+            throw new CodecException(e.getMessage());
+        }
+    }
+
+    /**
+     * Returns a P4Runtime protobuf message representing the P4Runtime.Entity
+     * "key" for the given PI entity, metadata and pipeconf.
+     *
+     * @param piEntity PI entity instance
+     * @param metadata metadata
+     * @param pipeconf pipeconf
+     * @return P4Runtime protobuf message
+     * @throws CodecException if the given PI entity cannot be encoded (see
+     *                        exception message)
+     */
+    public M encodeKey(P piEntity, X metadata, PiPipeconf pipeconf)
+            throws CodecException {
+        checkNotNull(piEntity);
+        try {
+            return encodeKey(piEntity, metadata, pipeconf, browserOrFail(pipeconf));
+        } catch (P4InfoBrowser.NotFoundException e) {
+            throw new CodecException(e.getMessage());
+        }
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionCodec.java
new file mode 100644
index 0000000..3ae46c9
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionCodec.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import com.google.protobuf.ByteString;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.net.pi.model.PiActionId;
+import org.onosproject.net.pi.model.PiActionParamId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiAction;
+import org.onosproject.net.pi.runtime.PiActionParam;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.config.v1.P4InfoOuterClass;
+import p4.v1.P4RuntimeOuterClass;
+
+import static java.lang.String.format;
+import static org.onosproject.p4runtime.ctl.codec.Utils.assertSize;
+
+/**
+ * Codec for P4Runtime Action.
+ */
+public final class ActionCodec
+        extends AbstractCodec<PiAction, P4RuntimeOuterClass.Action, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.Action encode(
+            PiAction piAction, Object ignored, PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        final int actionId = browser.actions()
+                .getByName(piAction.id().toString()).getPreamble().getId();
+        final P4RuntimeOuterClass.Action.Builder actionMsgBuilder =
+                P4RuntimeOuterClass.Action.newBuilder().setActionId(actionId);
+        for (PiActionParam p : piAction.parameters()) {
+            final P4InfoOuterClass.Action.Param paramInfo = browser.actionParams(actionId)
+                    .getByName(p.id().toString());
+            final ByteString paramValue = ByteString.copyFrom(p.value().asReadOnlyBuffer());
+            assertSize(format("param '%s' of action '%s'", p.id(), piAction.id()),
+                       paramValue, paramInfo.getBitwidth());
+            actionMsgBuilder.addParams(P4RuntimeOuterClass.Action.Param.newBuilder()
+                                               .setParamId(paramInfo.getId())
+                                               .setValue(paramValue)
+                                               .build());
+        }
+        return actionMsgBuilder.build();
+    }
+
+    @Override
+    protected PiAction decode(
+            P4RuntimeOuterClass.Action message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final P4InfoBrowser.EntityBrowser<P4InfoOuterClass.Action.Param> paramInfo =
+                browser.actionParams(message.getActionId());
+        final String actionName = browser.actions()
+                .getById(message.getActionId())
+                .getPreamble().getName();
+        final PiAction.Builder builder = PiAction.builder()
+                .withId(PiActionId.of(actionName));
+        for (P4RuntimeOuterClass.Action.Param p : message.getParamsList()) {
+            final String paramName = paramInfo.getById(p.getParamId()).getName();
+            final ImmutableByteSequence value = ImmutableByteSequence.copyFrom(
+                    p.getValue().toByteArray());
+            builder.withParameter(new PiActionParam(PiActionParamId.of(paramName), value));
+        }
+        return builder.build();
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionProfileGroupCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionProfileGroupCodec.java
new file mode 100644
index 0000000..5110274
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionProfileGroupCodec.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiActionProfileId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiActionProfileGroup;
+import org.onosproject.net.pi.runtime.PiActionProfileGroupHandle;
+import org.onosproject.net.pi.runtime.PiActionProfileGroupId;
+import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass.ActionProfileGroup;
+
+/**
+ * Codec for P4Runtime ActionProfileGroup.
+ */
+public final class ActionProfileGroupCodec
+        extends AbstractEntityCodec<PiActionProfileGroup, PiActionProfileGroupHandle, ActionProfileGroup, Object> {
+
+    @Override
+    public ActionProfileGroup encode(
+            PiActionProfileGroup piGroup, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final ActionProfileGroup.Builder msgBuilder = keyMsgBuilder(
+                piGroup.actionProfile(), piGroup.id(), browser)
+                .setMaxSize(piGroup.maxSize());
+        piGroup.members().forEach(m -> {
+            // TODO: currently we don't set "watch" field
+            ActionProfileGroup.Member member = ActionProfileGroup.Member.newBuilder()
+                    .setMemberId(m.id().id())
+                    .setWeight(m.weight())
+                    .build();
+            msgBuilder.addMembers(member);
+        });
+        return msgBuilder.build();
+    }
+
+    @Override
+    protected ActionProfileGroup encodeKey(
+            PiActionProfileGroupHandle handle, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(handle.actionProfile(), handle.groupId(), browser)
+                .build();
+    }
+
+    @Override
+    protected ActionProfileGroup encodeKey(
+            PiActionProfileGroup piEntity, Object metadata,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(piEntity.actionProfile(), piEntity.id(), browser)
+                .build();
+    }
+
+    private ActionProfileGroup.Builder keyMsgBuilder(
+            PiActionProfileId actProfId, PiActionProfileGroupId groupId,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return ActionProfileGroup.newBuilder()
+                .setGroupId(groupId.id())
+                .setActionProfileId(browser.actionProfiles()
+                                            .getByName(actProfId.id())
+                                            .getPreamble().getId());
+    }
+
+    @Override
+    public PiActionProfileGroup decode(
+            ActionProfileGroup msg, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final PiActionProfileGroup.Builder piGroupBuilder = PiActionProfileGroup.builder()
+                .withActionProfileId(PiActionProfileId.of(
+                        browser.actionProfiles()
+                                .getById(msg.getActionProfileId())
+                                .getPreamble().getName()))
+                .withId(PiActionProfileGroupId.of(msg.getGroupId()))
+                .withMaxSize(msg.getMaxSize());
+        msg.getMembersList().forEach(m -> {
+            final int weight;
+            if (m.getWeight() < 1) {
+                log.warn("Decoding group with invalid weight '{}', will set to 1",
+                         m.getWeight());
+                weight = 1;
+            } else {
+                weight = m.getWeight();
+            }
+            piGroupBuilder.addMember(
+                PiActionProfileMemberId.of(m.getMemberId()), weight);
+        });
+        return piGroupBuilder.build();
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionProfileMemberCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionProfileMemberCodec.java
new file mode 100644
index 0000000..183d827
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/ActionProfileMemberCodec.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiActionProfileId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiActionProfileMember;
+import org.onosproject.net.pi.runtime.PiActionProfileMemberHandle;
+import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+import p4.v1.P4RuntimeOuterClass.ActionProfileMember;
+
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+/**
+ * Codec for ActionProfileMember.
+ */
+public final class ActionProfileMemberCodec
+        extends AbstractEntityCodec<PiActionProfileMember, PiActionProfileMemberHandle, ActionProfileMember, Object> {
+
+    @Override
+    public ActionProfileMember encode(
+            PiActionProfileMember piEntity, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(
+                piEntity.actionProfile(), piEntity.id(), browser)
+                .setAction(CODECS.action().encode(
+                        piEntity.action(), null, pipeconf))
+                .build();
+    }
+
+    @Override
+    protected ActionProfileMember encodeKey(
+            PiActionProfileMemberHandle handle, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(handle.actionProfileId(), handle.memberId(), browser)
+                .build();
+    }
+
+    @Override
+    protected ActionProfileMember encodeKey(
+            PiActionProfileMember piEntity, Object metadata,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(
+                piEntity.actionProfile(), piEntity.id(), browser)
+                .build();
+    }
+
+    private P4RuntimeOuterClass.ActionProfileMember.Builder keyMsgBuilder(
+            PiActionProfileId actProfId, PiActionProfileMemberId memberId,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return P4RuntimeOuterClass.ActionProfileMember.newBuilder()
+                .setActionProfileId(browser.actionProfiles()
+                                            .getByName(actProfId.id())
+                                            .getPreamble().getId())
+                .setMemberId(memberId.id());
+    }
+
+    @Override
+    public PiActionProfileMember decode(
+            ActionProfileMember message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException, CodecException {
+        final PiActionProfileId actionProfileId = PiActionProfileId.of(
+                browser.actionProfiles()
+                        .getById(message.getActionProfileId())
+                        .getPreamble()
+                        .getName());
+        return PiActionProfileMember.builder()
+                .forActionProfile(actionProfileId)
+                .withId(PiActionProfileMemberId.of(message.getMemberId()))
+                .withAction(CODECS.action().decode(
+                        message.getAction(), null, pipeconf))
+                .build();
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/CloneSessionEntryCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/CloneSessionEntryCodec.java
new file mode 100644
index 0000000..337d76a
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/CloneSessionEntryCodec.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiCloneSessionEntry;
+import org.onosproject.net.pi.runtime.PiCloneSessionEntryHandle;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+/**
+ * Codec for P4Runtime CloneSessionEntry.
+ */
+public final class CloneSessionEntryCodec
+        extends AbstractEntityCodec<PiCloneSessionEntry, PiCloneSessionEntryHandle,
+        P4RuntimeOuterClass.CloneSessionEntry, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.CloneSessionEntry encode(
+            PiCloneSessionEntry piEntity, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser) throws CodecException {
+        return P4RuntimeOuterClass.CloneSessionEntry.newBuilder()
+                .setSessionId(piEntity.sessionId())
+                .addAllReplicas(
+                        CODECS.preReplica().encodeAll(
+                                piEntity.replicas(), null, pipeconf))
+                .setClassOfService(piEntity.classOfService())
+                .setPacketLengthBytes(piEntity.maxPacketLengthBytes())
+                .build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.CloneSessionEntry encodeKey(
+            PiCloneSessionEntryHandle handle, Object metadata,
+            PiPipeconf pipeconf, P4InfoBrowser browser) {
+        return P4RuntimeOuterClass.CloneSessionEntry.newBuilder()
+                .setSessionId(handle.sessionId()).build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.CloneSessionEntry encodeKey(
+            PiCloneSessionEntry piEntity, Object metadata,
+            PiPipeconf pipeconf, P4InfoBrowser browser) {
+        return P4RuntimeOuterClass.CloneSessionEntry.newBuilder()
+                .setSessionId(piEntity.sessionId()).build();
+    }
+
+    @Override
+    protected PiCloneSessionEntry decode(
+            P4RuntimeOuterClass.CloneSessionEntry message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser) throws CodecException {
+        return PiCloneSessionEntry.builder()
+                .withSessionId(message.getSessionId())
+                .addReplicas(
+                        CODECS.preReplica().decodeAll(
+                                message.getReplicasList(), null, pipeconf))
+                .withClassOfService(message.getClassOfService())
+                .withMaxPacketLengthBytes(message.getPacketLengthBytes())
+                .build();
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/CodecException.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/CodecException.java
new file mode 100644
index 0000000..6ef77f2
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/CodecException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+/**
+ * Signals an error during encoding/decoding of a PI entity/protobuf message.
+ */
+public final class CodecException extends Exception {
+
+    /**
+     * Creates a new exception with the given explanation message.
+     *
+     * @param explanation explanation
+     */
+    public CodecException(String explanation) {
+        super(explanation);
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/Codecs.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/Codecs.java
new file mode 100644
index 0000000..30e8d9d
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/Codecs.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+/**
+ * Utility class that provides access to P4Runtime codec instances.
+ */
+public final class Codecs {
+
+    public static final Codecs CODECS = new Codecs();
+
+    private final ActionCodec action;
+    private final ActionProfileGroupCodec actionProfileGroup;
+    private final ActionProfileMemberCodec actionProfileMember;
+    private final CounterEntryCodec counterEntry;
+    private final DirectCounterEntryCodec directCounterEntry;
+    private final DirectMeterEntryCodec directMeterEntry;
+    private final EntityCodec entity;
+    private final FieldMatchCodec fieldMatch;
+    private final HandleCodec handle;
+    private final MeterEntryCodec meterEntry;
+    private final MulticastGroupEntryCodec multicastGroupEntry;
+    private final CloneSessionEntryCodec cloneSessionEntry;
+    private final PreReplicaCodec preReplica;
+    private final PacketInCodec packetIn;
+    private final PacketMetadataCodec packetMetadata;
+    private final PacketOutCodec packetOut;
+    private final TableEntryCodec tableEntry;
+
+    private Codecs() {
+        this.action = new ActionCodec();
+        this.actionProfileGroup = new ActionProfileGroupCodec();
+        this.actionProfileMember = new ActionProfileMemberCodec();
+        this.counterEntry = new CounterEntryCodec();
+        this.directCounterEntry = new DirectCounterEntryCodec();
+        this.directMeterEntry = new DirectMeterEntryCodec();
+        this.entity = new EntityCodec();
+        this.fieldMatch = new FieldMatchCodec();
+        this.handle = new HandleCodec();
+        this.meterEntry = new MeterEntryCodec();
+        this.multicastGroupEntry = new MulticastGroupEntryCodec();
+        this.cloneSessionEntry = new CloneSessionEntryCodec();
+        this.preReplica = new PreReplicaCodec();
+        this.packetIn = new PacketInCodec();
+        this.packetMetadata = new PacketMetadataCodec();
+        this.packetOut = new PacketOutCodec();
+        this.tableEntry = new TableEntryCodec();
+    }
+
+    public EntityCodec entity() {
+        return entity;
+    }
+
+    public HandleCodec handle() {
+        return handle;
+    }
+
+    public PacketOutCodec packetOut() {
+        return packetOut;
+    }
+
+    public PacketInCodec packetIn() {
+        return packetIn;
+    }
+
+    TableEntryCodec tableEntry() {
+        return tableEntry;
+    }
+
+    FieldMatchCodec fieldMatch() {
+        return fieldMatch;
+    }
+
+    ActionCodec action() {
+        return action;
+    }
+
+    ActionProfileMemberCodec actionProfileMember() {
+        return actionProfileMember;
+    }
+
+    ActionProfileGroupCodec actionProfileGroup() {
+        return actionProfileGroup;
+    }
+
+    PacketMetadataCodec packetMetadata() {
+        return packetMetadata;
+    }
+
+    MulticastGroupEntryCodec multicastGroupEntry() {
+        return multicastGroupEntry;
+    }
+
+    CloneSessionEntryCodec cloneSessionEntry() {
+        return cloneSessionEntry;
+    }
+
+    PreReplicaCodec preReplica() {
+        return preReplica;
+    }
+
+    DirectMeterEntryCodec directMeterEntry() {
+        return directMeterEntry;
+    }
+
+    MeterEntryCodec meterEntry() {
+        return meterEntry;
+    }
+
+    CounterEntryCodec counterEntry() {
+        return counterEntry;
+    }
+
+    DirectCounterEntryCodec directCounterEntry() {
+        return directCounterEntry;
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/CounterEntryCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/CounterEntryCodec.java
new file mode 100644
index 0000000..deac993
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/CounterEntryCodec.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiCounterId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiCounterCell;
+import org.onosproject.net.pi.runtime.PiCounterCellHandle;
+import org.onosproject.net.pi.runtime.PiCounterCellId;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+
+/**
+ * Codec for P4Runtime CounterEntry.
+ */
+public final class CounterEntryCodec
+        extends AbstractEntityCodec<PiCounterCell, PiCounterCellHandle,
+        P4RuntimeOuterClass.CounterEntry, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.CounterEntry encode(
+            PiCounterCell piEntity, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(piEntity.cellId(), browser)
+                .setData(P4RuntimeOuterClass.CounterData.newBuilder()
+                                 .setByteCount(piEntity.data().bytes())
+                                 .setPacketCount(piEntity.data().packets())
+                                 .build())
+                .build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.CounterEntry encodeKey(
+            PiCounterCellHandle handle, Object metadata, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(handle.cellId(), browser).build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.CounterEntry encodeKey(
+            PiCounterCell piEntity, Object metadata, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(piEntity.cellId(), browser).build();
+    }
+
+    private P4RuntimeOuterClass.CounterEntry.Builder keyMsgBuilder(
+            PiCounterCellId cellId, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final int counterId = browser.counters().getByName(
+                cellId.counterId().id()).getPreamble().getId();
+        return P4RuntimeOuterClass.CounterEntry.newBuilder()
+                .setCounterId(counterId)
+                .setIndex(P4RuntimeOuterClass.Index.newBuilder()
+                                  .setIndex(cellId.index()).build());
+    }
+
+    @Override
+    protected PiCounterCell decode(
+            P4RuntimeOuterClass.CounterEntry message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final String counterName = browser.counters()
+                .getById(message.getCounterId())
+                .getPreamble()
+                .getName();
+        return new PiCounterCell(
+                PiCounterCellId.ofIndirect(
+                        PiCounterId.of(counterName), message.getIndex().getIndex()),
+                message.getData().getPacketCount(),
+                message.getData().getByteCount());
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/DirectCounterEntryCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/DirectCounterEntryCodec.java
new file mode 100644
index 0000000..46d0f3f
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/DirectCounterEntryCodec.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiCounterCell;
+import org.onosproject.net.pi.runtime.PiCounterCellHandle;
+import org.onosproject.net.pi.runtime.PiCounterCellId;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+/**
+ * Codec for P4Runtime DirectCounterEntryCodec.
+ */
+public final class DirectCounterEntryCodec
+        extends AbstractEntityCodec<PiCounterCell, PiCounterCellHandle,
+        P4RuntimeOuterClass.DirectCounterEntry, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.DirectCounterEntry encode(
+            PiCounterCell piEntity, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws CodecException {
+        return keyMsgBuilder(piEntity.cellId(), pipeconf)
+                .setData(P4RuntimeOuterClass.CounterData.newBuilder()
+                                 .setByteCount(piEntity.data().bytes())
+                                 .setPacketCount(piEntity.data().packets())
+                                 .build())
+                .build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.DirectCounterEntry encodeKey(
+            PiCounterCellHandle handle, Object metadata, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws CodecException {
+        return keyMsgBuilder(handle.cellId(), pipeconf).build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.DirectCounterEntry encodeKey(
+            PiCounterCell piEntity, Object metadata,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException {
+        return keyMsgBuilder(piEntity.cellId(), pipeconf).build();
+    }
+
+    private P4RuntimeOuterClass.DirectCounterEntry.Builder keyMsgBuilder(
+            PiCounterCellId cellId, PiPipeconf pipeconf)
+            throws CodecException {
+        return P4RuntimeOuterClass.DirectCounterEntry.newBuilder()
+                .setTableEntry(CODECS.tableEntry().encodeKey(
+                        cellId.tableEntry(), null, pipeconf));
+    }
+
+    @Override
+    protected PiCounterCell decode(
+            P4RuntimeOuterClass.DirectCounterEntry message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException {
+        return new PiCounterCell(
+                PiCounterCellId.ofDirect(
+                        CODECS.tableEntry().decode(
+                                message.getTableEntry(), null, pipeconf)),
+                message.getData().getPacketCount(),
+                message.getData().getByteCount());
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/DirectMeterEntryCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/DirectMeterEntryCodec.java
new file mode 100644
index 0000000..3bedfbf
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/DirectMeterEntryCodec.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiMeterBand;
+import org.onosproject.net.pi.runtime.PiMeterCellConfig;
+import org.onosproject.net.pi.runtime.PiMeterCellHandle;
+import org.onosproject.net.pi.runtime.PiMeterCellId;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+/**
+ * Codec for P4Runtime DirectMeterEntryCodec.
+ */
+public final class DirectMeterEntryCodec
+        extends AbstractEntityCodec<PiMeterCellConfig, PiMeterCellHandle,
+        P4RuntimeOuterClass.DirectMeterEntry, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.DirectMeterEntry encode(
+            PiMeterCellConfig piEntity, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws CodecException {
+        return P4RuntimeOuterClass.DirectMeterEntry.newBuilder()
+                .setTableEntry(CODECS.tableEntry().encode(
+                        piEntity.cellId().tableEntry(), null, pipeconf))
+                .setConfig(MeterEntryCodec.getP4Config(piEntity))
+                .build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.DirectMeterEntry encodeKey(
+            PiMeterCellHandle handle, Object metadata, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws CodecException {
+        return keyMsgBuilder(handle.cellId(), pipeconf).build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.DirectMeterEntry encodeKey(
+            PiMeterCellConfig piEntity, Object metadata,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException {
+        return keyMsgBuilder(piEntity.cellId(), pipeconf).build();
+    }
+
+    private P4RuntimeOuterClass.DirectMeterEntry.Builder keyMsgBuilder(
+            PiMeterCellId cellId, PiPipeconf pipeconf)
+            throws CodecException {
+        return P4RuntimeOuterClass.DirectMeterEntry.newBuilder()
+                .setTableEntry(CODECS.tableEntry().encodeKey(
+                        cellId.tableEntry(), null, pipeconf));
+    }
+
+    @Override
+    protected PiMeterCellConfig decode(
+            P4RuntimeOuterClass.DirectMeterEntry message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException {
+        return PiMeterCellConfig.builder()
+                .withMeterCellId(PiMeterCellId.ofDirect(
+                        CODECS.tableEntry().decode(
+                                message.getTableEntry(), null, pipeconf)))
+                .withMeterBand(new PiMeterBand(message.getConfig().getCir(),
+                                               message.getConfig().getCburst()))
+                .withMeterBand(new PiMeterBand(message.getConfig().getPir(),
+                                               message.getConfig().getPburst()))
+                .build();
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/EntityCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/EntityCodec.java
new file mode 100644
index 0000000..e063faa
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/EntityCodec.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiActionProfileGroup;
+import org.onosproject.net.pi.runtime.PiActionProfileMember;
+import org.onosproject.net.pi.runtime.PiCloneSessionEntry;
+import org.onosproject.net.pi.runtime.PiCounterCell;
+import org.onosproject.net.pi.runtime.PiEntity;
+import org.onosproject.net.pi.runtime.PiMeterCellConfig;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
+import org.onosproject.net.pi.runtime.PiPreEntry;
+import org.onosproject.net.pi.runtime.PiTableEntry;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+
+import static java.lang.String.format;
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+/**
+ * Codec for P4Runtime Entity.
+ */
+public final class EntityCodec extends AbstractCodec<PiEntity, P4RuntimeOuterClass.Entity, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.Entity encode(
+            PiEntity piEntity, Object ignored, PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException {
+        final P4RuntimeOuterClass.Entity.Builder p4Entity = P4RuntimeOuterClass.Entity.newBuilder();
+        switch (piEntity.piEntityType()) {
+            case TABLE_ENTRY:
+                return p4Entity.setTableEntry(
+                        CODECS.tableEntry().encode(
+                                (PiTableEntry) piEntity, null, pipeconf))
+                        .build();
+            case ACTION_PROFILE_GROUP:
+                return p4Entity.setActionProfileGroup(
+                        CODECS.actionProfileGroup().encode(
+                                (PiActionProfileGroup) piEntity, null, pipeconf))
+                        .build();
+            case ACTION_PROFILE_MEMBER:
+                return p4Entity.setActionProfileMember(
+                        CODECS.actionProfileMember().encode(
+                                (PiActionProfileMember) piEntity, null, pipeconf))
+                        .build();
+            case PRE_ENTRY:
+                final PiPreEntry preEntry = (PiPreEntry) piEntity;
+                switch (preEntry.preEntryType()) {
+                    case MULTICAST_GROUP:
+                        return p4Entity.setPacketReplicationEngineEntry(
+                                P4RuntimeOuterClass.PacketReplicationEngineEntry.newBuilder()
+                                        .setMulticastGroupEntry(CODECS.multicastGroupEntry().encode(
+                                                (PiMulticastGroupEntry) piEntity, null, pipeconf))
+                                        .build())
+                                .build();
+                    case CLONE_SESSION:
+                        return p4Entity.setPacketReplicationEngineEntry(
+                                P4RuntimeOuterClass.PacketReplicationEngineEntry.newBuilder()
+                                        .setCloneSessionEntry(CODECS.cloneSessionEntry().encode(
+                                                (PiCloneSessionEntry) piEntity, null, pipeconf))
+                                        .build())
+                                .build();
+                    default:
+                        throw new CodecException(format(
+                                "Encoding of %s of type %s is not supported",
+                                piEntity.piEntityType(),
+                                preEntry.preEntryType()));
+                }
+            case METER_CELL_CONFIG:
+                final PiMeterCellConfig meterCellConfig = (PiMeterCellConfig) piEntity;
+                switch (meterCellConfig.cellId().meterType()) {
+                    case DIRECT:
+                        return p4Entity.setDirectMeterEntry(
+                                CODECS.directMeterEntry().encode(
+                                        meterCellConfig, null, pipeconf))
+                                .build();
+                    case INDIRECT:
+                        return p4Entity.setMeterEntry(
+                                CODECS.meterEntry().encode(
+                                        meterCellConfig, null, pipeconf))
+                                .build();
+                    default:
+                        throw new CodecException(format(
+                                "Encoding of %s of type %s is not supported",
+                                piEntity.piEntityType(),
+                                meterCellConfig.cellId().meterType()));
+                }
+            case COUNTER_CELL:
+                final PiCounterCell counterCell = (PiCounterCell) piEntity;
+                switch (counterCell.cellId().counterType()) {
+                    case DIRECT:
+                        return p4Entity.setDirectCounterEntry(
+                                CODECS.directCounterEntry().encode(
+                                        counterCell, null, pipeconf))
+                                .build();
+                    case INDIRECT:
+                        return p4Entity.setCounterEntry(
+                                CODECS.counterEntry().encode(
+                                        counterCell, null, pipeconf))
+                                .build();
+                    default:
+                        throw new CodecException(format(
+                                "Encoding of %s of type %s is not supported",
+                                piEntity.piEntityType(),
+                                counterCell.cellId().counterType()));
+                }
+            case REGISTER_CELL:
+            default:
+                throw new CodecException(format(
+                        "Encoding of %s not supported",
+                        piEntity.piEntityType()));
+        }
+    }
+
+    @Override
+    protected PiEntity decode(
+            P4RuntimeOuterClass.Entity message, Object ignored, PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException {
+        switch (message.getEntityCase()) {
+            case TABLE_ENTRY:
+                return CODECS.tableEntry().decode(
+                        message.getTableEntry(), null, pipeconf);
+            case ACTION_PROFILE_MEMBER:
+                return CODECS.actionProfileMember().decode(
+                        message.getActionProfileMember(), null, pipeconf);
+            case ACTION_PROFILE_GROUP:
+                return CODECS.actionProfileGroup().decode(
+                        message.getActionProfileGroup(), null, pipeconf);
+            case METER_ENTRY:
+                return CODECS.meterEntry().decode(
+                        message.getMeterEntry(), null, pipeconf);
+            case DIRECT_METER_ENTRY:
+                return CODECS.directMeterEntry().decode(
+                        message.getDirectMeterEntry(), null, pipeconf);
+            case COUNTER_ENTRY:
+                return CODECS.counterEntry().decode(
+                        message.getCounterEntry(), null, pipeconf);
+            case DIRECT_COUNTER_ENTRY:
+                return CODECS.directCounterEntry().decode(
+                        message.getDirectCounterEntry(), null, pipeconf);
+            case PACKET_REPLICATION_ENGINE_ENTRY:
+                switch (message.getPacketReplicationEngineEntry().getTypeCase()) {
+                    case MULTICAST_GROUP_ENTRY:
+                        return CODECS.multicastGroupEntry().decode(
+                                message.getPacketReplicationEngineEntry()
+                                        .getMulticastGroupEntry(), null, pipeconf);
+                    case CLONE_SESSION_ENTRY:
+                        return CODECS.cloneSessionEntry().decode(
+                                message.getPacketReplicationEngineEntry()
+                                        .getCloneSessionEntry(), null, pipeconf);
+                    case TYPE_NOT_SET:
+                    default:
+                        throw new CodecException(format(
+                                "Decoding of %s of type %s not supported",
+                                message.getEntityCase(),
+                                message.getPacketReplicationEngineEntry().getTypeCase()));
+                }
+            case VALUE_SET_ENTRY:
+            case REGISTER_ENTRY:
+            case DIGEST_ENTRY:
+            case EXTERN_ENTRY:
+            case ENTITY_NOT_SET:
+            default:
+                throw new CodecException(format(
+                        "Decoding of %s not supported",
+                        message.getEntityCase()));
+
+        }
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/FieldMatchCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/FieldMatchCodec.java
new file mode 100644
index 0000000..f289d5d
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/FieldMatchCodec.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import com.google.protobuf.ByteString;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.net.pi.model.PiMatchFieldId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiExactFieldMatch;
+import org.onosproject.net.pi.runtime.PiFieldMatch;
+import org.onosproject.net.pi.runtime.PiLpmFieldMatch;
+import org.onosproject.net.pi.runtime.PiRangeFieldMatch;
+import org.onosproject.net.pi.runtime.PiTernaryFieldMatch;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.config.v1.P4InfoOuterClass;
+import p4.v1.P4RuntimeOuterClass;
+
+import static java.lang.String.format;
+import static org.onlab.util.ImmutableByteSequence.copyFrom;
+import static org.onosproject.p4runtime.ctl.codec.Utils.assertPrefixLen;
+import static org.onosproject.p4runtime.ctl.codec.Utils.assertSize;
+
+/**
+ * Codec for P4Runtime FieldMatch. Metadata is expected to be a Preamble for
+ * P4Info.Table.
+ */
+public final class FieldMatchCodec
+        extends AbstractCodec<PiFieldMatch, P4RuntimeOuterClass.FieldMatch,
+        P4InfoOuterClass.Preamble> {
+
+    private static final String VALUE_OF_PREFIX = "value of ";
+    private static final String MASK_OF_PREFIX = "mask of ";
+    private static final String HIGH_RANGE_VALUE_OF_PREFIX = "high range value of ";
+    private static final String LOW_RANGE_VALUE_OF_PREFIX = "low range value of ";
+
+    @Override
+    public P4RuntimeOuterClass.FieldMatch encode(
+            PiFieldMatch piFieldMatch, P4InfoOuterClass.Preamble tablePreamble,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+
+        P4RuntimeOuterClass.FieldMatch.Builder messageBuilder = P4RuntimeOuterClass
+                .FieldMatch.newBuilder();
+
+        // FIXME: check how field names for stacked headers are constructed in P4Runtime.
+        String fieldName = piFieldMatch.fieldId().id();
+        P4InfoOuterClass.MatchField matchFieldInfo = browser.matchFields(
+                tablePreamble.getId()).getByName(fieldName);
+        String entityName = format("field match '%s' of table '%s'",
+                                   matchFieldInfo.getName(), tablePreamble.getName());
+        int fieldId = matchFieldInfo.getId();
+        int fieldBitwidth = matchFieldInfo.getBitwidth();
+
+        messageBuilder.setFieldId(fieldId);
+
+        switch (piFieldMatch.type()) {
+            case EXACT:
+                PiExactFieldMatch fieldMatch = (PiExactFieldMatch) piFieldMatch;
+                ByteString exactValue = ByteString.copyFrom(fieldMatch.value().asReadOnlyBuffer());
+                assertSize(VALUE_OF_PREFIX + entityName, exactValue, fieldBitwidth);
+                return messageBuilder.setExact(
+                        P4RuntimeOuterClass.FieldMatch.Exact
+                                .newBuilder()
+                                .setValue(exactValue)
+                                .build())
+                        .build();
+            case TERNARY:
+                PiTernaryFieldMatch ternaryMatch = (PiTernaryFieldMatch) piFieldMatch;
+                ByteString ternaryValue = ByteString.copyFrom(ternaryMatch.value().asReadOnlyBuffer());
+                ByteString ternaryMask = ByteString.copyFrom(ternaryMatch.mask().asReadOnlyBuffer());
+                assertSize(VALUE_OF_PREFIX + entityName, ternaryValue, fieldBitwidth);
+                assertSize(MASK_OF_PREFIX + entityName, ternaryMask, fieldBitwidth);
+                return messageBuilder.setTernary(
+                        P4RuntimeOuterClass.FieldMatch.Ternary
+                                .newBuilder()
+                                .setValue(ternaryValue)
+                                .setMask(ternaryMask)
+                                .build())
+                        .build();
+            case LPM:
+                PiLpmFieldMatch lpmMatch = (PiLpmFieldMatch) piFieldMatch;
+                ByteString lpmValue = ByteString.copyFrom(lpmMatch.value().asReadOnlyBuffer());
+                int lpmPrefixLen = lpmMatch.prefixLength();
+                assertSize(VALUE_OF_PREFIX + entityName, lpmValue, fieldBitwidth);
+                assertPrefixLen(entityName, lpmPrefixLen, fieldBitwidth);
+                return messageBuilder.setLpm(
+                        P4RuntimeOuterClass.FieldMatch.LPM.newBuilder()
+                                .setValue(lpmValue)
+                                .setPrefixLen(lpmPrefixLen)
+                                .build())
+                        .build();
+            case RANGE:
+                PiRangeFieldMatch rangeMatch = (PiRangeFieldMatch) piFieldMatch;
+                ByteString rangeHighValue = ByteString.copyFrom(rangeMatch.highValue().asReadOnlyBuffer());
+                ByteString rangeLowValue = ByteString.copyFrom(rangeMatch.lowValue().asReadOnlyBuffer());
+                assertSize(HIGH_RANGE_VALUE_OF_PREFIX + entityName, rangeHighValue, fieldBitwidth);
+                assertSize(LOW_RANGE_VALUE_OF_PREFIX + entityName, rangeLowValue, fieldBitwidth);
+                return messageBuilder.setRange(
+                        P4RuntimeOuterClass.FieldMatch.Range.newBuilder()
+                                .setHigh(rangeHighValue)
+                                .setLow(rangeLowValue)
+                                .build())
+                        .build();
+            default:
+                throw new CodecException(format(
+                        "Building of match type %s not implemented", piFieldMatch.type()));
+        }
+    }
+
+    @Override
+    public PiFieldMatch decode(
+            P4RuntimeOuterClass.FieldMatch message, P4InfoOuterClass.Preamble tablePreamble,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+
+        String fieldMatchName = browser.matchFields(tablePreamble.getId())
+                .getById(message.getFieldId()).getName();
+        PiMatchFieldId headerFieldId = PiMatchFieldId.of(fieldMatchName);
+
+        P4RuntimeOuterClass.FieldMatch.FieldMatchTypeCase typeCase = message.getFieldMatchTypeCase();
+
+        switch (typeCase) {
+            case EXACT:
+                P4RuntimeOuterClass.FieldMatch.Exact exactFieldMatch = message.getExact();
+                ImmutableByteSequence exactValue = copyFrom(exactFieldMatch.getValue().asReadOnlyByteBuffer());
+                return new PiExactFieldMatch(headerFieldId, exactValue);
+            case TERNARY:
+                P4RuntimeOuterClass.FieldMatch.Ternary ternaryFieldMatch = message.getTernary();
+                ImmutableByteSequence ternaryValue = copyFrom(ternaryFieldMatch.getValue().asReadOnlyByteBuffer());
+                ImmutableByteSequence ternaryMask = copyFrom(ternaryFieldMatch.getMask().asReadOnlyByteBuffer());
+                return new PiTernaryFieldMatch(headerFieldId, ternaryValue, ternaryMask);
+            case LPM:
+                P4RuntimeOuterClass.FieldMatch.LPM lpmFieldMatch = message.getLpm();
+                ImmutableByteSequence lpmValue = copyFrom(lpmFieldMatch.getValue().asReadOnlyByteBuffer());
+                int lpmPrefixLen = lpmFieldMatch.getPrefixLen();
+                return new PiLpmFieldMatch(headerFieldId, lpmValue, lpmPrefixLen);
+            case RANGE:
+                P4RuntimeOuterClass.FieldMatch.Range rangeFieldMatch = message.getRange();
+                ImmutableByteSequence rangeHighValue = copyFrom(rangeFieldMatch.getHigh().asReadOnlyByteBuffer());
+                ImmutableByteSequence rangeLowValue = copyFrom(rangeFieldMatch.getLow().asReadOnlyByteBuffer());
+                return new PiRangeFieldMatch(headerFieldId, rangeLowValue, rangeHighValue);
+            default:
+                throw new CodecException(format(
+                        "Decoding of field match type '%s' not implemented", typeCase.name()));
+        }
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/HandleCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/HandleCodec.java
new file mode 100644
index 0000000..7a9c727
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/HandleCodec.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiActionProfileGroupHandle;
+import org.onosproject.net.pi.runtime.PiActionProfileMemberHandle;
+import org.onosproject.net.pi.runtime.PiCloneSessionEntryHandle;
+import org.onosproject.net.pi.runtime.PiCounterCellHandle;
+import org.onosproject.net.pi.runtime.PiHandle;
+import org.onosproject.net.pi.runtime.PiMeterCellHandle;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntryHandle;
+import org.onosproject.net.pi.runtime.PiPreEntryHandle;
+import org.onosproject.net.pi.runtime.PiTableEntryHandle;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+
+import static java.lang.String.format;
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+public final class HandleCodec extends AbstractCodec<PiHandle, P4RuntimeOuterClass.Entity, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.Entity encode(
+            PiHandle piHandle, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws CodecException {
+        final P4RuntimeOuterClass.Entity.Builder p4Entity = P4RuntimeOuterClass.Entity.newBuilder();
+
+        switch (piHandle.entityType()) {
+            case TABLE_ENTRY:
+                return p4Entity.setTableEntry(
+                        CODECS.tableEntry().encodeKey(
+                                (PiTableEntryHandle) piHandle, null, pipeconf))
+                        .build();
+            case ACTION_PROFILE_GROUP:
+                return p4Entity.setActionProfileGroup(
+                        CODECS.actionProfileGroup().encodeKey(
+                                (PiActionProfileGroupHandle) piHandle, null, pipeconf))
+                        .build();
+            case ACTION_PROFILE_MEMBER:
+                return p4Entity.setActionProfileMember(
+                        CODECS.actionProfileMember().encodeKey(
+                                (PiActionProfileMemberHandle) piHandle, null, pipeconf))
+                        .build();
+            case PRE_ENTRY:
+                final PiPreEntryHandle preEntryHandle = (PiPreEntryHandle) piHandle;
+                switch (preEntryHandle.preEntryType()) {
+                    case MULTICAST_GROUP:
+                        return p4Entity.setPacketReplicationEngineEntry(
+                                P4RuntimeOuterClass.PacketReplicationEngineEntry.newBuilder()
+                                        .setMulticastGroupEntry(CODECS.multicastGroupEntry().encodeKey(
+                                                (PiMulticastGroupEntryHandle) piHandle, null, pipeconf))
+                                        .build())
+                                .build();
+                    case CLONE_SESSION:
+                        return p4Entity.setPacketReplicationEngineEntry(
+                                P4RuntimeOuterClass.PacketReplicationEngineEntry.newBuilder()
+                                        .setCloneSessionEntry(CODECS.cloneSessionEntry().encodeKey(
+                                                (PiCloneSessionEntryHandle) piHandle, null, pipeconf))
+                                        .build())
+                                .build();
+                    default:
+                        throw new CodecException(format(
+                                "Encoding of handle for %s of type %s is not supported",
+                                piHandle.entityType(),
+                                preEntryHandle.preEntryType()));
+                }
+            case METER_CELL_CONFIG:
+                final PiMeterCellHandle meterCellHandle = (PiMeterCellHandle) piHandle;
+                switch (meterCellHandle.cellId().meterType()) {
+                    case DIRECT:
+                        return p4Entity.setDirectMeterEntry(
+                                CODECS.directMeterEntry().encodeKey(
+                                        meterCellHandle, null, pipeconf))
+                                .build();
+                    case INDIRECT:
+                        return p4Entity.setMeterEntry(
+                                CODECS.meterEntry().encodeKey(
+                                        meterCellHandle, null, pipeconf))
+                                .build();
+                    default:
+                        throw new CodecException(format(
+                                "Encoding of handle for %s of type %s is not supported",
+                                piHandle.entityType(),
+                                meterCellHandle.cellId().meterType()));
+                }
+            case COUNTER_CELL:
+                final PiCounterCellHandle counterCellHandle = (PiCounterCellHandle) piHandle;
+                switch (counterCellHandle.cellId().counterType()) {
+                    case DIRECT:
+                        return p4Entity.setDirectCounterEntry(
+                                CODECS.directCounterEntry().encodeKey(
+                                        counterCellHandle, null, pipeconf))
+                                .build();
+                    case INDIRECT:
+                        return p4Entity.setCounterEntry(
+                                CODECS.counterEntry().encodeKey(
+                                        counterCellHandle, null, pipeconf))
+                                .build();
+                    default:
+                        throw new CodecException(format(
+                                "Encoding of handle for %s of type %s is not supported",
+                                piHandle.entityType(),
+                                counterCellHandle.cellId().counterType()));
+                }
+            case REGISTER_CELL:
+            default:
+                throw new CodecException(format(
+                        "Encoding of handle for %s not supported",
+                        piHandle.entityType()));
+        }
+    }
+
+    @Override
+    protected PiHandle decode(
+            P4RuntimeOuterClass.Entity message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException {
+        throw new CodecException("Decoding of Entity to PiHandle is not supported");
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/MeterEntryCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/MeterEntryCodec.java
new file mode 100644
index 0000000..b210709
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/MeterEntryCodec.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiMeterId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiMeterBand;
+import org.onosproject.net.pi.runtime.PiMeterCellConfig;
+import org.onosproject.net.pi.runtime.PiMeterCellHandle;
+import org.onosproject.net.pi.runtime.PiMeterCellId;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+
+/**
+ * Codec for P4Runtime MeterEntry.
+ */
+public final class MeterEntryCodec
+        extends AbstractEntityCodec<PiMeterCellConfig, PiMeterCellHandle,
+        P4RuntimeOuterClass.MeterEntry, Object> {
+
+    static P4RuntimeOuterClass.MeterConfig getP4Config(PiMeterCellConfig piConfig)
+            throws CodecException {
+        if (piConfig.meterBands().size() != 2) {
+            throw new CodecException("Number of meter bands should be 2");
+        }
+        final PiMeterBand[] bands = piConfig.meterBands().toArray(new PiMeterBand[0]);
+        long cir, cburst, pir, pburst;
+        // The band with bigger burst is peak if rate of them is equal.
+        if (bands[0].rate() > bands[1].rate() ||
+                (bands[0].rate() == bands[1].rate() &&
+                        bands[0].burst() >= bands[1].burst())) {
+            cir = bands[1].rate();
+            cburst = bands[1].burst();
+            pir = bands[0].rate();
+            pburst = bands[0].burst();
+        } else {
+            cir = bands[0].rate();
+            cburst = bands[0].burst();
+            pir = bands[1].rate();
+            pburst = bands[1].burst();
+        }
+        return P4RuntimeOuterClass.MeterConfig.newBuilder()
+                .setCir(cir)
+                .setCburst(cburst)
+                .setPir(pir)
+                .setPburst(pburst)
+                .build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.MeterEntry encode(
+            PiMeterCellConfig piEntity, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException, CodecException {
+        final int meterId = browser.meters().getByName(
+                piEntity.cellId().meterId().id()).getPreamble().getId();
+        return P4RuntimeOuterClass.MeterEntry.newBuilder()
+                .setMeterId(meterId)
+                .setIndex(P4RuntimeOuterClass.Index.newBuilder()
+                                  .setIndex(piEntity.cellId().index()).build())
+                .setConfig(getP4Config(piEntity))
+                .build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.MeterEntry encodeKey(
+            PiMeterCellHandle handle, Object metadata, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(handle.cellId(), browser).build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.MeterEntry encodeKey(
+            PiMeterCellConfig piEntity, Object metadata, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(piEntity.cellId(), browser).build();
+    }
+
+    private P4RuntimeOuterClass.MeterEntry.Builder keyMsgBuilder(
+            PiMeterCellId cellId, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final int meterId = browser.meters().getByName(
+                cellId.meterId().id()).getPreamble().getId();
+        return P4RuntimeOuterClass.MeterEntry.newBuilder()
+                .setMeterId(meterId)
+                .setIndex(P4RuntimeOuterClass.Index.newBuilder()
+                                  .setIndex(cellId.index()).build());
+    }
+
+    @Override
+    protected PiMeterCellConfig decode(
+            P4RuntimeOuterClass.MeterEntry message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final String meterName = browser.meters()
+                .getById(message.getMeterId())
+                .getPreamble()
+                .getName();
+        return PiMeterCellConfig.builder()
+                .withMeterCellId(PiMeterCellId.ofIndirect(
+                        PiMeterId.of(meterName), message.getIndex().getIndex()))
+                .withMeterBand(new PiMeterBand(message.getConfig().getCir(),
+                                               message.getConfig().getCburst()))
+                .withMeterBand(new PiMeterBand(message.getConfig().getPir(),
+                                               message.getConfig().getPburst()))
+                .build();
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/MulticastGroupEntryCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/MulticastGroupEntryCodec.java
new file mode 100644
index 0000000..bcc4555
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/MulticastGroupEntryCodec.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntry;
+import org.onosproject.net.pi.runtime.PiMulticastGroupEntryHandle;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+/**
+ * Codec for P4Runtime MulticastGroupEntry.
+ */
+public final class MulticastGroupEntryCodec
+        extends AbstractEntityCodec<PiMulticastGroupEntry, PiMulticastGroupEntryHandle,
+        P4RuntimeOuterClass.MulticastGroupEntry, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.MulticastGroupEntry encode(
+            PiMulticastGroupEntry piEntity, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser) throws CodecException {
+        return P4RuntimeOuterClass.MulticastGroupEntry.newBuilder()
+                .setMulticastGroupId(piEntity.groupId())
+                .addAllReplicas(
+                        CODECS.preReplica().encodeAll(
+                                piEntity.replicas(), null, pipeconf))
+                .build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.MulticastGroupEntry encodeKey(
+            PiMulticastGroupEntryHandle handle, Object metadata,
+            PiPipeconf pipeconf, P4InfoBrowser browser) {
+        return P4RuntimeOuterClass.MulticastGroupEntry.newBuilder()
+                .setMulticastGroupId(handle.groupId()).build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.MulticastGroupEntry encodeKey(
+            PiMulticastGroupEntry piEntity, Object metadata,
+            PiPipeconf pipeconf, P4InfoBrowser browser) {
+        return P4RuntimeOuterClass.MulticastGroupEntry.newBuilder()
+                .setMulticastGroupId(piEntity.groupId()).build();
+    }
+
+    @Override
+    protected PiMulticastGroupEntry decode(
+            P4RuntimeOuterClass.MulticastGroupEntry message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser) throws CodecException {
+        return PiMulticastGroupEntry.builder()
+                .withGroupId(message.getMulticastGroupId())
+                .addReplicas(
+                        CODECS.preReplica().decodeAll(
+                                message.getReplicasList(), null, pipeconf))
+                .build();
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/P4DataCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/P4DataCodec.java
new file mode 100644
index 0000000..d55b614
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/P4DataCodec.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import com.google.protobuf.ByteString;
+import org.onlab.util.ImmutableByteSequence;
+import org.onosproject.net.pi.model.PiData;
+import org.onosproject.net.pi.runtime.data.PiBitString;
+import org.onosproject.net.pi.runtime.data.PiBool;
+import org.onosproject.net.pi.runtime.data.PiEnumString;
+import org.onosproject.net.pi.runtime.data.PiErrorString;
+import org.onosproject.net.pi.runtime.data.PiHeader;
+import org.onosproject.net.pi.runtime.data.PiHeaderStack;
+import org.onosproject.net.pi.runtime.data.PiHeaderUnion;
+import org.onosproject.net.pi.runtime.data.PiHeaderUnionStack;
+import org.onosproject.net.pi.runtime.data.PiStruct;
+import org.onosproject.net.pi.runtime.data.PiTuple;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static p4.v1.P4DataOuterClass.P4Data;
+import static p4.v1.P4DataOuterClass.P4Header;
+import static p4.v1.P4DataOuterClass.P4HeaderStack;
+import static p4.v1.P4DataOuterClass.P4HeaderUnion;
+import static p4.v1.P4DataOuterClass.P4HeaderUnionStack;
+import static p4.v1.P4DataOuterClass.P4StructLike;
+
+/**
+ * Encoder/decoder of PI Data entry to P4 Data entry protobuf messages, and vice
+ * versa.
+ * <p>
+ * TODO: implement codec for each P4Data type using AbstractP4RuntimeCodec.
+ */
+final class P4DataCodec {
+
+    private P4DataCodec() {
+        // Hides constructor.
+    }
+
+    private static P4Header encodeHeader(PiHeader piHeader) {
+        P4Header.Builder builder = P4Header.newBuilder();
+        int i = 0;
+        for (ImmutableByteSequence bitString : piHeader.bitStrings()) {
+            builder.setBitstrings(i, ByteString.copyFrom(bitString.asArray()));
+            i++;
+        }
+        return builder.setIsValid(piHeader.isValid()).build();
+    }
+
+    private static PiHeader decodeHeader(P4Header p4Header) {
+        List<ImmutableByteSequence> bitStrings = p4Header.getBitstringsList().stream()
+                .map(bit -> ImmutableByteSequence.copyFrom(bit.asReadOnlyByteBuffer()))
+                .collect(Collectors.toList());
+
+        return PiHeader.of(p4Header.getIsValid(), bitStrings);
+    }
+
+    private static P4HeaderUnion encodeHeaderUnion(PiHeaderUnion headerUnion) {
+
+        P4HeaderUnion.Builder builder = P4HeaderUnion.newBuilder();
+        if (headerUnion.isValid()) {
+            builder.setValidHeader(encodeHeader(headerUnion.header()));
+            builder.setValidHeaderName(headerUnion.headerName());
+        } else {
+            // An empty string indicates that none of the union members are valid and
+            // valid_header must therefore be unset.
+            builder.setValidHeaderName("");
+        }
+
+        return builder.build();
+    }
+
+    private static PiHeaderUnion decodeHeaderUnion(P4HeaderUnion p4HeaderUnion) {
+
+        return PiHeaderUnion.of(p4HeaderUnion.getValidHeaderName(),
+                                decodeHeader(p4HeaderUnion.getValidHeader()));
+    }
+
+    private static P4StructLike encodeStruct(PiStruct piStruct) {
+        P4StructLike.Builder builder = P4StructLike.newBuilder();
+        builder.addAllMembers(piStruct.struct().stream()
+                                      .map(P4DataCodec::encodeP4Data)
+                                      .collect(Collectors.toList()));
+        return builder.build();
+    }
+
+    private static PiStruct decodeStruct(P4StructLike p4StructLike) {
+
+        return PiStruct.of(p4StructLike.getMembersList().stream()
+                                   .map(P4DataCodec::decodeP4Data)
+                                   .collect(Collectors.toList()));
+    }
+
+    private static P4StructLike encodeTuple(PiTuple piTuple) {
+        P4StructLike.Builder builder = P4StructLike.newBuilder();
+        builder.addAllMembers(piTuple.tuple().stream()
+                                      .map(P4DataCodec::encodeP4Data)
+                                      .collect(Collectors.toList()));
+        return builder.build();
+    }
+
+    private static PiTuple decodeTuple(P4StructLike p4StructLike) {
+
+        return PiTuple.of(p4StructLike.getMembersList().stream()
+                                  .map(P4DataCodec::decodeP4Data)
+                                  .collect(Collectors.toList()));
+    }
+
+    static P4Data encodeP4Data(PiData piData) {
+
+        P4Data.Builder builder = P4Data.newBuilder();
+        switch (piData.type()) {
+            case BITSTRING:
+                builder.setBitstring(ByteString.copyFrom(((PiBitString) piData).bitString().asArray()));
+                break;
+            case ENUMSTRING:
+                builder.setEnum(((PiEnumString) piData).enumString());
+                break;
+            case ERRORSTRING:
+                builder.setError(((PiErrorString) piData).errorString());
+                break;
+            case BOOL:
+                builder.setBool(((PiBool) piData).bool());
+                break;
+            case STRUCT:
+                builder.setStruct(encodeStruct((PiStruct) piData));
+                break;
+            case TUPLE:
+                builder.setTuple(encodeTuple((PiTuple) piData));
+                break;
+            case HEADER:
+                builder.setHeader(encodeHeader((PiHeader) piData));
+                break;
+            case HEADERSTACK:
+                P4HeaderStack.Builder headerStack = P4HeaderStack.newBuilder();
+                int i = 0;
+                for (PiHeader header : ((PiHeaderStack) piData).headers()) {
+                    headerStack.setEntries(i, encodeHeader(header));
+                    i++;
+                }
+                builder.setHeaderStack(headerStack.build());
+                break;
+            case HEADERUNION:
+                builder.setHeaderUnion(encodeHeaderUnion((PiHeaderUnion) piData));
+                break;
+            case HEADERUNIONSTACK:
+                P4HeaderUnionStack.Builder headerUnionStack = P4HeaderUnionStack.newBuilder();
+                int j = 0;
+                for (PiHeaderUnion headerUnion : ((PiHeaderUnionStack) piData).headerUnions()) {
+                    headerUnionStack.setEntries(j, encodeHeaderUnion(headerUnion));
+                    j++;
+                }
+                builder.setHeaderUnionStack(headerUnionStack.build());
+                break;
+            default:
+                break;
+        }
+
+        return builder.build();
+    }
+
+    static PiData decodeP4Data(P4Data p4Data) {
+        PiData piData = null;
+
+        switch (p4Data.getDataCase()) {
+            case BITSTRING:
+                piData = PiBitString.of(ImmutableByteSequence.copyFrom(p4Data.getBitstring().asReadOnlyByteBuffer()));
+                break;
+            case BOOL:
+                piData = PiBool.of(p4Data.getBool());
+                break;
+            case TUPLE:
+                piData = decodeTuple(p4Data.getTuple());
+                break;
+            case STRUCT:
+                piData = decodeStruct(p4Data.getStruct());
+                break;
+            case HEADER:
+                piData = decodeHeader(p4Data.getHeader());
+                break;
+            case HEADER_UNION:
+                piData = decodeHeaderUnion(p4Data.getHeaderUnion());
+                break;
+            case HEADER_STACK:
+                piData = PiHeaderStack.of(p4Data.getHeaderStack().getEntriesList().stream()
+                                                  .map(P4DataCodec::decodeHeader)
+                                                  .collect(Collectors.toList()));
+                break;
+            case HEADER_UNION_STACK:
+                piData = PiHeaderUnionStack.of(p4Data.getHeaderUnionStack()
+                                                       .getEntriesList().stream()
+                                                       .map(P4DataCodec::decodeHeaderUnion)
+                                                       .collect(Collectors.toList()));
+                break;
+            case ENUM:
+                piData = PiEnumString.of(p4Data.getEnum());
+                break;
+            case ERROR:
+                piData = PiErrorString.of(p4Data.getError());
+                break;
+            case DATA_NOT_SET:
+                break;
+            default:
+                break;
+        }
+
+        return piData;
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketInCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketInCodec.java
new file mode 100644
index 0000000..e4de896
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketInCodec.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiPacketOperationType;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiPacketOperation;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.config.v1.P4InfoOuterClass;
+import p4.v1.P4RuntimeOuterClass;
+
+import static org.onlab.util.ImmutableByteSequence.copyFrom;
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+/**
+ * Codec for P4Runtime PacketIn. Only decoding is supported, as encoding is not
+ * meaningful in this case (packet-ins are always generated by the server).
+ */
+public final class PacketInCodec
+        extends AbstractCodec<PiPacketOperation,
+        P4RuntimeOuterClass.PacketIn, Object> {
+
+    private static final String PACKET_IN = "packet_in";
+
+    @Override
+    protected P4RuntimeOuterClass.PacketIn encode(
+            PiPacketOperation piEntity, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws CodecException {
+        throw new CodecException("Encoding of packet-in is not supported");
+    }
+
+    @Override
+    protected PiPacketOperation decode(
+            P4RuntimeOuterClass.PacketIn message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        final P4InfoOuterClass.Preamble ctrlPktMetaPreamble = browser
+                .controllerPacketMetadatas()
+                .getByName(PACKET_IN)
+                .getPreamble();
+        return PiPacketOperation.builder()
+                .withType(PiPacketOperationType.PACKET_IN)
+                .withMetadatas(CODECS.packetMetadata().decodeAll(
+                        message.getMetadataList(), ctrlPktMetaPreamble, pipeconf))
+                .withData(copyFrom(message.getPayload().asReadOnlyByteBuffer()))
+                .build();
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketMetadataCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketMetadataCodec.java
new file mode 100644
index 0000000..3f78085
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketMetadataCodec.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import com.google.protobuf.ByteString;
+import org.onosproject.net.pi.model.PiPacketMetadataId;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiPacketMetadata;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.config.v1.P4InfoOuterClass;
+import p4.v1.P4RuntimeOuterClass;
+
+import static org.onlab.util.ImmutableByteSequence.copyFrom;
+
+/**
+ * Coded for P4Runtime PacketMetadata. The metadata is expected to be a Preamble
+ * of a P4Info.ControllerPacketMetadata message.
+ */
+public final class PacketMetadataCodec
+        extends AbstractCodec<PiPacketMetadata,
+        P4RuntimeOuterClass.PacketMetadata, P4InfoOuterClass.Preamble> {
+
+    @Override
+    protected P4RuntimeOuterClass.PacketMetadata encode(
+            PiPacketMetadata piEntity, P4InfoOuterClass.Preamble ctrlPktMetaPreamble,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final int metadataId = browser
+                .packetMetadatas(ctrlPktMetaPreamble.getId())
+                .getByName(piEntity.id().id()).getId();
+        return P4RuntimeOuterClass.PacketMetadata.newBuilder()
+                .setMetadataId(metadataId)
+                .setValue(ByteString.copyFrom(piEntity.value().asReadOnlyBuffer()))
+                .build();
+    }
+
+    @Override
+    protected PiPacketMetadata decode(
+            P4RuntimeOuterClass.PacketMetadata message,
+            P4InfoOuterClass.Preamble ctrlPktMetaPreamble,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException {
+        final String packetMetadataName = browser
+                .packetMetadatas(ctrlPktMetaPreamble.getId())
+                .getById(message.getMetadataId()).getName();
+        final PiPacketMetadataId metadataId = PiPacketMetadataId
+                .of(packetMetadataName);
+        return PiPacketMetadata.builder()
+                .withId(metadataId)
+                .withValue(copyFrom(message.getValue().asReadOnlyByteBuffer()))
+                .build();
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketOutCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketOutCodec.java
new file mode 100644
index 0000000..6d020c3
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/PacketOutCodec.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import com.google.protobuf.ByteString;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiPacketOperation;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.config.v1.P4InfoOuterClass;
+import p4.v1.P4RuntimeOuterClass;
+
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+/**
+ * Codec for P4Runtime PacketOut. Only encoding is supported, as decoding is not
+ * meaningful in this case (packet-outs are always generated by the client).
+ */
+public final class PacketOutCodec
+        extends AbstractCodec<PiPacketOperation,
+        P4RuntimeOuterClass.PacketOut, Object> {
+
+    private static final String PACKET_OUT = "packet_out";
+
+    @Override
+    protected P4RuntimeOuterClass.PacketOut encode(
+            PiPacketOperation piPacket, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        final P4InfoOuterClass.Preamble ctrlPktMetaPreamble = browser
+                .controllerPacketMetadatas()
+                .getByName(PACKET_OUT)
+                .getPreamble();
+        return P4RuntimeOuterClass.PacketOut.newBuilder()
+                .addAllMetadata(CODECS.packetMetadata().encodeAll(
+                        piPacket.metadatas(), ctrlPktMetaPreamble, pipeconf))
+                .setPayload(ByteString.copyFrom(piPacket.data().asReadOnlyBuffer()))
+                .build();
+
+    }
+
+    @Override
+    protected PiPacketOperation decode(
+            P4RuntimeOuterClass.PacketOut message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException {
+        throw new CodecException("Decoding of packet-out is not supported");
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/PreReplicaCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/PreReplicaCodec.java
new file mode 100644
index 0000000..7402b14
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/PreReplicaCodec.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.runtime.PiPreReplica;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.v1.P4RuntimeOuterClass;
+
+import static java.lang.String.format;
+
+/**
+ * Codec for P4Runtime PRE Replica.
+ */
+public class PreReplicaCodec extends AbstractCodec<PiPreReplica,
+        P4RuntimeOuterClass.Replica, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.Replica encode(
+            PiPreReplica replica, Object ignore,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        final int p4PortId;
+        try {
+            p4PortId = Math.toIntExact(replica.egressPort().toLong());
+        } catch (ArithmeticException e) {
+            throw new CodecException(format(
+                    "Cannot cast 64 bit port value '%s' to 32 bit",
+                    replica.egressPort()));
+        }
+        return P4RuntimeOuterClass.Replica.newBuilder()
+                .setEgressPort(p4PortId)
+                .setInstance(replica.instanceId())
+                .build();
+    }
+
+    @Override
+    protected PiPreReplica decode(
+            P4RuntimeOuterClass.Replica message, Object ignore,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        return new PiPreReplica(
+                PortNumber.portNumber(message.getEgressPort()),
+                message.getInstance());
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/TableEntryCodec.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/TableEntryCodec.java
new file mode 100644
index 0000000..513b4fa
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/TableEntryCodec.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.onosproject.net.pi.model.PiTableId;
+import org.onosproject.net.pi.runtime.PiAction;
+import org.onosproject.net.pi.runtime.PiActionProfileGroupId;
+import org.onosproject.net.pi.runtime.PiActionProfileMemberId;
+import org.onosproject.net.pi.runtime.PiCounterCellData;
+import org.onosproject.net.pi.runtime.PiMatchKey;
+import org.onosproject.net.pi.runtime.PiTableAction;
+import org.onosproject.net.pi.runtime.PiTableEntry;
+import org.onosproject.net.pi.runtime.PiTableEntryHandle;
+import org.onosproject.p4runtime.ctl.utils.P4InfoBrowser;
+import p4.config.v1.P4InfoOuterClass;
+import p4.v1.P4RuntimeOuterClass;
+
+import java.util.OptionalInt;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+import static org.onosproject.p4runtime.ctl.codec.Codecs.CODECS;
+
+/**
+ * Codec for P4Runtime TableEntry.
+ */
+public final class TableEntryCodec
+        extends AbstractEntityCodec<PiTableEntry, PiTableEntryHandle,
+        P4RuntimeOuterClass.TableEntry, Object> {
+
+    @Override
+    protected P4RuntimeOuterClass.TableEntry encode(
+            PiTableEntry piTableEntry, Object ignored, PiPipeconf pipeconf,
+            P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        final P4RuntimeOuterClass.TableEntry.Builder tableEntryMsgBuilder =
+                keyMsgBuilder(piTableEntry.table(), piTableEntry.matchKey(),
+                              piTableEntry.priority(), pipeconf, browser);
+        // Controller metadata (cookie)
+        tableEntryMsgBuilder.setControllerMetadata(piTableEntry.cookie());
+        // Timeout.
+        if (piTableEntry.timeout().isPresent()) {
+            // FIXME: timeout is supported in P4Runtime v1.0
+            log.warn("Found PI table entry with timeout set, " +
+                             "not supported in P4Runtime: {}", piTableEntry);
+        }
+        // Table action.
+        if (piTableEntry.action() != null) {
+            tableEntryMsgBuilder.setAction(
+                    encodePiTableAction(piTableEntry.action(), pipeconf));
+        }
+        // Counter.
+        if (piTableEntry.counter() != null) {
+            tableEntryMsgBuilder.setCounterData(encodeCounter(piTableEntry.counter()));
+        }
+        return tableEntryMsgBuilder.build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.TableEntry encodeKey(
+            PiTableEntryHandle handle, Object metadata, PiPipeconf pipeconf,
+            P4InfoBrowser browser) throws CodecException, P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(handle.tableId(), handle.matchKey(),
+                             handle.priority(), pipeconf, browser).build();
+    }
+
+    @Override
+    protected P4RuntimeOuterClass.TableEntry encodeKey(
+            PiTableEntry piEntity, Object metadata, PiPipeconf pipeconf,
+            P4InfoBrowser browser) throws CodecException, P4InfoBrowser.NotFoundException {
+        return keyMsgBuilder(piEntity.table(), piEntity.matchKey(),
+                             piEntity.priority(), pipeconf, browser).build();
+    }
+
+    @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+    private P4RuntimeOuterClass.TableEntry.Builder keyMsgBuilder(
+            PiTableId tableId, PiMatchKey matchKey, OptionalInt priority,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws P4InfoBrowser.NotFoundException, CodecException {
+        final P4RuntimeOuterClass.TableEntry.Builder tableEntryMsgBuilder =
+                P4RuntimeOuterClass.TableEntry.newBuilder();
+        final P4InfoOuterClass.Preamble tablePreamble = browser.tables()
+                .getByName(tableId.id()).getPreamble();
+        // Table id.
+        tableEntryMsgBuilder.setTableId(tablePreamble.getId());
+        // Field matches.
+        if (matchKey.equals(PiMatchKey.EMPTY)) {
+            tableEntryMsgBuilder.setIsDefaultAction(true);
+        } else {
+            tableEntryMsgBuilder.addAllMatch(
+                    CODECS.fieldMatch().encodeAll(
+                            matchKey.fieldMatches(),
+                            tablePreamble, pipeconf));
+        }
+        // Priority.
+        priority.ifPresent(tableEntryMsgBuilder::setPriority);
+        return tableEntryMsgBuilder;
+    }
+
+    @Override
+    protected PiTableEntry decode(
+            P4RuntimeOuterClass.TableEntry message, Object ignored,
+            PiPipeconf pipeconf, P4InfoBrowser browser)
+            throws CodecException, P4InfoBrowser.NotFoundException {
+        PiTableEntry.Builder piTableEntryBuilder = PiTableEntry.builder();
+
+        P4InfoOuterClass.Preamble tablePreamble = browser.tables()
+                .getById(message.getTableId()).getPreamble();
+
+        // Table id.
+        piTableEntryBuilder.forTable(PiTableId.of(tablePreamble.getName()));
+
+        // Priority.
+        if (message.getPriority() > 0) {
+            piTableEntryBuilder.withPriority(message.getPriority());
+        }
+
+        // Controller metadata (cookie)
+        piTableEntryBuilder.withCookie(message.getControllerMetadata());
+
+        // Table action.
+        if (message.hasAction()) {
+            piTableEntryBuilder.withAction(decodeTableActionMsg(
+                    message.getAction(), pipeconf));
+        }
+
+        // Timeout.
+        // FIXME: how to decode table entry messages with timeout, given that
+        //  the timeout value is lost after encoding?
+
+        // Match key for field matches.
+        piTableEntryBuilder.withMatchKey(
+                PiMatchKey.builder()
+                        .addFieldMatches(CODECS.fieldMatch().decodeAll(
+                                message.getMatchList(),
+                                tablePreamble, pipeconf))
+                        .build());
+
+        // Counter.
+        if (message.hasCounterData()) {
+            piTableEntryBuilder.withCounterCellData(decodeCounter(message.getCounterData()));
+        }
+
+        return piTableEntryBuilder.build();
+    }
+
+    private P4RuntimeOuterClass.TableAction encodePiTableAction(
+            PiTableAction piTableAction, PiPipeconf pipeconf)
+            throws CodecException {
+        checkNotNull(piTableAction, "Cannot encode null PiTableAction");
+        final P4RuntimeOuterClass.TableAction.Builder tableActionMsgBuilder =
+                P4RuntimeOuterClass.TableAction.newBuilder();
+        switch (piTableAction.type()) {
+            case ACTION:
+                P4RuntimeOuterClass.Action theAction = CODECS.action()
+                        .encode((PiAction) piTableAction, null, pipeconf);
+                tableActionMsgBuilder.setAction(theAction);
+                break;
+            case ACTION_PROFILE_GROUP_ID:
+                tableActionMsgBuilder.setActionProfileGroupId(
+                        ((PiActionProfileGroupId) piTableAction).id());
+                break;
+            case ACTION_PROFILE_MEMBER_ID:
+                tableActionMsgBuilder.setActionProfileMemberId(
+                        ((PiActionProfileMemberId) piTableAction).id());
+                break;
+            default:
+                throw new CodecException(
+                        format("Building of table action type %s not implemented",
+                               piTableAction.type()));
+        }
+        return tableActionMsgBuilder.build();
+    }
+
+    private PiTableAction decodeTableActionMsg(
+            P4RuntimeOuterClass.TableAction tableActionMsg, PiPipeconf pipeconf)
+            throws CodecException {
+        P4RuntimeOuterClass.TableAction.TypeCase typeCase = tableActionMsg.getTypeCase();
+        switch (typeCase) {
+            case ACTION:
+                P4RuntimeOuterClass.Action actionMsg = tableActionMsg.getAction();
+                return CODECS.action().decode(
+                        actionMsg, null, pipeconf);
+            case ACTION_PROFILE_GROUP_ID:
+                return PiActionProfileGroupId.of(
+                        tableActionMsg.getActionProfileGroupId());
+            case ACTION_PROFILE_MEMBER_ID:
+                return PiActionProfileMemberId.of(
+                        tableActionMsg.getActionProfileMemberId());
+            default:
+                throw new CodecException(
+                        format("Decoding of table action type %s not implemented",
+                               typeCase.name()));
+        }
+    }
+
+    private P4RuntimeOuterClass.CounterData encodeCounter(
+            PiCounterCellData piCounterCellData) {
+        return P4RuntimeOuterClass.CounterData.newBuilder()
+                .setPacketCount(piCounterCellData.packets())
+                .setByteCount(piCounterCellData.bytes()).build();
+    }
+
+    private PiCounterCellData decodeCounter(
+            P4RuntimeOuterClass.CounterData counterData) {
+        return new PiCounterCellData(
+                counterData.getPacketCount(), counterData.getByteCount());
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/Utils.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/Utils.java
new file mode 100644
index 0000000..c72489a
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/Utils.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.codec;
+
+import com.google.protobuf.ByteString;
+
+import static java.lang.String.format;
+
+/**
+ * Codec utilities.
+ */
+final class Utils {
+
+    private Utils() {
+        // Hide default construction
+    }
+
+    static void assertSize(String entityDescr, ByteString value, int bitWidth)
+            throws CodecException {
+
+        int byteWidth = (int) Math.ceil((float) bitWidth / 8);
+        if (value.size() != byteWidth) {
+            throw new CodecException(format(
+                    "Wrong size for %s, expected %d bytes, but found %d",
+                    entityDescr, byteWidth, value.size()));
+        }
+    }
+
+    static void assertPrefixLen(String entityDescr, int prefixLength, int bitWidth)
+            throws CodecException {
+
+        if (prefixLength > bitWidth) {
+            throw new CodecException(format(
+                    "wrong prefix length for %s, field size is %d bits, but found one is %d",
+                    entityDescr, bitWidth, prefixLength));
+        }
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/package-info.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/package-info.java
new file mode 100644
index 0000000..25cee2b
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/codec/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Classes to translates from PI framework-related objects to P4Runtime protobuf
+ * messages, and vice versa.
+ */
+package org.onosproject.p4runtime.ctl.codec;
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/utils/P4InfoBrowser.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/utils/P4InfoBrowser.java
new file mode 100644
index 0000000..29cddff
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/utils/P4InfoBrowser.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.utils;
+
+
+import com.google.common.collect.Maps;
+import com.google.protobuf.Message;
+import p4.config.v1.P4InfoOuterClass.Action;
+import p4.config.v1.P4InfoOuterClass.ActionProfile;
+import p4.config.v1.P4InfoOuterClass.ControllerPacketMetadata;
+import p4.config.v1.P4InfoOuterClass.Counter;
+import p4.config.v1.P4InfoOuterClass.DirectCounter;
+import p4.config.v1.P4InfoOuterClass.DirectMeter;
+import p4.config.v1.P4InfoOuterClass.MatchField;
+import p4.config.v1.P4InfoOuterClass.Meter;
+import p4.config.v1.P4InfoOuterClass.P4Info;
+import p4.config.v1.P4InfoOuterClass.Preamble;
+import p4.config.v1.P4InfoOuterClass.Table;
+
+import java.util.Map;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+
+/**
+ * Utility class to easily retrieve information from a P4Info protobuf message.
+ */
+public final class P4InfoBrowser {
+
+    private final EntityBrowser<Table> tables = new EntityBrowser<>("table");
+    private final EntityBrowser<Action> actions = new EntityBrowser<>("action");
+    private final EntityBrowser<ActionProfile> actionProfiles = new EntityBrowser<>("action profile");
+    private final EntityBrowser<Counter> counters = new EntityBrowser<>("counter");
+    private final EntityBrowser<DirectCounter> directCounters = new EntityBrowser<>("direct counter");
+    private final EntityBrowser<Meter> meters = new EntityBrowser<>("meter");
+    private final EntityBrowser<DirectMeter> directMeters = new EntityBrowser<>("direct meter");
+    private final EntityBrowser<ControllerPacketMetadata> ctrlPktMetadatas =
+            new EntityBrowser<>("controller packet metadata");
+    private final Map<Integer, EntityBrowser<Action.Param>> actionParams = Maps.newHashMap();
+    private final Map<Integer, EntityBrowser<MatchField>> matchFields = Maps.newHashMap();
+    private final Map<Integer, EntityBrowser<ControllerPacketMetadata.Metadata>> ctrlPktMetadatasMetadata =
+            Maps.newHashMap();
+
+    /**
+     * Creates a new browser for the given P4Info.
+     *
+     * @param p4info P4Info protobuf message
+     */
+    public P4InfoBrowser(P4Info p4info) {
+        parseP4Info(p4info);
+    }
+
+    private void parseP4Info(P4Info p4info) {
+        p4info.getTablesList().forEach(
+                entity -> {
+                    tables.addWithPreamble(entity.getPreamble(), entity);
+                    // Index match fields.
+                    int tableId = entity.getPreamble().getId();
+                    String tableName = entity.getPreamble().getName();
+                    EntityBrowser<MatchField> matchFieldBrowser = new EntityBrowser<>(format(
+                            "match field for table '%s'", tableName));
+                    entity.getMatchFieldsList().forEach(m -> matchFieldBrowser.add(m.getName(), null, m.getId(), m));
+                    matchFields.put(tableId, matchFieldBrowser);
+                });
+
+        p4info.getActionsList().forEach(
+                entity -> {
+                    actions.addWithPreamble(entity.getPreamble(), entity);
+                    // Index action params.
+                    int actionId = entity.getPreamble().getId();
+                    String actionName = entity.getPreamble().getName();
+                    EntityBrowser<Action.Param> paramBrowser = new EntityBrowser<>(format(
+                            "param for action '%s'", actionName));
+                    entity.getParamsList().forEach(p -> paramBrowser.add(p.getName(), null, p.getId(), p));
+                    actionParams.put(actionId, paramBrowser);
+                });
+
+        p4info.getActionProfilesList().forEach(
+                entity -> actionProfiles.addWithPreamble(entity.getPreamble(), entity));
+
+        p4info.getCountersList().forEach(
+                entity -> counters.addWithPreamble(entity.getPreamble(), entity));
+
+        p4info.getDirectCountersList().forEach(
+                entity -> directCounters.addWithPreamble(entity.getPreamble(), entity));
+
+        p4info.getMetersList().forEach(
+                entity -> meters.addWithPreamble(entity.getPreamble(), entity));
+
+        p4info.getDirectMetersList().forEach(
+                entity -> directMeters.addWithPreamble(entity.getPreamble(), entity));
+
+        p4info.getControllerPacketMetadataList().forEach(
+                entity -> {
+                    ctrlPktMetadatas.addWithPreamble(entity.getPreamble(), entity);
+                    // Index control packet metadata metadata.
+                    int ctrlPktMetadataId = entity.getPreamble().getId();
+                    String ctrlPktMetadataName = entity.getPreamble().getName();
+                    EntityBrowser<ControllerPacketMetadata.Metadata> metadataBrowser = new EntityBrowser<>(format(
+                            "metadata field for controller packet metadata '%s'", ctrlPktMetadataName));
+                    entity.getMetadataList().forEach(m -> metadataBrowser.add(m.getName(), null, m.getId(), m));
+                    ctrlPktMetadatasMetadata.put(ctrlPktMetadataId, metadataBrowser);
+                });
+    }
+
+    /**
+     * Returns a browser for tables.
+     *
+     * @return table browser
+     */
+    public EntityBrowser<Table> tables() {
+        return tables;
+    }
+
+    /**
+     * Returns a browser for actions.
+     *
+     * @return action browser
+     */
+    public EntityBrowser<Action> actions() {
+        return actions;
+    }
+
+    /**
+     * Returns a browser for action profiles.
+     *
+     * @return action profile browser
+     */
+    public EntityBrowser<ActionProfile> actionProfiles() {
+        return actionProfiles;
+    }
+
+    /**
+     * Returns a browser for counters.
+     *
+     * @return counter browser
+     */
+    public EntityBrowser<Counter> counters() {
+        return counters;
+    }
+
+    /**
+     * Returns a browser for direct counters.
+     *
+     * @return direct counter browser
+     */
+    public EntityBrowser<DirectCounter> directCounters() {
+        return directCounters;
+    }
+
+    /**
+     * Returns a browser for meters.
+     *
+     * @return meter browser
+     */
+    public EntityBrowser<Meter> meters() {
+        return meters;
+    }
+
+    /**
+     * Returns a browser for direct meters.
+     *
+     * @return table browser
+     */
+    public EntityBrowser<DirectMeter> directMeters() {
+        return directMeters;
+    }
+
+    /**
+     * Returns a browser for controller packet metadata.
+     *
+     * @return controller packet metadata browser
+     */
+    public EntityBrowser<ControllerPacketMetadata> controllerPacketMetadatas() {
+        return ctrlPktMetadatas;
+    }
+
+    /**
+     * Returns a browser for params of the given action.
+     *
+     * @param actionId action identifier
+     * @return action params browser
+     * @throws NotFoundException if the action cannot be found
+     */
+    public EntityBrowser<Action.Param> actionParams(int actionId) throws NotFoundException {
+        // Throws exception if action id is not found.
+        actions.getById(actionId);
+        return actionParams.get(actionId);
+    }
+
+    /**
+     * Returns a browser for match fields of the given table.
+     *
+     * @param tableId table identifier
+     * @return match field browser
+     * @throws NotFoundException if the table cannot be found
+     */
+    public EntityBrowser<MatchField> matchFields(int tableId) throws NotFoundException {
+        // Throws exception if action id is not found.
+        tables.getById(tableId);
+        return matchFields.get(tableId);
+    }
+
+    /**
+     * Returns a browser for metadata fields of the controller packet metadata.
+     *
+     * @param controllerPacketMetadataId controller packet metadata identifier
+     * @return metadata browser
+     * @throws NotFoundException controller packet metadata cannot be found
+     */
+    public EntityBrowser<ControllerPacketMetadata.Metadata> packetMetadatas(int controllerPacketMetadataId)
+            throws NotFoundException {
+        // Throws exception if controller packet metadata id is not found.
+        ctrlPktMetadatas.getById(controllerPacketMetadataId);
+        return ctrlPktMetadatasMetadata.get(controllerPacketMetadataId);
+    }
+
+    /**
+     * Browser of P4Info entities.
+     *
+     * @param <T> protobuf message type
+     */
+    public static final class EntityBrowser<T extends Message> {
+
+        private String entityName;
+        private final Map<String, T> names = Maps.newHashMap();
+        private final Map<String, String> aliasToNames = Maps.newHashMap();
+        private final Map<Integer, T> ids = Maps.newHashMap();
+
+        private EntityBrowser(String entityName) {
+            this.entityName = entityName;
+        }
+
+        /**
+         * Adds the given entity identified by the given name, alias (nullable) and id.
+         *
+         * @param name   entity name
+         * @param alias  entity alias or null
+         * @param id     entity id
+         * @param entity entity message
+         */
+        private void add(String name, String alias, int id, T entity) {
+            checkNotNull(name);
+            checkArgument(!name.isEmpty(), "Name cannot be empty");
+            checkNotNull(entity);
+            names.put(name, entity);
+            ids.put(id, entity);
+            if (alias != null && !alias.isEmpty()) {
+                aliasToNames.put(alias, name);
+            }
+        }
+
+        /**
+         * Adds the given entity identified by the given P4Info preamble.
+         *
+         * @param preamble P4Info preamble protobuf message
+         * @param entity   entity message
+         */
+        private void addWithPreamble(Preamble preamble, T entity) {
+            checkNotNull(preamble);
+            add(preamble.getName(), preamble.getAlias(), preamble.getId(), entity);
+        }
+
+        /**
+         * Returns true if the P4Info defines an entity with such name, false otherwise.
+         *
+         * @param name entity name
+         * @return boolean
+         */
+        public boolean hasName(String name) {
+            return names.containsKey(name);
+        }
+
+        /**
+         * Returns the entity identified by the given name, if present, otherwise, throws an exception.
+         *
+         * @param name entity name or alias
+         * @return entity message
+         * @throws NotFoundException if the entity cannot be found
+         */
+        public T getByName(String name) throws NotFoundException {
+            if (hasName(name)) {
+                return names.get(name);
+            } else {
+                final String hint = aliasToNames.containsKey(name)
+                        ? format("Did you mean '%s'? Make sure to use entity names in PI IDs, not aliases",
+                                 aliasToNames.get(name))
+                        : "";
+                throw new NotFoundException(entityName, name, hint);
+            }
+        }
+
+        /**
+         * Returns true if the P4Info defines an entity with such id, false otherwise.
+         *
+         * @param id entity id
+         * @return boolean
+         */
+        public boolean hasId(int id) {
+            return ids.containsKey(id);
+        }
+
+        /**
+         * Returns the entity identified by the given id, if present, otherwise, throws an exception.
+         *
+         * @param id entity id
+         * @return entity message
+         * @throws NotFoundException if the entity cannot be found
+         */
+        public T getById(int id) throws NotFoundException {
+            if (!hasId(id)) {
+                throw new NotFoundException(entityName, id);
+            }
+            return ids.get(id);
+        }
+    }
+
+    /**
+     * Signals tha an entity cannot be found in the P4Info.
+     */
+    public static final class NotFoundException extends Exception {
+
+        public NotFoundException(String entityName, String key, String hint) {
+            super(format(
+                    "No such %s in P4Info with name '%s'%s",
+                    entityName, key, hint.isEmpty() ? "" : " (" + hint + ")"));
+        }
+
+        public NotFoundException(String entityName, int id) {
+            super(format("No such %s in P4Info with id '%d'", entityName, id));
+        }
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/utils/PipeconfHelper.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/utils/PipeconfHelper.java
new file mode 100644
index 0000000..f5afd41
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/utils/PipeconfHelper.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.p4runtime.ctl.utils;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.Maps;
+import com.google.protobuf.ExtensionRegistry;
+import com.google.protobuf.TextFormat;
+import org.onosproject.net.pi.model.PiPipeconf;
+import org.slf4j.Logger;
+import p4.config.v1.P4InfoOuterClass.P4Info;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType.P4_INFO_TEXT;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Utility class to deal with pipeconfs in the context of P4Runtime.
+ */
+public final class PipeconfHelper {
+
+    private static final int P4INFO_BROWSER_EXPIRE_TIME_IN_MIN = 10;
+    private static final Logger log = getLogger(PipeconfHelper.class);
+
+    private static final Cache<Long, P4InfoBrowser> BROWSERS = CacheBuilder.newBuilder()
+            .expireAfterAccess(P4INFO_BROWSER_EXPIRE_TIME_IN_MIN, TimeUnit.MINUTES)
+            .build();
+    private static final Map<Long, P4Info> P4INFOS = Maps.newConcurrentMap();
+
+    private PipeconfHelper() {
+        // hide.
+    }
+
+    /**
+     * Extracts and returns a P4Info protobuf message from the given pipeconf. If the pipeconf does not define any
+     * extension of type {@link PiPipeconf.ExtensionType#P4_INFO_TEXT}, returns null;
+     *
+     * @param pipeconf pipeconf
+     * @return P4Info or null
+     */
+    public static P4Info getP4Info(PiPipeconf pipeconf) {
+        return P4INFOS.computeIfAbsent(pipeconf.fingerprint(), piPipeconfId -> {
+            if (!pipeconf.extension(P4_INFO_TEXT).isPresent()) {
+                log.warn("Missing P4Info extension in pipeconf {}", pipeconf.id());
+                return null;
+            }
+
+            InputStream p4InfoStream = pipeconf.extension(P4_INFO_TEXT).get();
+            P4Info.Builder p4iInfoBuilder = P4Info.newBuilder();
+            try {
+                TextFormat.getParser().merge(new InputStreamReader(p4InfoStream), ExtensionRegistry.getEmptyRegistry(),
+                                             p4iInfoBuilder);
+            } catch (IOException ex) {
+                log.warn("Unable to parse P4Info of pipeconf {}: {}", pipeconf.id(), ex.getMessage());
+                return null;
+            }
+
+            return p4iInfoBuilder.build();
+        });
+    }
+
+    /**
+     * Returns a P4Info browser for the given pipeconf. If the pipeconf does not define any extension of type
+     * {@link PiPipeconf.ExtensionType#P4_INFO_TEXT}, returns null;
+     *
+     * @param pipeconf pipeconf
+     * @return P4Info browser or null
+     */
+    public static P4InfoBrowser getP4InfoBrowser(PiPipeconf pipeconf) {
+        try {
+            return BROWSERS.get(pipeconf.fingerprint(), () -> {
+                P4Info p4info = PipeconfHelper.getP4Info(pipeconf);
+                if (p4info == null) {
+                    return null;
+                } else {
+                    return new P4InfoBrowser(p4info);
+                }
+            });
+        } catch (ExecutionException e) {
+            log.error("Exception while accessing the P4InfoBrowser cache", e);
+            return null;
+        }
+    }
+}
diff --git a/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/utils/package-info.java b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/utils/package-info.java
new file mode 100644
index 0000000..ae09455
--- /dev/null
+++ b/protocols/p4runtime/utils/src/main/java/org/onosproject/p4runtime/ctl/utils/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Utility classes for the P4Runtime protocol subsystem.
+ */
+package org.onosproject.p4runtime.ctl.utils;