Serialize / deserialize functions for IGMP, IGMPv3 Membership
Query and IGMPv3 Membership Report.  IGMP has been added to
the IPv4 deserialization map.

Change-Id: I6d46c3771b6589f1cbd839c58521ffab94b5e230
diff --git a/utils/misc/src/main/java/org/onlab/packet/IGMP.java b/utils/misc/src/main/java/org/onlab/packet/IGMP.java
new file mode 100644
index 0000000..9d5535d
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/IGMP.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onlab.packet;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.slf4j.Logger;
+
+import static org.slf4j.LoggerFactory.getLogger;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.packet.PacketUtils.checkInput;
+
+
+/**
+ * Implements IGMP control packet format.
+ */
+public class IGMP extends BasePacket {
+    private final Logger log = getLogger(getClass());
+
+    public static final byte TYPE_IGMPV3_MEMBERSHIP_QUERY = 0x11;
+    public static final byte TYPE_IGMPV1_MEMBERSHIP_REPORT = 0x12;
+    public static final byte TYPE_IGMPV2_MEMBERSHIP_REPORT = 0x16;
+    public static final byte TYPE_IGMPV2_LEAVE_GROUP = 0x17;
+    public static final byte TYPE_IGMPV3_MEMBERSHIP_REPORT = 0x22;
+    public static final Map<Byte, Deserializer<? extends IPacket>> PROTOCOL_DESERIALIZER_MAP = new HashMap<>();
+
+    public static final int MINIMUM_HEADER_LEN = 12;
+
+    List<IGMPGroup> groups = new ArrayList<>();
+
+    // Fields contained in the IGMP header
+    private byte igmpType;
+    private byte resField = 0;
+    private short checksum = 0;
+
+    private byte[] unsupportTypeData;
+
+    public IGMP() {
+    }
+
+    /**
+     * Get the IGMP message type.
+     *
+     * @return the IGMP message type
+     */
+    public byte getIgmpType() {
+        return igmpType;
+    }
+
+    /**
+     * Set the IGMP message type.
+     *
+     * @param msgType IGMP message type
+     */
+    public void setIgmpType(byte msgType) {
+        igmpType = msgType;
+    }
+
+    /**
+     * Get the checksum of this message.
+     *
+     * @return the checksum
+     */
+    public short getChecksum() {
+        return checksum;
+    }
+
+    /**
+     * get the Max Resp Code.
+     *
+     * @return The Maximum Time allowed before before sending a responding report.
+     */
+    public byte getMaxRespField() {
+        return resField;
+    }
+
+    /**
+     * Set the Max Resp Code.
+     *
+     * @param respCode the Maximum Response Code.
+     */
+    public void setMaxRespCode(byte respCode) {
+        if (igmpType != IGMP.TYPE_IGMPV3_MEMBERSHIP_QUERY) {
+            log.debug("Requesting the max response code for an incorrect field: ");
+        }
+        this.resField = respCode;
+    }
+
+    /**
+     * Get the list of IGMPGroups.  The group objects will be either IGMPQuery or IGMPMembership
+     * depending on the IGMP message type.  For IGMP Query, the groups list should only be
+     * one group.
+     *
+     * @return The list of IGMP groups.
+     */
+    public List<IGMPGroup> getGroups() {
+        return groups;
+    }
+
+    /**
+     * Add a multicast group to this IGMP message.
+     *
+     * @param group the IGMPGroup will be IGMPQuery or IGMPMembership depending on the message type.
+     * @return true if group was valid and added, false otherwise.
+     */
+    public boolean addGroup(IGMPGroup group) {
+        checkNotNull(group);
+        switch (this.igmpType) {
+            case TYPE_IGMPV3_MEMBERSHIP_QUERY:
+                if (group instanceof IGMPMembership) {
+                    return false;
+                }
+
+                if (group.sources.size() > 1) {
+                    return false;
+                }
+                break;
+
+            case TYPE_IGMPV3_MEMBERSHIP_REPORT:
+                if (group instanceof IGMPMembership) {
+                    return false;
+                }
+                break;
+
+            default:
+                log.debug("Warning no IGMP message type has been set");
+        }
+
+        this.groups.add(group);
+        return true;
+    }
+
+    /**
+     * Serialize this IGMP packet.  This will take care
+     * of serializing IGMPv3 Queries and IGMPv3 Membership
+     * Reports.
+     *
+     * @return the serialized IGMP message
+     */
+    @Override
+    public byte[] serialize() {
+        byte [] data = new byte[8915];
+
+        ByteBuffer bb = ByteBuffer.wrap(data);
+        bb.put(this.getIgmpType());
+
+        // reserved or max resp code depending on type.
+        bb.put(this.resField);
+
+        // Must calculate checksum
+        bb.putShort((short) 0);
+
+        switch (this.igmpType) {
+
+            case IGMP.TYPE_IGMPV3_MEMBERSHIP_REPORT:
+                // reserved
+                bb.putShort((short) 0);
+                // Number of groups
+                bb.putShort((short) groups.size());
+                // Fall through
+
+            case IGMP.TYPE_IGMPV3_MEMBERSHIP_QUERY:
+
+                for (IGMPGroup grp : groups) {
+                    grp.serialize(bb);
+                }
+                break;
+
+            default:
+                bb.put(this.unsupportTypeData);
+                break;
+        }
+
+        int size = bb.position();
+        bb.position(0);
+        byte [] rdata = new byte[size];
+        bb.get(rdata, 0, size);
+        return rdata;
+    }
+
+    /**
+     * Deserialize an IGMP message.
+     *
+     * @param data bytes to deserialize
+     * @param offset offset to start deserializing from
+     * @param length length of the data to deserialize
+     * @return populated IGMP object
+     */
+    @Override
+    public IPacket deserialize(final byte[] data, final int offset,
+                               final int length) {
+
+        IGMP igmp = new IGMP();
+        try {
+            igmp = IGMP.deserializer().deserialize(data, offset, length);
+        } catch (DeserializationException e) {
+            log.error(e.getStackTrace().toString());
+            return this;
+        }
+        this.igmpType = igmp.igmpType;
+        this.resField = igmp.resField;
+        this.checksum = igmp.checksum;
+        this.groups = igmp.groups;
+        return this;
+    }
+
+    /**
+     * Deserializer function for IPv4 packets.
+     *
+     * @return deserializer function
+     */
+    public static Deserializer<IGMP> deserializer() {
+        return (data, offset, length) -> {
+            checkInput(data, offset, length, MINIMUM_HEADER_LEN);
+
+            IGMP igmp = new IGMP();
+
+            ByteBuffer bb = ByteBuffer.wrap(data);
+            igmp.igmpType = bb.get();
+            igmp.resField = bb.get();
+            igmp.checksum = bb.getShort();
+            int len = MINIMUM_HEADER_LEN;
+            String msg;
+
+            switch (igmp.igmpType) {
+
+                case TYPE_IGMPV3_MEMBERSHIP_QUERY:
+                    IGMPQuery qgroup = new IGMPQuery();
+                    qgroup.deserialize(bb);
+                    igmp.groups.add(qgroup);
+                    break;
+
+                case TYPE_IGMPV3_MEMBERSHIP_REPORT:
+                    bb.getShort();  // Ignore resvd
+                    int ngrps = bb.getShort();
+
+                    for (; ngrps > 0; ngrps--) {
+                        IGMPMembership mgroup = new IGMPMembership();
+                        mgroup.deserialize(bb);
+                        igmp.groups.add(mgroup);
+                    }
+                    break;
+
+                /*
+                 * NOTE: according to the IGMPv3 spec. These previous IGMP type fields
+                 * must be supported.  At this time we are going to <b>assume</b> we run
+                 * in a modern network where all devices are IGMPv3 capable.
+                 */
+                case TYPE_IGMPV1_MEMBERSHIP_REPORT:
+                case TYPE_IGMPV2_MEMBERSHIP_REPORT:
+                case TYPE_IGMPV2_LEAVE_GROUP:
+                    igmp.unsupportTypeData = bb.array();  // Is this the entire array?
+                    msg = "IGMP message type: " + igmp.igmpType + " is not supported";
+                    igmp.log.debug(msg);
+                    break;
+
+                default:
+                    msg = "IGMP message type: " + igmp.igmpType + " is not recodnized";
+                    igmp.log.debug(msg);
+                    break;
+            }
+            return igmp;
+        };
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!super.equals(obj)) {
+            return false;
+        }
+        if (!(obj instanceof IGMP)) {
+            return false;
+        }
+        final IGMP other = (IGMP) obj;
+        if (this.igmpType != other.igmpType) {
+            return false;
+        }
+        if (this.resField != other.resField) {
+            return false;
+        }
+        if (this.checksum != other.checksum) {
+            return false;
+        }
+        if (this.groups.size() != other.groups.size()) {
+            return false;
+        }
+        // TODO: equals should be true regardless of order.
+        if (!groups.equals(other.groups)) {
+            return false;
+        }
+        return true;
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        final int prime = 2521;
+        int result = super.hashCode();
+        result = prime * result + this.igmpType;
+        result = prime * result + this.groups.size();
+        result = prime * result + this.resField;
+        result = prime * result + this.checksum;
+        result = prime * result + this.groups.hashCode();
+        return result;
+    }
+}