initial code for the mac-learning app

Change-Id: I04f3b17c453e502f20477e83cb7cdc796b4160ae
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7ca0587
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,19 @@
+# Created by .ignore support plugin (hsz.mobi)
+
+# IntelliJ project files
+.idea
+*.iml
+out
+gen
+
+# core
+/*/target/
+/target/
+*.log
+*.gz
+
+# Visual Studio Code
+.settings
+.project
+.classpath
+.factorypath
\ No newline at end of file
diff --git a/.gitreview b/.gitreview
new file mode 100644
index 0000000..0d979d3
--- /dev/null
+++ b/.gitreview
@@ -0,0 +1,5 @@
+[gerrit]
+host=gerrit.opencord.org
+port=29418
+project=mac-learning.git
+defaultremote=origin
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..4b4b15c
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,3 @@
+We expect all ONF employees, member companies, and participants to abide by our Code of Conduct.
+
+If you are being harassed, notice that someone else is being harassed, or have any other concerns involving someone’s welfare, please notify a member of the ONF team or email conduct@opennetworking.org.
\ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..d9d9d30
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2016 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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e011b80
--- /dev/null
+++ b/README.md
@@ -0,0 +1,21 @@
+ONOS MAC Learner Application
+=====================================
+MAC Learner is an ONOS application that examines the DISCOVER or REQUEST type DHCP packets and
+keeps the MAC address temporarily according to the device, port and vlanId information obtained from
+these packets. If the package has a single tag, VlanVID is used;
+if it is a double tag, QinqVID is used as vlanId.
+
+Functionalities
+---------------
+- MAC Learner has REST API and CLI integration. You can access and modify MAC Address map via these.
+
+- By defining ports to the Igonered Port Map,
+it can be ensured that DHCP packets from these ports are not taken into account.
+
+###Parameters
+* __cacheDurationSec__ - MAC Mappings are held with a timestamp and scheduled executor running in the
+background that clears expired mappings(exist more than cacheDuration). The operating frequency of
+this executor can be set with this parameter. By default, it is 86400(1 day).
+
+* __enableDeviceListener__ - By enabling this parameter, the relevant mappings can be cleared
+for DEVICE_REMOVED and PORT_REMOVED device events. By default it is disabled.
\ No newline at end of file
diff --git a/api/pom.xml b/api/pom.xml
new file mode 100644
index 0000000..042b5e4
--- /dev/null
+++ b/api/pom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2017-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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>maclearner</artifactId>
+        <groupId>org.opencord</groupId>
+        <version>1.0.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>maclearner-api</artifactId>
+    <packaging>bundle</packaging>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/api/src/main/java/org/opencord/maclearner/api/DefaultMacLearner.java b/api/src/main/java/org/opencord/maclearner/api/DefaultMacLearner.java
new file mode 100644
index 0000000..17109f3
--- /dev/null
+++ b/api/src/main/java/org/opencord/maclearner/api/DefaultMacLearner.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.api;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+/**
+ * Default Mac Learner object model implementation.
+ */
+public class DefaultMacLearner implements MacLearner {
+
+    private MacLearnerKey macLearnerKey;
+    private MacAddress macAddress;
+
+    public static final String DEVICE_ID_FN = "deviceId";
+    public static final String PORT_NUMBER_FN = "portNumber";
+    public static final String VLAN_ID_FN = "vlanId";
+    public static final String MAC_ADDRESS_FN = "macAddress";
+
+    @Override
+    public DeviceId deviceId() {
+        return macLearnerKey.getDeviceId();
+    }
+
+    @Override
+    public PortNumber portNumber() {
+        return macLearnerKey.getPortNumber();
+    }
+
+    @Override
+    public VlanId vlanId() {
+        return macLearnerKey.getVlanId();
+    }
+
+    @Override
+    public MacAddress macAddress() {
+        return macAddress;
+    }
+
+    public DefaultMacLearner(DeviceId deviceId, PortNumber portNumber, VlanId vlanId, MacAddress macAddress) {
+        this.macLearnerKey = new MacLearnerKey(deviceId, portNumber, vlanId);
+        this.macAddress = macAddress;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this)
+                .append(DEVICE_ID_FN, deviceId())
+                .append(PORT_NUMBER_FN, portNumber())
+                .append(VLAN_ID_FN, vlanId())
+                .append(MAC_ADDRESS_FN, macAddress())
+                .toString();
+    }
+
+}
diff --git a/api/src/main/java/org/opencord/maclearner/api/MacDeleteResult.java b/api/src/main/java/org/opencord/maclearner/api/MacDeleteResult.java
new file mode 100644
index 0000000..3db8a62
--- /dev/null
+++ b/api/src/main/java/org/opencord/maclearner/api/MacDeleteResult.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.api;
+
+/**
+ * Result of Delete Mac Mapping operation.
+ */
+public enum MacDeleteResult {
+    SUCCESSFUL,
+    UNSUCCESSFUL,
+    NOT_EXIST
+}
diff --git a/api/src/main/java/org/opencord/maclearner/api/MacLearner.java b/api/src/main/java/org/opencord/maclearner/api/MacLearner.java
new file mode 100644
index 0000000..1ec8d36
--- /dev/null
+++ b/api/src/main/java/org/opencord/maclearner/api/MacLearner.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.api;
+
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+/**
+ * Representation of a mac learner object.
+ */
+public interface MacLearner {
+
+    /**
+     * Returns the device identifier.
+     *
+     * @return device id
+     */
+    DeviceId deviceId();
+
+    /**
+     * Returns from which port the mac address is learned.
+     * @return port number
+     */
+    PortNumber portNumber();
+
+    /**
+     * Returns which vlan id of the package the mac address is learned from.
+     * If packet is double tagged, vlan id is equal to QinQVID.
+     * @return vlan id
+     */
+    VlanId vlanId();
+
+    /**
+     * Returns Mac Address information.
+     * @return mac address
+     */
+    MacAddress macAddress();
+
+}
diff --git a/api/src/main/java/org/opencord/maclearner/api/MacLearnerEvent.java b/api/src/main/java/org/opencord/maclearner/api/MacLearnerEvent.java
new file mode 100644
index 0000000..d28e31d
--- /dev/null
+++ b/api/src/main/java/org/opencord/maclearner/api/MacLearnerEvent.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.api;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.onosproject.event.AbstractEvent;
+
+/**
+ * Entity that represents Mac Learner events.
+ */
+public class MacLearnerEvent extends AbstractEvent<MacLearnerEvent.Type, MacLearner> {
+
+    public enum Type {
+
+        /**
+         * New entry added to MacLearnerMap.
+         */
+        ADDED,
+        /**
+         * An entry removed from MacLearnerMap.
+         */
+        REMOVED,
+    }
+
+    /**
+     * Creates an event due to mac learner map update.
+     * @param type type of event
+     * @param macLearner mac learner object
+     */
+    public MacLearnerEvent(MacLearnerEvent.Type type, MacLearner macLearner) {
+        super(type, macLearner);
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this)
+                .append(super.toString())
+                .toString();
+    }
+
+}
diff --git a/api/src/main/java/org/opencord/maclearner/api/MacLearnerKey.java b/api/src/main/java/org/opencord/maclearner/api/MacLearnerKey.java
new file mode 100644
index 0000000..d892f18
--- /dev/null
+++ b/api/src/main/java/org/opencord/maclearner/api/MacLearnerKey.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.api;
+
+import org.onlab.packet.VlanId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+import java.util.Objects;
+
+/**
+ * Key of Mac Address Map.
+ */
+public class MacLearnerKey {
+
+    private DeviceId deviceId;
+    private PortNumber portNumber;
+    private VlanId vlanId;
+
+    public DeviceId getDeviceId() {
+        return deviceId;
+    }
+
+    public void setDeviceId(DeviceId deviceId) {
+        this.deviceId = deviceId;
+    }
+
+    public PortNumber getPortNumber() {
+        return portNumber;
+    }
+
+    public void setPortNumber(PortNumber portNumber) {
+        this.portNumber = portNumber;
+    }
+
+    public VlanId getVlanId() {
+        return vlanId;
+    }
+
+    public void setVlanId(VlanId vlanId) {
+        this.vlanId = vlanId;
+    }
+
+    public MacLearnerKey(DeviceId deviceId, PortNumber portNumber, VlanId vlanId) {
+        this.deviceId = deviceId;
+        this.portNumber = portNumber;
+        this.vlanId = vlanId;
+    }
+
+    @Override
+    public String toString() {
+        return "MacLearnerKey{" +
+                "deviceId=" + deviceId +
+                ", portNumber=" + portNumber +
+                ", vlanId=" + vlanId +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        MacLearnerKey that = (MacLearnerKey) o;
+        return Objects.equals(deviceId, that.deviceId) &&
+                Objects.equals(portNumber, that.portNumber) &&
+                Objects.equals(vlanId, that.vlanId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(deviceId, portNumber, vlanId);
+    }
+
+}
diff --git a/api/src/main/java/org/opencord/maclearner/api/MacLearnerListener.java b/api/src/main/java/org/opencord/maclearner/api/MacLearnerListener.java
new file mode 100644
index 0000000..e8ea9cb
--- /dev/null
+++ b/api/src/main/java/org/opencord/maclearner/api/MacLearnerListener.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.api;
+
+import org.onosproject.event.EventListener;
+
+/**
+ * Entity capable of receiving Mac Learner related events.
+ */
+public interface MacLearnerListener extends EventListener<MacLearnerEvent> {
+}
diff --git a/api/src/main/java/org/opencord/maclearner/api/MacLearnerProvider.java b/api/src/main/java/org/opencord/maclearner/api/MacLearnerProvider.java
new file mode 100644
index 0000000..fb4ace9
--- /dev/null
+++ b/api/src/main/java/org/opencord/maclearner/api/MacLearnerProvider.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.api;
+
+import org.onosproject.net.provider.Provider;
+
+/**
+ * Abstraction of mac learner provider.
+ */
+public interface MacLearnerProvider extends Provider {
+}
diff --git a/api/src/main/java/org/opencord/maclearner/api/MacLearnerProviderService.java b/api/src/main/java/org/opencord/maclearner/api/MacLearnerProviderService.java
new file mode 100644
index 0000000..226aa98
--- /dev/null
+++ b/api/src/main/java/org/opencord/maclearner/api/MacLearnerProviderService.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.api;
+
+import org.onosproject.net.provider.ProviderService;
+
+/**
+ * Abstraction of a flow rule provider service.
+ */
+public interface MacLearnerProviderService extends ProviderService<MacLearnerProvider> {
+}
diff --git a/api/src/main/java/org/opencord/maclearner/api/MacLearnerService.java b/api/src/main/java/org/opencord/maclearner/api/MacLearnerService.java
new file mode 100644
index 0000000..1fc3d97
--- /dev/null
+++ b/api/src/main/java/org/opencord/maclearner/api/MacLearnerService.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.api;
+
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.event.ListenerService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Mac Learner Service.
+ */
+public interface MacLearnerService extends ListenerService<MacLearnerEvent, MacLearnerListener> {
+
+    /**
+     * Adding a port to ignore map to prevent mac mapping.
+     * @param deviceId openflow device id
+     * @param portNumber port number
+     */
+    void addPortToIgnore(DeviceId deviceId, PortNumber portNumber);
+
+    /**
+     * Removing a port from ignore port map.
+     * @param deviceId openflow device id
+     * @param portNumber port number
+     */
+    void removeFromIgnoredPorts(DeviceId deviceId, PortNumber portNumber);
+
+    /**
+     * Getting All MAC Mappings.
+     * @return Map of MAC Addresses by device id, port number and vlan id
+     */
+    Map<MacLearnerKey, MacAddress> getAllMappings();
+
+    /**
+     * Getting Requested MAC Address.
+     * @param deviceId openflow device id
+     * @param portNumber port number
+     * @param vlanId vlan id
+     * @return  MAC Address of requested device id, port number and vlan id
+     */
+    Optional<MacAddress> getMacMapping(DeviceId deviceId, PortNumber portNumber, VlanId vlanId);
+
+    /**
+     * Deleting a MAC Mapping.
+     * @param deviceId openflow device id
+     * @param portNumber port number
+     * @param vlanId vlan id
+     * @return MacDeleteResult situation after method call
+     */
+    MacDeleteResult deleteMacMapping(DeviceId deviceId, PortNumber portNumber, VlanId vlanId);
+
+    /**
+     * Deleting MAC Mappings of port.
+     * @param deviceId openflow device id
+     * @param portNumber port number
+     * @return true if the mappings deleted successfully; false otherwise
+     */
+    boolean deleteMacMappings(DeviceId deviceId, PortNumber portNumber);
+
+    /**
+     * Deleting MAC Mappings of device.
+     * @param deviceId openflow device id
+     * @return true if the mappings deleted successfully; false otherwise
+     */
+    boolean deleteMacMappings(DeviceId deviceId);
+
+    /**
+     * Getting Device List in MAC Mapping List.
+     * @return mapped ONOS devices
+     */
+    Set<DeviceId> getMappedDevices();
+
+    /**
+     * Getting Port Number List in MAC Mapping List.
+     * @return mapped port numbers
+     */
+    Set<PortNumber> getMappedPorts();
+
+    /**
+     * Getting Ignored Ports for MAC Mapping.
+     * @return device and its ignored ports map
+     */
+    Map<DeviceId, Set<PortNumber>> getIgnoredPorts();
+
+}
diff --git a/api/src/main/java/org/opencord/maclearner/api/MacLearnerValue.java b/api/src/main/java/org/opencord/maclearner/api/MacLearnerValue.java
new file mode 100644
index 0000000..bc1f244
--- /dev/null
+++ b/api/src/main/java/org/opencord/maclearner/api/MacLearnerValue.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.api;
+
+import org.onlab.packet.MacAddress;
+
+import java.util.Objects;
+
+/**
+ * Value of Mac Address Map which keeps a timestamp via Mac Address.
+ */
+public class MacLearnerValue {
+
+    private MacAddress macAddress;
+    private long timestamp;
+
+    public MacAddress getMacAddress() {
+        return macAddress;
+    }
+
+    public void setMacAddress(MacAddress macAddress) {
+        this.macAddress = macAddress;
+    }
+
+    public long getTimestamp() {
+        return timestamp;
+    }
+
+    public void setTimestamp(long timestamp) {
+        this.timestamp = timestamp;
+    }
+
+    public MacLearnerValue(MacAddress macAddress, long timestamp) {
+        this.macAddress = macAddress;
+        this.timestamp = timestamp;
+    }
+
+    @Override
+    public String toString() {
+        return "MacLearnerValue{" +
+                "macAddress=" + macAddress +
+                ", timestamp=" + timestamp +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        MacLearnerValue that = (MacLearnerValue) o;
+        return timestamp == that.timestamp &&
+                Objects.equals(macAddress, that.macAddress);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(macAddress, timestamp);
+    }
+
+}
diff --git a/api/src/main/java/org/opencord/maclearner/api/package-info.java b/api/src/main/java/org/opencord/maclearner/api/package-info.java
new file mode 100755
index 0000000..11e53de
--- /dev/null
+++ b/api/src/main/java/org/opencord/maclearner/api/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2017-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.
+ */
+
+/**
+ * Mac Learner API.
+ */
+package org.opencord.maclearner.api;
diff --git a/app/app.xml b/app/app.xml
new file mode 100644
index 0000000..1cb39b2
--- /dev/null
+++ b/app/app.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015-present Open Networking Laboratory
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<app name="org.opencord.maclearner" origin="${onos.app.origin}"
+     version="${project.version}"
+     title="Mac Learner" category="Utility" url="${onos.app.url}"
+     featuresRepo="mvn:${project.groupId}/${project.artifactId}/${project.version}/xml/features"
+     features="${project.artifactId}">
+    <description>Mapping clients' MAC Address and Port Number information temporarily.</description>
+    <artifact>mvn:${project.groupId}/${project.artifactId}/${project.version}</artifact>
+    <artifact>mvn:${project.groupId}/maclearner-api/${project.version}</artifact>
+</app>
diff --git a/app/features.xml b/app/features.xml
new file mode 100644
index 0000000..ca3587a
--- /dev/null
+++ b/app/features.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ Copyright 2017-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.
+  -->
+<features xmlns="http://karaf.apache.org/xmlns/features/v1.2.0" name="${project.artifactId}-${project.version}">
+    <feature name="${project.artifactId}" version="${project.version}"
+             description="${project.description}">
+        <bundle>mvn:${project.groupId}/maclearner-api/${project.version}</bundle>
+        <bundle>mvn:${project.groupId}/${project.artifactId}/${project.version}</bundle>
+    </feature>
+</features>
\ No newline at end of file
diff --git a/app/pom.xml b/app/pom.xml
new file mode 100644
index 0000000..fff910f
--- /dev/null
+++ b/app/pom.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2017-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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>maclearner</artifactId>
+        <groupId>org.opencord</groupId>
+        <version>1.0.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>maclearner-app</artifactId>
+    <packaging>bundle</packaging>
+
+    <properties>
+        <!--For REST-->
+        <web.context>/onos/v2</web.context>
+        <api.version>2.0.0</api.version>
+        <api.title>MAC Learner REST API</api.title>
+        <api.description>MAC Learner REST API</api.description>
+        <api.package>org.opencord.maclearner.app.rest</api.package>
+        <!--For REST-->
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.opencord</groupId>
+            <artifactId>maclearner-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-cli</artifactId>
+            <version>${onos.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-core-serializers</artifactId>
+            <version>${onos.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.karaf.shell</groupId>
+            <artifactId>org.apache.karaf.shell.console</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-api</artifactId>
+            <version>${onos.version}</version>
+            <scope>test</scope>
+            <classifier>tests</classifier>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onlab-junit</artifactId>
+            <version>2.2.2</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.onosproject</groupId>
+                <artifactId>onos-maven-plugin</artifactId>
+            </plugin>
+            <!--For REST-->
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <_wab>src/main/webapp/</_wab>
+                        <Include-Resource>
+                            WEB-INF/classes/apidoc/swagger.json=target/swagger.json,
+                            {maven-resources}
+                        </Include-Resource>
+                        <Bundle-SymbolicName>
+                            ${project.groupId}.${project.artifactId}
+                        </Bundle-SymbolicName>
+                        <Import-Package>
+                            *,org.glassfish.jersey.servlet
+                        </Import-Package>
+                        <Web-ContextPath>${web.context}</Web-ContextPath>
+                        <!--For Command-->
+                        <Karaf-Commands>org.opencord.maclearner.app.cli</Karaf-Commands>
+                        <!--For Command-->
+                    </instructions>
+                </configuration>
+            </plugin>
+            <!--For REST-->
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/app/src/main/java/org/opencord/maclearner/app/cli/MacLearnerAddIgnorePort.java b/app/src/main/java/org/opencord/maclearner/app/cli/MacLearnerAddIgnorePort.java
new file mode 100644
index 0000000..614bcbe
--- /dev/null
+++ b/app/src/main/java/org/opencord/maclearner/app/cli/MacLearnerAddIgnorePort.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.app.cli;
+
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.opencord.maclearner.api.MacLearnerService;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+/**
+ * Adds a port of device will not be MAC mapping.
+ */
+@Service
+@Command(scope = "onos", name = "mac-learner-add-ignore-port",
+        description = "Adds a port of device will not be MAC mapping")
+public class MacLearnerAddIgnorePort extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "deviceId",
+            description = "OpenFlow Device Id",
+            required = true)
+    String devId = null;
+
+    @Argument(index = 1, name = "portNo",
+            description = "Integer value of Port Number",
+            required = true)
+    Integer portNo;
+
+    @Override
+    protected void doExecute() {
+        try {
+            MacLearnerService macLearnerService = AbstractShellCommand.get(MacLearnerService.class);
+            macLearnerService.addPortToIgnore(DeviceId.deviceId(devId),
+                    PortNumber.portNumber(portNo));
+
+        } catch (IllegalArgumentException e) {
+            String msg = String.format("Exception occurred while executing %s command",
+                    this.getClass().getSimpleName());
+            print(msg);
+            log.error(msg, e);
+        }
+    }
+
+}
diff --git a/app/src/main/java/org/opencord/maclearner/app/cli/MacLearnerDeleteMapping.java b/app/src/main/java/org/opencord/maclearner/app/cli/MacLearnerDeleteMapping.java
new file mode 100644
index 0000000..6e8149a
--- /dev/null
+++ b/app/src/main/java/org/opencord/maclearner/app/cli/MacLearnerDeleteMapping.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.app.cli;
+
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onlab.packet.VlanId;
+import org.opencord.maclearner.api.MacDeleteResult;
+import org.opencord.maclearner.api.MacLearnerService;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+/**
+ * Deletes MAC Address information of client connected to requested device and port.
+ */
+@Service
+@Command(scope = "onos", name = "mac-learner-delete-mapping",
+        description = "Deletes MAC Address information of client connected to requested device and port")
+public class MacLearnerDeleteMapping extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "deviceId",
+            description = "OpenFlow Device Id",
+            required = true)
+    @Completion(MappedDeviceIdCompleter.class)
+    String devId = null;
+
+    @Argument(index = 1, name = "portNo",
+            description = "Integer value of Port Number",
+            required = true)
+    @Completion(MappedPortNumberCompleter.class)
+    Integer portNo;
+
+    @Argument(index = 2, name = "vlanId",
+            description = "Short value of Vlan Id",
+            required = true)
+    Short vlanId;
+
+    private static final String DELETE_MAPPING_SUCCESS = "Mac Mapping Successfully Deleted.";
+    private static final String DELETE_MAPPING_FAILURE = "Mac Mapping Deletion Failed.";
+    private static final String MAPPING_NOT_FOUND = "Mac Mapping requested to delete is not found.";
+
+    @Override
+    protected void doExecute() {
+        MacLearnerService macLearnerService = AbstractShellCommand.get(MacLearnerService.class);
+        try {
+            if (portNo == null || devId == null || vlanId == null) {
+                throw new IllegalArgumentException();
+            }
+
+            MacDeleteResult result = macLearnerService.deleteMacMapping(DeviceId.deviceId(devId),
+                    PortNumber.portNumber(portNo),
+                    VlanId.vlanId(vlanId));
+            switch (result) {
+                case SUCCESSFUL:
+                    print(DELETE_MAPPING_SUCCESS);
+                    break;
+                case NOT_EXIST:
+                    print(MAPPING_NOT_FOUND);
+                    break;
+                case UNSUCCESSFUL:
+                    print(DELETE_MAPPING_FAILURE);
+                    break;
+                default:
+                    throw new IllegalArgumentException();
+            }
+
+        } catch (IllegalArgumentException e) {
+            String msg = String.format("Exception occurred while executing %s command",
+                    this.getClass().getSimpleName());
+            print(msg);
+            log.error(msg, e);
+        }
+    }
+
+}
diff --git a/app/src/main/java/org/opencord/maclearner/app/cli/MacLearnerGetMapping.java b/app/src/main/java/org/opencord/maclearner/app/cli/MacLearnerGetMapping.java
new file mode 100644
index 0000000..8de3cbd
--- /dev/null
+++ b/app/src/main/java/org/opencord/maclearner/app/cli/MacLearnerGetMapping.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.app.cli;
+
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onlab.packet.VlanId;
+import org.opencord.maclearner.api.MacLearnerKey;
+import org.opencord.maclearner.api.MacLearnerService;
+import org.onlab.packet.MacAddress;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Gets MAC Address information of client connected to requested device and port.
+ */
+@Service
+@Command(scope = "onos", name = "mac-learner-get-mapping",
+        description = "Gets MAC Address information of client connected to requested device and port")
+public class MacLearnerGetMapping extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "deviceId",
+            description = "OpenFlow Device Id")
+    @Completion(MappedDeviceIdCompleter.class)
+    String devId = null;
+
+    @Argument(index = 1, name = "portNo",
+            description = "Integer value of Port Number")
+    @Completion(MappedPortNumberCompleter.class)
+    Integer portNo;
+
+    @Argument(index = 2, name = "vlanId",
+            description = "Short value of Vlan Id")
+    Short vlanId;
+
+    @Override
+    protected void doExecute() {
+        try {
+            MacLearnerService macLearnerService = AbstractShellCommand.get(MacLearnerService.class);
+            if (portNo == null && devId == null && vlanId == null) {
+                Map<MacLearnerKey, MacAddress> mapMacAddressMap = macLearnerService.getAllMappings();
+                for (Map.Entry<MacLearnerKey, MacAddress> entry : mapMacAddressMap.entrySet()) {
+                    print("Client with MAC: %s and VlanID: %s, uses port number: %s of device with id: %s",
+                          entry.getValue(),
+                          entry.getKey().getVlanId(),
+                          entry.getKey().getPortNumber(),
+                          entry.getKey().getDeviceId());
+                }
+            } else if (portNo != null && devId != null && vlanId != null) {
+                Optional<MacAddress> macAddress = macLearnerService.getMacMapping(DeviceId.deviceId(devId),
+                                                                                  PortNumber.portNumber(portNo),
+                                                                                  VlanId.vlanId(vlanId));
+                if (!macAddress.isPresent()) {
+                    print("MAC Address not found with given parameters.\nUse -1 for VlanId=None");
+                } else {
+                    print(String.format("MAC: %s", macAddress.get()));
+                }
+            } else {
+                print("Either device id, port number and vlan id must be entered or not at all!");
+            }
+
+        } catch (IllegalArgumentException e) {
+            String msg = String.format("Exception occurred while executing %s command",
+                    this.getClass().getSimpleName());
+            print(msg);
+            log.error(msg, e);
+        }
+    }
+
+}
diff --git a/app/src/main/java/org/opencord/maclearner/app/cli/MacLearnerListIgnoredPorts.java b/app/src/main/java/org/opencord/maclearner/app/cli/MacLearnerListIgnoredPorts.java
new file mode 100644
index 0000000..7f66dc2
--- /dev/null
+++ b/app/src/main/java/org/opencord/maclearner/app/cli/MacLearnerListIgnoredPorts.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.app.cli;
+
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.opencord.maclearner.api.MacLearnerService;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Gets ignored port information for MAC Mapping.
+ */
+@Service
+@Command(scope = "onos", name = "mac-learner-list-ignored-ports",
+        description = "Gets ignored port information for MAC Mapping")
+public class MacLearnerListIgnoredPorts extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "deviceId",
+            description = "Device Id")
+    @Completion(MappedDeviceIdCompleter.class)
+    String devId = null;
+
+    @Override
+    protected void doExecute() {
+        try {
+            MacLearnerService macLearnerService = AbstractShellCommand.get(MacLearnerService.class);
+            Map<DeviceId, Set<PortNumber>> ignoredPorts = macLearnerService.getIgnoredPorts();
+            if (devId == null) {
+                if (ignoredPorts != null && !ignoredPorts.isEmpty()) {
+                    for (Map.Entry<DeviceId, Set<PortNumber>> entry : ignoredPorts.entrySet()) {
+                        print("Port(s): %s ignored of device with ID: %s", entry.getValue(), entry.getKey());
+                    }
+                } else {
+                    print("There is no ignored port.");
+                }
+            } else {
+                Set<PortNumber> portNumbers = ignoredPorts.get(DeviceId.deviceId(devId));
+                if (!ignoredPorts.isEmpty()) {
+                    print("Port(s): %s ignored of device with ID: %s", portNumbers, devId);
+                } else {
+                    print("There is no ignored port of device with ID: %s", devId);
+                }
+            }
+
+        } catch (IllegalArgumentException e) {
+            String msg = String.format("Exception occurred while executing %s command",
+                    this.getClass().getSimpleName());
+            print(msg);
+            log.error(msg, e);
+        }
+    }
+
+}
diff --git a/app/src/main/java/org/opencord/maclearner/app/cli/MacLearnerRemoveIgnorePort.java b/app/src/main/java/org/opencord/maclearner/app/cli/MacLearnerRemoveIgnorePort.java
new file mode 100644
index 0000000..62aed8d
--- /dev/null
+++ b/app/src/main/java/org/opencord/maclearner/app/cli/MacLearnerRemoveIgnorePort.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.app.cli;
+
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.opencord.maclearner.api.MacLearnerService;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+
+/**
+ * Removes a port of device from Ignore Map.
+ */
+@Service
+@Command(scope = "onos", name = "mac-learner-remove-ignore-port",
+        description = "Removes a port of device from Ignore Map")
+public class MacLearnerRemoveIgnorePort extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "deviceId",
+            description = "OpenFlow Device Id",
+            required = true)
+    String devId = null;
+
+    @Argument(index = 1, name = "portNo",
+            description = "Integer value of Port Number",
+            required = true)
+    Integer portNo;
+
+    @Override
+    protected void doExecute() {
+        try {
+            MacLearnerService macLearnerService = AbstractShellCommand.get(MacLearnerService.class);
+            macLearnerService.removeFromIgnoredPorts(DeviceId.deviceId(devId),
+                                                     PortNumber.portNumber(portNo));
+
+        } catch (IllegalArgumentException e) {
+            String msg = String.format("Exception occurred while executing %s command",
+                    this.getClass().getSimpleName());
+            print(msg);
+            log.error(msg, e);
+        }
+    }
+
+}
diff --git a/app/src/main/java/org/opencord/maclearner/app/cli/MappedDeviceIdCompleter.java b/app/src/main/java/org/opencord/maclearner/app/cli/MappedDeviceIdCompleter.java
new file mode 100644
index 0000000..8f64bff
--- /dev/null
+++ b/app/src/main/java/org/opencord/maclearner/app/cli/MappedDeviceIdCompleter.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.app.cli;
+
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+import org.onosproject.cli.AbstractShellCommand;
+import org.opencord.maclearner.api.MacLearnerService;
+import org.onosproject.net.DeviceId;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.SortedSet;
+
+/**
+ * CLI completer for mapped device ids.
+ */
+@Service
+public class MappedDeviceIdCompleter implements Completer {
+
+    @Override
+    public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+        // Delegate string completer
+        StringsCompleter delegate = new StringsCompleter();
+        MacLearnerService macLearnerService = AbstractShellCommand.get(MacLearnerService.class);
+        Iterator<DeviceId> it = macLearnerService.getMappedDevices().iterator();
+        SortedSet<String> strings = delegate.getStrings();
+
+        while (it.hasNext()) {
+            strings.add(it.next().toString());
+        }
+
+        // Now let the completer do the work for figuring out what to offer.
+        return delegate.complete(session, commandLine, candidates);
+    }
+
+}
diff --git a/app/src/main/java/org/opencord/maclearner/app/cli/MappedPortNumberCompleter.java b/app/src/main/java/org/opencord/maclearner/app/cli/MappedPortNumberCompleter.java
new file mode 100644
index 0000000..829f1e7
--- /dev/null
+++ b/app/src/main/java/org/opencord/maclearner/app/cli/MappedPortNumberCompleter.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.app.cli;
+
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+import org.opencord.maclearner.api.MacLearnerService;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.PortNumber;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.SortedSet;
+
+/**
+ * CLI completer for mapped port numbers.
+ */
+@Service
+public class MappedPortNumberCompleter implements Completer {
+
+    @Override
+    public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+        // Delegate string completer
+        StringsCompleter delegate = new StringsCompleter();
+        MacLearnerService macLearnerService = AbstractShellCommand.get(MacLearnerService.class);
+        Iterator<PortNumber> it = macLearnerService.getMappedPorts().iterator();
+        SortedSet<String> strings = delegate.getStrings();
+
+        while (it.hasNext()) {
+            strings.add(it.next().toString());
+        }
+
+        // Now let the completer do the work for figuring out what to offer.
+        return delegate.complete(session, commandLine, candidates);
+    }
+
+}
diff --git a/app/src/main/java/org/opencord/maclearner/app/cli/package-info.java b/app/src/main/java/org/opencord/maclearner/app/cli/package-info.java
new file mode 100755
index 0000000..5270265
--- /dev/null
+++ b/app/src/main/java/org/opencord/maclearner/app/cli/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2017-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.
+ */
+
+/**
+ * Mac Learner CLI.
+ */
+package org.opencord.maclearner.app.cli;
diff --git a/app/src/main/java/org/opencord/maclearner/app/impl/MacLearnerManager.java b/app/src/main/java/org/opencord/maclearner/app/impl/MacLearnerManager.java
new file mode 100644
index 0000000..0a73e3e
--- /dev/null
+++ b/app/src/main/java/org/opencord/maclearner/app/impl/MacLearnerManager.java
@@ -0,0 +1,554 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.app.impl;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.onlab.packet.VlanId;
+import org.onlab.util.Tools;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.Versioned;
+import org.opencord.maclearner.api.DefaultMacLearner;
+import org.opencord.maclearner.api.MacDeleteResult;
+import org.opencord.maclearner.api.MacLearnerEvent;
+import org.opencord.maclearner.api.MacLearnerKey;
+import org.opencord.maclearner.api.MacLearnerListener;
+import org.opencord.maclearner.api.MacLearnerProvider;
+import org.opencord.maclearner.api.MacLearnerProviderService;
+import org.opencord.maclearner.api.MacLearnerService;
+import org.opencord.maclearner.api.MacLearnerValue;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.onlab.packet.DHCP;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IPv4;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.UDP;
+import org.onlab.packet.dhcp.DhcpOption;
+import org.onlab.util.KryoNamespace;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.packet.PacketContext;
+import org.onosproject.net.packet.PacketProcessor;
+import org.onosproject.net.packet.PacketService;
+import org.onosproject.net.provider.AbstractListenerProviderRegistry;
+import org.onosproject.net.provider.AbstractProviderService;
+import org.onosproject.store.LogicalTimestamp;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.Serializer;
+import org.onosproject.store.service.WallClockTimestamp;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.URI;
+import java.util.Date;
+import java.util.Dictionary;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_MessageType;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.opencord.maclearner.app.impl.OsgiPropertyConstants.CACHE_DURATION_DEFAULT;
+import static org.opencord.maclearner.app.impl.OsgiPropertyConstants.CACHE_DURATION;
+import static org.opencord.maclearner.app.impl.OsgiPropertyConstants.ENABLE_DEVICE_LISTENER;
+import static org.opencord.maclearner.app.impl.OsgiPropertyConstants.ENABLE_DEVICE_LISTENER_DEFAULT;
+import static org.osgi.service.component.annotations.ReferenceCardinality.MANDATORY;
+
+/**
+ * Mac Learner Service implementation.
+ */
+@Component(immediate = true,
+        property = {
+                CACHE_DURATION + ":Integer=" + CACHE_DURATION_DEFAULT,
+                ENABLE_DEVICE_LISTENER + ":Boolean=" + ENABLE_DEVICE_LISTENER_DEFAULT
+        },
+        service = MacLearnerService.class
+)
+public class MacLearnerManager
+        extends AbstractListenerProviderRegistry<MacLearnerEvent, MacLearnerListener,
+        MacLearnerProvider, MacLearnerProviderService>
+        implements MacLearnerService {
+
+    private static final String MAC_LEARNER_APP = "org.opencord.maclearner";
+    private static final String MAC_LEARNER = "maclearner";
+    private ApplicationId appId;
+
+    private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
+    private ScheduledFuture scheduledFuture;
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    @Reference(cardinality = MANDATORY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = MANDATORY)
+    protected MastershipService mastershipService;
+
+    @Reference(cardinality = MANDATORY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = MANDATORY)
+    protected PacketService packetService;
+
+    @Reference(cardinality = MANDATORY)
+    protected StorageService storageService;
+
+
+    @Reference(cardinality = MANDATORY)
+    protected ComponentConfigService componentConfigService;
+
+    private MacLearnerPacketProcessor macLearnerPacketProcessor =
+            new MacLearnerPacketProcessor();
+
+    private DeviceListener deviceListener = new InternalDeviceListener();
+
+    /**
+     * Minimum duration of mapping, mapping can be exist until 2*cacheDuration because of cleanerTimer fixed rate.
+     */
+    protected int cacheDurationSec = CACHE_DURATION_DEFAULT;
+
+    /**
+     * Register a device event listener for removing mappings from MAC Address Map for removed events.
+     */
+    protected boolean enableDeviceListener = ENABLE_DEVICE_LISTENER_DEFAULT;
+
+    private ConsistentMap<DeviceId, Set<PortNumber>> ignoredPortsMap;
+    private ConsistentMap<MacLearnerKey, MacLearnerValue> macAddressMap;
+
+    protected ExecutorService eventExecutor;
+
+    @Activate
+    public void activate() {
+        eventExecutor = Executors.newFixedThreadPool(5, groupedThreads("onos/maclearner",
+                "events-%d", log));
+        appId = coreService.registerApplication(MAC_LEARNER_APP);
+        componentConfigService.registerProperties(getClass());
+        eventDispatcher.addSink(MacLearnerEvent.class, listenerRegistry);
+        macAddressMap = storageService.<MacLearnerKey, MacLearnerValue>consistentMapBuilder()
+                .withName(MAC_LEARNER)
+                .withSerializer(createSerializer())
+                .withApplicationId(appId)
+                .build();
+        ignoredPortsMap = storageService
+                .<DeviceId, Set<PortNumber>>consistentMapBuilder()
+                .withName("maclearner-ignored")
+                .withSerializer(createSerializer())
+                .withApplicationId(appId)
+                .build();
+        //mac learner must process the packet before director processors
+        packetService.addProcessor(macLearnerPacketProcessor,
+                PacketProcessor.advisor(2));
+        if (enableDeviceListener) {
+            deviceService.addListener(deviceListener);
+        }
+        createSchedulerForClearMacMappings();
+        log.info("{} is started.", getClass().getSimpleName());
+    }
+
+    private Serializer createSerializer() {
+        return Serializer.using(KryoNamespace.newBuilder()
+                .register(KryoNamespace.newBuilder().build(MAC_LEARNER))
+                // not so robust way to avoid collision with other
+                // user supplied registrations
+                .nextId(KryoNamespaces.BEGIN_USER_CUSTOM_ID + 100)
+                .register(KryoNamespaces.BASIC)
+                .register(LogicalTimestamp.class)
+                .register(WallClockTimestamp.class)
+                .register(MacLearnerKey.class)
+                .register(MacLearnerValue.class)
+                .register(DeviceId.class)
+                .register(URI.class)
+                .register(PortNumber.class)
+                .register(VlanId.class)
+                .register(MacAddress.class)
+                .build(MAC_LEARNER + "-ecmap"));
+    }
+
+    private void createSchedulerForClearMacMappings() {
+        scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(this::clearExpiredMacMappings,
+                0,
+                cacheDurationSec,
+                TimeUnit.SECONDS);
+    }
+
+    private void clearExpiredMacMappings() {
+        Date curDate = new Date();
+        for (Map.Entry<MacLearnerKey, Versioned<MacLearnerValue>> entry : macAddressMap.entrySet()) {
+            if (!mastershipService.isLocalMaster(entry.getKey().getDeviceId())) {
+                return;
+            }
+            if (curDate.getTime() - entry.getValue().value().getTimestamp() > cacheDurationSec * 1000) {
+                removeFromMacAddressMap(entry.getKey());
+            }
+        }
+    }
+
+    @Deactivate
+    public void deactivate() {
+        if (scheduledFuture != null) {
+            scheduledFuture.cancel(true);
+        }
+        packetService.removeProcessor(macLearnerPacketProcessor);
+        if (enableDeviceListener) {
+            deviceService.removeListener(deviceListener);
+        }
+        eventDispatcher.removeSink(MacLearnerEvent.class);
+        componentConfigService.unregisterProperties(getClass(), false);
+        log.info("{} is stopped.", getClass().getSimpleName());
+    }
+
+    @Modified
+    public void modified(ComponentContext context) {
+        Dictionary<?, ?> properties = context != null ? context.getProperties() : new Properties();
+
+        String cacheDuration = Tools.get(properties, CACHE_DURATION);
+        if (!isNullOrEmpty(cacheDuration)) {
+            int cacheDur = Integer.parseInt(cacheDuration.trim());
+            if (cacheDurationSec != cacheDur) {
+                setMacMappingCacheDuration(cacheDur);
+            }
+        }
+
+        Boolean enableDevListener = Tools.isPropertyEnabled(properties, ENABLE_DEVICE_LISTENER);
+        if (enableDevListener != null && enableDeviceListener != enableDevListener) {
+            enableDeviceListener = enableDevListener;
+            log.info("enableDeviceListener parameter changed to: {}", enableDeviceListener);
+            if (this.enableDeviceListener) {
+                deviceService.addListener(deviceListener);
+            } else {
+                deviceService.removeListener(deviceListener);
+            }
+        }
+    }
+
+    private Integer setMacMappingCacheDuration(Integer second) {
+        if (cacheDurationSec == second) {
+            log.info("Cache duration already: {}", second);
+            return second;
+        }
+        log.info("Changing cache duration to: {} second from {} second...", second, cacheDurationSec);
+        this.cacheDurationSec = second;
+        if (scheduledFuture != null) {
+            scheduledFuture.cancel(false);
+        }
+        createSchedulerForClearMacMappings();
+        return cacheDurationSec;
+    }
+
+    @Override
+    public void addPortToIgnore(DeviceId deviceId, PortNumber portNumber) {
+        log.info("Adding ignore port: {} {}", deviceId, portNumber);
+        Set<PortNumber> updatedPorts = Sets.newHashSet();
+        Versioned<Set<PortNumber>> storedPorts = ignoredPortsMap.get(deviceId);
+        if (storedPorts == null || !storedPorts.value().contains(portNumber)) {
+            if (storedPorts != null) {
+                updatedPorts.addAll(storedPorts.value());
+            }
+            updatedPorts.add(portNumber);
+            ignoredPortsMap.put(deviceId, updatedPorts);
+            log.info("Port:{} of device: {} is added to ignoredPortsMap.", portNumber, deviceId);
+            deleteMacMappings(deviceId, portNumber);
+        } else {
+            log.warn("Port:{} of device: {} is already ignored.", portNumber, deviceId);
+        }
+    }
+
+    @Override
+    public void removeFromIgnoredPorts(DeviceId deviceId, PortNumber portNumber) {
+        log.info("Removing ignore port: {} {}", deviceId, portNumber);
+        Versioned<Set<PortNumber>> storedPorts = ignoredPortsMap.get(deviceId);
+        if (storedPorts != null && storedPorts.value().contains(portNumber)) {
+            if (storedPorts.value().size() == 1) {
+                ignoredPortsMap.remove(deviceId);
+            } else {
+                Set<PortNumber> updatedPorts = Sets.newHashSet();
+                updatedPorts.addAll(storedPorts.value());
+                updatedPorts.remove(portNumber);
+                ignoredPortsMap.put(deviceId, updatedPorts);
+            }
+            log.info("Port:{} of device: {} is removed ignoredPortsMap.", portNumber, deviceId);
+        } else {
+            log.warn("Port:{} of device: {} is not found in ignoredPortsMap.", portNumber, deviceId);
+        }
+    }
+
+    @Override
+    public ImmutableMap<MacLearnerKey, MacAddress> getAllMappings() {
+        log.info("Getting all MAC Mappings");
+        Map<MacLearnerKey, MacAddress> immutableMap = Maps.newHashMap();
+        macAddressMap.entrySet().forEach(entry ->
+                immutableMap.put(entry.getKey(),
+                        entry.getValue() != null ? entry.getValue().value().getMacAddress() : null));
+        return ImmutableMap.copyOf(immutableMap);
+    }
+
+    @Override
+    public Optional<MacAddress> getMacMapping(DeviceId deviceId, PortNumber portNumber, VlanId vlanId) {
+        log.info("Getting MAC mapping for: {} {} {}", deviceId, portNumber, vlanId);
+        Versioned<MacLearnerValue> value = macAddressMap.get(new MacLearnerKey(deviceId, portNumber, vlanId));
+        return value != null ? Optional.ofNullable(value.value().getMacAddress()) : Optional.empty();
+    }
+
+    @Override
+    public MacDeleteResult deleteMacMapping(DeviceId deviceId, PortNumber portNumber, VlanId vlanId) {
+        log.info("Deleting MAC mapping for: {} {} {}", deviceId, portNumber, vlanId);
+        MacLearnerKey key = new MacLearnerKey(deviceId, portNumber, vlanId);
+        return removeFromMacAddressMap(key);
+    }
+
+    @Override
+    public boolean deleteMacMappings(DeviceId deviceId, PortNumber portNumber) {
+        log.info("Deleting MAC mappings for: {} {}", deviceId, portNumber);
+        Set<Map.Entry<MacLearnerKey, Versioned<MacLearnerValue>>> entriesToDelete = macAddressMap.entrySet().stream()
+                .filter(entry -> entry.getKey().getDeviceId().equals(deviceId) &&
+                        entry.getKey().getPortNumber().equals(portNumber))
+                .collect(Collectors.toSet());
+        if (entriesToDelete.isEmpty()) {
+            log.warn("MAC mapping not found for deviceId: {} and portNumber: {}", deviceId, portNumber);
+            return false;
+        }
+        entriesToDelete.forEach(e -> removeFromMacAddressMap(e.getKey()));
+        return true;
+    }
+
+    @Override
+    public boolean deleteMacMappings(DeviceId deviceId) {
+        log.info("Deleting MAC mappings for: {}", deviceId);
+        Set<Map.Entry<MacLearnerKey, Versioned<MacLearnerValue>>> entriesToDelete = macAddressMap.entrySet().stream()
+                .filter(entry -> entry.getKey().getDeviceId().equals(deviceId))
+                .collect(Collectors.toSet());
+        if (entriesToDelete.isEmpty()) {
+            log.warn("MAC mapping not found for deviceId: {}", deviceId);
+            return false;
+        }
+        entriesToDelete.forEach(e -> removeFromMacAddressMap(e.getKey()));
+        return true;
+    }
+
+    @Override
+    public ImmutableSet<DeviceId> getMappedDevices() {
+        Set<DeviceId> deviceIds = Sets.newHashSet();
+        for (Map.Entry<MacLearnerKey, MacAddress> entry : getAllMappings().entrySet()) {
+            deviceIds.add(entry.getKey().getDeviceId());
+        }
+        return ImmutableSet.copyOf(deviceIds);
+    }
+
+    @Override
+    public ImmutableSet<PortNumber> getMappedPorts() {
+        Set<PortNumber> portNumbers = Sets.newHashSet();
+        for (Map.Entry<MacLearnerKey, MacAddress> entry : getAllMappings().entrySet()) {
+            portNumbers.add(entry.getKey().getPortNumber());
+        }
+        return ImmutableSet.copyOf(portNumbers);
+    }
+
+    @Override
+    public ImmutableMap<DeviceId, Set<PortNumber>> getIgnoredPorts() {
+        log.info("Getting ignored ports");
+        Map<DeviceId, Set<PortNumber>> immutableMap = Maps.newHashMap();
+        ignoredPortsMap.forEach(entry -> immutableMap.put(entry.getKey(),
+                entry.getValue() != null ? entry.getValue().value() : Sets.newHashSet()));
+        return ImmutableMap.copyOf(immutableMap);
+    }
+
+    @Override
+    protected MacLearnerProviderService createProviderService(MacLearnerProvider provider) {
+        return new InternalMacLearnerProviderService(provider);
+    }
+
+    private static class InternalMacLearnerProviderService extends AbstractProviderService<MacLearnerProvider>
+            implements MacLearnerProviderService {
+
+        InternalMacLearnerProviderService(MacLearnerProvider provider) {
+            super(provider);
+        }
+    }
+
+    private void sendMacLearnerEvent(MacLearnerEvent.Type type, DeviceId deviceId,
+                                     PortNumber portNumber, VlanId vlanId, MacAddress macAddress) {
+        log.info("Sending MAC Learner Event: type: {} deviceId: {} portNumber: {} vlanId: {} macAddress: {}",
+                type, deviceId, portNumber, vlanId.toShort(), macAddress);
+        DefaultMacLearner macLearner = new DefaultMacLearner(deviceId, portNumber, vlanId, macAddress);
+        MacLearnerEvent macLearnerEvent = new MacLearnerEvent(type, macLearner);
+        post(macLearnerEvent);
+    }
+
+    private class MacLearnerPacketProcessor implements PacketProcessor {
+
+        @Override
+        public void process(PacketContext context) {
+            // process the packet and get the payload
+            Ethernet packet = context.inPacket().parsed();
+
+            if (packet == null) {
+                log.warn("Packet is null");
+                return;
+            }
+
+            PortNumber sourcePort = context.inPacket().receivedFrom().port();
+            DeviceId deviceId = context.inPacket().receivedFrom().deviceId();
+
+            Versioned<Set<PortNumber>> ignoredPortsOfDevice = ignoredPortsMap.get(deviceId);
+            if (ignoredPortsOfDevice != null && ignoredPortsOfDevice.value().contains(sourcePort)) {
+                log.warn("Port Number: {} is in ignoredPortsMap. Returning", sourcePort);
+                return;
+            }
+
+            if (packet.getEtherType() == Ethernet.TYPE_IPV4) {
+                IPv4 ipv4Packet = (IPv4) packet.getPayload();
+
+                if (ipv4Packet.getProtocol() == IPv4.PROTOCOL_UDP) {
+                    UDP udpPacket = (UDP) ipv4Packet.getPayload();
+                    int udpSourcePort = udpPacket.getSourcePort();
+                    if ((udpSourcePort == UDP.DHCP_CLIENT_PORT) || (udpSourcePort == UDP.DHCP_SERVER_PORT)) {
+                        DHCP dhcpPayload = (DHCP) udpPacket.getPayload();
+                        //This packet is dhcp.
+                        VlanId vlanId = packet.getQinQVID() != -1 ?
+                                VlanId.vlanId(packet.getQinQVID()) : VlanId.vlanId(packet.getVlanID());
+                        processDhcpPacket(context, packet, dhcpPayload, sourcePort, deviceId, vlanId);
+                    }
+                }
+            }
+        }
+
+        //process the dhcp packet before forwarding
+        private void processDhcpPacket(PacketContext context, Ethernet packet,
+                                       DHCP dhcpPayload, PortNumber sourcePort, DeviceId deviceId, VlanId vlanId) {
+            if (dhcpPayload == null) {
+                log.warn("DHCP payload is null");
+                return;
+            }
+
+            DHCP.MsgType incomingPacketType = getDhcpPacketType(dhcpPayload);
+
+            if (incomingPacketType == null) {
+                log.warn("Incoming packet type is null!");
+                return;
+            }
+
+            log.info("Received DHCP Packet of type {} from {}",
+                    incomingPacketType, context.inPacket().receivedFrom());
+
+            if (incomingPacketType.equals(DHCP.MsgType.DHCPDISCOVER) ||
+                    incomingPacketType.equals(DHCP.MsgType.DHCPREQUEST)) {
+                addToMacAddressMap(deviceId, sourcePort, vlanId, packet.getSourceMAC());
+            }
+        }
+
+        // get type of the DHCP packet
+        private DHCP.MsgType getDhcpPacketType(DHCP dhcpPayload) {
+
+            for (DhcpOption option : dhcpPayload.getOptions()) {
+                if (option.getCode() == OptionCode_MessageType.getValue()) {
+                    byte[] data = option.getData();
+                    return DHCP.MsgType.getType(data[0]);
+                }
+            }
+            return null;
+        }
+
+        private void addToMacAddressMap(DeviceId deviceId, PortNumber portNumber,
+                                        VlanId vlanId, MacAddress macAddress) {
+            Versioned<MacLearnerValue> prevMacAddress =
+                    macAddressMap.put(new MacLearnerKey(deviceId, portNumber, vlanId),
+                            new MacLearnerValue(macAddress, new Date().getTime()));
+            if (prevMacAddress != null && !prevMacAddress.value().getMacAddress().equals(macAddress)) {
+                sendMacLearnerEvent(MacLearnerEvent.Type.REMOVED,
+                        deviceId,
+                        portNumber,
+                        vlanId,
+                        prevMacAddress.value().getMacAddress());
+            } else if (prevMacAddress == null || !prevMacAddress.value().getMacAddress().equals(macAddress)) {
+                // Not sending event for already mapped
+                log.info("Mapped MAC: {} for port: {} of deviceId: {} and vlanId: {}",
+                        macAddress, portNumber, deviceId, vlanId);
+                sendMacLearnerEvent(MacLearnerEvent.Type.ADDED, deviceId, portNumber, vlanId, macAddress);
+            }
+        }
+
+    }
+
+    private MacDeleteResult removeFromMacAddressMap(MacLearnerKey macLearnerKey) {
+        Versioned<MacLearnerValue> verMacAddress = macAddressMap.remove(macLearnerKey);
+        if (verMacAddress != null) {
+            log.info("Mapping removed. deviceId: {} portNumber: {} vlanId: {} macAddress: {}",
+                    macLearnerKey.getDeviceId(), macLearnerKey.getPortNumber(),
+                    verMacAddress.value(), verMacAddress.value().getMacAddress());
+            sendMacLearnerEvent(MacLearnerEvent.Type.REMOVED,
+                    macLearnerKey.getDeviceId(),
+                    macLearnerKey.getPortNumber(),
+                    macLearnerKey.getVlanId(),
+                    verMacAddress.value().getMacAddress());
+            return MacDeleteResult.SUCCESSFUL;
+        } else {
+            log.warn("MAC not removed, because mapping not found for deviceId: {} and portNumber: {} and vlanId: {}",
+                    macLearnerKey.getDeviceId(),
+                    macLearnerKey.getPortNumber(),
+                    macLearnerKey.getVlanId());
+            return MacDeleteResult.NOT_EXIST;
+        }
+    }
+
+    private class InternalDeviceListener implements DeviceListener {
+
+        @Override
+        public void event(DeviceEvent event) {
+            eventExecutor.execute(() -> {
+                switch (event.type()) {
+                    case DEVICE_REMOVED:
+                        deleteMacMappings(event.subject().id());
+                        break;
+                    case PORT_REMOVED:
+                        deleteMacMappings(event.subject().id(), event.port().number());
+                        break;
+                    default:
+                        log.debug("Unhandled device event for Mac Learner: {}", event.type());
+                }
+            });
+        }
+
+        @Override
+        public boolean isRelevant(DeviceEvent event) {
+            return mastershipService.isLocalMaster(event.subject().id());
+        }
+
+    }
+
+}
diff --git a/app/src/main/java/org/opencord/maclearner/app/impl/OsgiPropertyConstants.java b/app/src/main/java/org/opencord/maclearner/app/impl/OsgiPropertyConstants.java
new file mode 100644
index 0000000..d57d322
--- /dev/null
+++ b/app/src/main/java/org/opencord/maclearner/app/impl/OsgiPropertyConstants.java
@@ -0,0 +1,33 @@
+/*
+ * 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.opencord.maclearner.app.impl;
+
+/**
+ * Constants for default values of configurable properties.
+ */
+public final class OsgiPropertyConstants {
+
+    private OsgiPropertyConstants() {
+    }
+
+    public static final String CACHE_DURATION = "cacheDurationSec";
+    public static final int CACHE_DURATION_DEFAULT = 86400; // 1 day
+
+    public static final String ENABLE_DEVICE_LISTENER = "enableDeviceListener";
+    public static final boolean ENABLE_DEVICE_LISTENER_DEFAULT = false;
+
+}
diff --git a/app/src/main/java/org/opencord/maclearner/app/impl/package-info.java b/app/src/main/java/org/opencord/maclearner/app/impl/package-info.java
new file mode 100755
index 0000000..df7a157
--- /dev/null
+++ b/app/src/main/java/org/opencord/maclearner/app/impl/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2017-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.
+ */
+
+/**
+ * Mac Learner Service Implementation.
+ */
+package org.opencord.maclearner.app.impl;
diff --git a/app/src/main/java/org/opencord/maclearner/app/rest/MacLearnerCodec.java b/app/src/main/java/org/opencord/maclearner/app/rest/MacLearnerCodec.java
new file mode 100644
index 0000000..cb7771a
--- /dev/null
+++ b/app/src/main/java/org/opencord/maclearner/app/rest/MacLearnerCodec.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.app.rest;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.codec.CodecContext;
+import org.onosproject.codec.JsonCodec;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.opencord.maclearner.api.DefaultMacLearner;
+import org.opencord.maclearner.api.MacLearner;
+import org.opencord.maclearner.api.MacLearnerKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * MAC Learner JSON codec.
+ */
+public final class MacLearnerCodec extends JsonCodec<MacLearner> {
+
+    private static final Logger log = LoggerFactory.getLogger(MacLearnerCodec.class);
+
+    @Override
+    public ObjectNode encode(MacLearner macLearner, CodecContext context) {
+        checkNotNull(macLearner, "macLearner cannot be null");
+
+        ObjectMapper mapper = context.mapper();
+        ObjectNode ofAgentNode = mapper.createObjectNode();
+        ofAgentNode
+                .put(DefaultMacLearner.DEVICE_ID_FN, macLearner.deviceId().toString())
+                .put(DefaultMacLearner.PORT_NUMBER_FN, macLearner.portNumber().toString())
+                .put(DefaultMacLearner.VLAN_ID_FN, macLearner.vlanId().toString())
+                .put(DefaultMacLearner.MAC_ADDRESS_FN, macLearner.macAddress().toString());
+        return ofAgentNode;
+    }
+
+    public ObjectNode encodePort(MacLearnerKey ignoredPort, CodecContext context) {
+        checkNotNull(ignoredPort, "ignoredPort cannot be null");
+
+        ObjectMapper mapper = context.mapper();
+        ObjectNode ofAgentNode = mapper.createObjectNode();
+        ofAgentNode
+                .put(DefaultMacLearner.DEVICE_ID_FN, ignoredPort.getDeviceId().toString())
+                .put(DefaultMacLearner.PORT_NUMBER_FN, ignoredPort.getPortNumber().toString());
+        return ofAgentNode;
+    }
+
+    public ObjectNode encodeMac(MacAddress macAddress, CodecContext context) {
+        checkNotNull(macAddress, "macAddress cannot be null");
+
+        ObjectMapper mapper = context.mapper();
+        ObjectNode ofAgentNode = mapper.createObjectNode();
+        ofAgentNode
+                .put(DefaultMacLearner.MAC_ADDRESS_FN, macAddress.toString());
+        return ofAgentNode;
+    }
+
+    @Override
+    public MacLearner decode(ObjectNode json, CodecContext context) {
+        JsonNode deviceId = json.get(DefaultMacLearner.DEVICE_ID_FN);
+        checkNotNull(deviceId);
+        JsonNode portNumber = json.get(DefaultMacLearner.PORT_NUMBER_FN);
+        checkNotNull(portNumber);
+        JsonNode vlanId = json.get(DefaultMacLearner.VLAN_ID_FN);
+        checkNotNull(vlanId);
+        JsonNode macAddress = json.get(DefaultMacLearner.MAC_ADDRESS_FN);
+        checkNotNull(macAddress);
+
+        return new DefaultMacLearner(DeviceId.deviceId(deviceId.asText()),
+                PortNumber.portNumber(portNumber.asLong()),
+                VlanId.vlanId(vlanId.shortValue()),
+                MacAddress.valueOf(macAddress.asText()));
+    }
+
+}
diff --git a/app/src/main/java/org/opencord/maclearner/app/rest/MacLearnerWebApplication.java b/app/src/main/java/org/opencord/maclearner/app/rest/MacLearnerWebApplication.java
new file mode 100644
index 0000000..968925b
--- /dev/null
+++ b/app/src/main/java/org/opencord/maclearner/app/rest/MacLearnerWebApplication.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.app.rest;
+
+import org.onlab.rest.AbstractWebApplication;
+
+import java.util.Set;
+
+/**
+ * Mac Learner REST API web application.
+ */
+public class MacLearnerWebApplication extends AbstractWebApplication {
+
+    @Override
+    public Set<Class<?>> getClasses() {
+        return getClasses(MacLearnerWebResource.class);
+    }
+
+}
diff --git a/app/src/main/java/org/opencord/maclearner/app/rest/MacLearnerWebResource.java b/app/src/main/java/org/opencord/maclearner/app/rest/MacLearnerWebResource.java
new file mode 100644
index 0000000..334540e
--- /dev/null
+++ b/app/src/main/java/org/opencord/maclearner/app/rest/MacLearnerWebResource.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.app.rest;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.rest.AbstractWebResource;
+import org.opencord.maclearner.api.DefaultMacLearner;
+import org.opencord.maclearner.api.MacDeleteResult;
+import org.opencord.maclearner.api.MacLearnerKey;
+import org.opencord.maclearner.api.MacLearnerService;
+import org.slf4j.Logger;
+
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.slf4j.LoggerFactory.getLogger;
+import static javax.ws.rs.core.Response.Status.*;
+
+/**
+ * Mac Learner web resource.
+ */
+@Path("maclearner")
+public class MacLearnerWebResource extends AbstractWebResource {
+
+    MacLearnerCodec codec = new MacLearnerCodec();
+
+    private final MacLearnerService macLearnerService = get(MacLearnerService.class);
+
+    private final Logger log = getLogger(getClass());
+
+    private static final String INVALID_PATH_PARAMETERS = "Invalid path parameters";
+    private static final String PATH_DELIMITER = "/";
+
+    /**
+     * Get all MAC Mappings.
+     *
+     * @return list of MAC Mapping json object with deviceId, portNumber, vlanId, macAddress fields
+     */
+    @GET
+    @Path("/mapping/all")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response getAllMappings() {
+        Map<MacLearnerKey, MacAddress> macMappings = macLearnerService.getAllMappings();
+        ObjectMapper mapper = new ObjectMapper();
+        ObjectNode root = mapper.createObjectNode();
+        ArrayNode macArray = mapper.createArrayNode();
+        if (macMappings == null) {
+            root.set("data", macArray);
+            return Response.ok(root, MediaType.APPLICATION_JSON_TYPE).build();
+        } else {
+            macMappings.forEach((k, v) -> macArray.add(
+                    codec.encode(new DefaultMacLearner(k.getDeviceId(),
+                            k.getPortNumber(),
+                            k.getVlanId(),
+                            v), this)
+            ));
+        }
+        root.set("data", macArray);
+        return Response.ok(root, MediaType.APPLICATION_JSON_TYPE).build();
+    }
+
+    /**
+     * Get MAC Mapping for request paramaters.
+     *
+     * @param ofDeviceId device id
+     * @param portNumber port number
+     * @param vlanId     vlan id
+     * @return MAC Address json object with macAddress field
+     * 204 NO_CONTENT if it does not exist
+     */
+    @GET
+    @Path("/mapping/{ofDeviceId}/{portNumber}/{vlanId}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response getMacMapping(@PathParam("ofDeviceId") String ofDeviceId,
+                                  @PathParam("portNumber") Integer portNumber,
+                                  @PathParam("vlanId") Short vlanId) {
+        Optional<MacAddress> mac = macLearnerService.getMacMapping(DeviceId.deviceId(ofDeviceId),
+                PortNumber.portNumber(portNumber),
+                VlanId.vlanId(vlanId));
+        if (mac.isEmpty()) {
+            log.warn("MAC Address not found for: ofDeviceId:{} portNumber:{} vlanId:{}",
+                    ofDeviceId, portNumber, vlanId);
+            return Response.status(NO_CONTENT).build();
+        }
+        ObjectMapper mapper = new ObjectMapper();
+        ObjectNode root = mapper.createObjectNode();
+        root.set("data", codec.encodeMac(mac.get(), this));
+        return Response.ok(root, MediaType.APPLICATION_JSON_TYPE).status(OK).build();
+    }
+
+    /**
+     * Delete MAC Mapping for request paramaters.
+     *
+     * @param ofDeviceId device id
+     * @param portNumber port number
+     * @param vlanId     vlan id
+     * @return URI of request
+     */
+    @DELETE
+    @Path("/mapping/{ofDeviceId}/{portNumber}/{vlanId}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response deleteMacMapping(@PathParam("ofDeviceId") String ofDeviceId,
+                                     @PathParam("portNumber") Integer portNumber,
+                                     @PathParam("vlanId") Short vlanId) {
+        try {
+            if (ofDeviceId == null || portNumber == null || vlanId == null) {
+                throw new IllegalArgumentException(INVALID_PATH_PARAMETERS);
+            }
+            MacDeleteResult deleteResult = macLearnerService.deleteMacMapping(DeviceId.deviceId(ofDeviceId),
+                    PortNumber.portNumber(portNumber),
+                    VlanId.vlanId(vlanId));
+            if (deleteResult.equals(MacDeleteResult.UNSUCCESSFUL)) {
+                return Response.status(NO_CONTENT).build();
+            }
+            return Response
+                    .created(new URI("/delete/mapping/" +
+                            ofDeviceId + PATH_DELIMITER +
+                            portNumber + PATH_DELIMITER +
+                            vlanId))
+                    .status(OK)
+                    .build();
+        } catch (URISyntaxException e) {
+            log.error("URI Syntax Exception occurred while deleting MAC Mapping " +
+                            "for deviceId: {} portNumber: {} vlanId: {}",
+                    ofDeviceId, portNumber, vlanId, e);
+            return Response.serverError().build();
+        }
+    }
+
+    /**
+     * Delete MAC Mappings for specific port of a device.
+     *
+     * @param ofDeviceId device id
+     * @param portNumber port number
+     * @return URI of request
+     */
+    @DELETE
+    @Path("/mappings/{ofDeviceId}/{portNumber}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response deleteMacMappings(@PathParam("ofDeviceId") String ofDeviceId,
+                                      @PathParam("portNumber") Integer portNumber) {
+        try {
+            if (ofDeviceId == null || portNumber == null) {
+                throw new IllegalArgumentException(INVALID_PATH_PARAMETERS);
+            }
+            boolean deleteSuccess = macLearnerService.deleteMacMappings(DeviceId.deviceId(ofDeviceId),
+                    PortNumber.portNumber(portNumber));
+            if (!deleteSuccess) {
+                return Response.status(NO_CONTENT).build();
+            }
+            return Response.created(new URI("/delete/mappings/" +
+                    ofDeviceId + PATH_DELIMITER +
+                    portNumber)).status(OK).build();
+        } catch (URISyntaxException e) {
+            log.error("URI Syntax Exception occurred while deleting MAC Mappings for deviceId: {} portNumber: {}",
+                    ofDeviceId, portNumber, e);
+            return Response.serverError().build();
+        }
+    }
+
+    /**
+     * Get ignored ports for MAC Mapping.
+     *
+     * @return list of ignored port json object with deviceId and portNumber fields
+     */
+    @GET
+    @Path("/ports/ignored")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response getIgnoredPorts() {
+        Map<DeviceId, Set<PortNumber>> ignoredPorts = macLearnerService.getIgnoredPorts();
+        ObjectMapper mapper = new ObjectMapper();
+        ObjectNode root = mapper.createObjectNode();
+        ArrayNode ignoredPortsArray = mapper.createArrayNode();
+        if (ignoredPorts == null) {
+            root.set("data", ignoredPortsArray);
+            return Response.ok(root, MediaType.APPLICATION_JSON_TYPE).build();
+        } else {
+            for (Map.Entry<DeviceId, Set<PortNumber>> entry : ignoredPorts.entrySet()) {
+                entry.getValue().forEach(portNumber -> ignoredPortsArray.add(
+                        codec.encodePort(new MacLearnerKey(entry.getKey(), portNumber, VlanId.NONE), this)
+                ));
+            }
+        }
+        root.set("data", ignoredPortsArray);
+        return Response.ok(root, MediaType.APPLICATION_JSON_TYPE).build();
+    }
+
+    /**
+     * Add to ignore ports map.
+     *
+     * @param ofDeviceId deviceId
+     * @param portNumber portNumber
+     * @return URI of request
+     */
+    @POST
+    @Path("/ignore-port/{ofDeviceId}/{portNumber}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response addPortToIgnore(@PathParam("ofDeviceId") String ofDeviceId,
+                                    @PathParam("portNumber") Integer portNumber) {
+        try {
+            if (ofDeviceId == null || portNumber == null) {
+                throw new IllegalArgumentException(INVALID_PATH_PARAMETERS);
+            }
+            macLearnerService.addPortToIgnore(DeviceId.deviceId(ofDeviceId),
+                    PortNumber.portNumber(portNumber));
+            return Response.created(new URI("/add/ignore-port/" +
+                    ofDeviceId + PATH_DELIMITER +
+                    portNumber)).status(OK).build();
+        } catch (URISyntaxException e) {
+            log.error("URI Syntax Exception occurred while adding ignore port deviceId: {} portNumber {}",
+                    ofDeviceId, portNumber, e);
+            return Response.serverError().build();
+        }
+    }
+
+    /**
+     * Remove from ignored ports map.
+     *
+     * @param ofDeviceId device id
+     * @param portNumber port number
+     * @return URI of request
+     */
+    @DELETE
+    @Path("/ignore-port/{ofDeviceId}/{portNumber}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response removeFromIgnoredPorts(@PathParam("ofDeviceId") String ofDeviceId,
+                                           @PathParam("portNumber") Integer portNumber) {
+        try {
+            if (ofDeviceId == null || portNumber == null) {
+                throw new IllegalArgumentException(INVALID_PATH_PARAMETERS);
+            }
+            macLearnerService.removeFromIgnoredPorts(DeviceId.deviceId(ofDeviceId),
+                    PortNumber.portNumber(portNumber));
+            return Response.created(new URI("/remove/ignore-port/" +
+                    ofDeviceId + PATH_DELIMITER +
+                    portNumber)).status(OK).build();
+        } catch (URISyntaxException e) {
+            log.error("URISyntaxException occurred while removing ignore port deviceId: {} portNumber {}",
+                    ofDeviceId, portNumber, e);
+            return Response.serverError().build();
+        }
+    }
+
+}
diff --git a/app/src/main/java/org/opencord/maclearner/app/rest/package-info.java b/app/src/main/java/org/opencord/maclearner/app/rest/package-info.java
new file mode 100755
index 0000000..564a108
--- /dev/null
+++ b/app/src/main/java/org/opencord/maclearner/app/rest/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2017-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.
+ */
+
+/**
+ * Mac Learner REST API.
+ */
+package org.opencord.maclearner.app.rest;
diff --git a/app/src/main/webapp/WEB-INF/web.xml b/app/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..c842ded
--- /dev/null
+++ b/app/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2017-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.
+  -->
+<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://java.sun.com/xml/ns/javaee"
+         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
+         id="ONOS" version="2.5">
+    <display-name>Mac Learner REST API v1.0</display-name>
+
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>Secured</web-resource-name>
+            <url-pattern>/*</url-pattern>
+        </web-resource-collection>
+        <auth-constraint>
+            <role-name>admin</role-name>
+        </auth-constraint>
+    </security-constraint>
+
+    <security-role>
+        <role-name>admin</role-name>
+    </security-role>
+
+    <login-config>
+        <auth-method>BASIC</auth-method>
+        <realm-name>karaf</realm-name>
+    </login-config>
+
+    <servlet>
+        <servlet-name>JAX-RS Service</servlet-name>
+        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
+        <init-param>
+            <param-name>javax.ws.rs.Application</param-name>
+            <param-value>org.opencord.maclearner.app.rest.MacLearnerWebApplication</param-value>
+        </init-param>
+        <load-on-startup>1</load-on-startup>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>JAX-RS Service</servlet-name>
+        <url-pattern>/*</url-pattern>
+    </servlet-mapping>
+</web-app>
diff --git a/app/src/test/java/org/opencord/maclearner/app/impl/MacLearnerManagerTest.java b/app/src/test/java/org/opencord/maclearner/app/impl/MacLearnerManagerTest.java
new file mode 100644
index 0000000..1e05637
--- /dev/null
+++ b/app/src/test/java/org/opencord/maclearner/app/impl/MacLearnerManagerTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.app.impl;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Set of tests of the Mac Learner ONOS application component.
+ */
+public class MacLearnerManagerTest extends TestBaseMacLearner {
+
+    @Before
+    public void setUp() {
+        setUpApp();
+    }
+
+    @After
+    public void tearDown() {
+        this.macLearnerManager.deactivate();
+    }
+
+    private static final MacAddress CLIENT_MAC = MacAddress.valueOf("00:00:00:00:00:01");
+    private static final VlanId CLIENT_VLAN = VlanId.vlanId("100");
+    private static final VlanId CLIENT_QINQ_VLAN = VlanId.vlanId("200");
+    private static final ConnectPoint CLIENT_CP = ConnectPoint.deviceConnectPoint("of:0000000000000001/1");
+
+    @Test
+    public void testSingleTagDhcpPacket() {
+        packetService.processPacket(new TestDhcpRequestPacketContext(CLIENT_MAC,
+                CLIENT_VLAN,
+                VlanId.NONE,
+                CLIENT_CP));
+        Optional<MacAddress> macAddress = macLearnerManager.getMacMapping(CLIENT_CP.deviceId(),
+                CLIENT_CP.port(), CLIENT_VLAN);
+        assertTrue(macAddress.isPresent());
+        assertEquals(CLIENT_MAC, macAddress.get());
+    }
+
+    @Test
+    public void testDoubleTagDhcpPacket() {
+        packetService.processPacket(new TestDhcpRequestPacketContext(CLIENT_MAC,
+                CLIENT_VLAN,
+                CLIENT_QINQ_VLAN,
+                CLIENT_CP));
+        Optional<MacAddress> macAddress = macLearnerManager.getMacMapping(CLIENT_CP.deviceId(),
+                CLIENT_CP.port(), CLIENT_QINQ_VLAN);
+        assertTrue(macAddress.isPresent());
+        assertEquals(CLIENT_MAC, macAddress.get());
+    }
+
+}
diff --git a/app/src/test/java/org/opencord/maclearner/app/impl/TestBaseMacLearner.java b/app/src/test/java/org/opencord/maclearner/app/impl/TestBaseMacLearner.java
new file mode 100644
index 0000000..c9d4af5
--- /dev/null
+++ b/app/src/test/java/org/opencord/maclearner/app/impl/TestBaseMacLearner.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright 2017-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.opencord.maclearner.app.impl;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.onlab.junit.TestUtils;
+import org.onlab.packet.DHCP;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IPv4;
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.UDP;
+import org.onlab.packet.VlanId;
+import org.onlab.packet.dhcp.DhcpOption;
+import org.onosproject.cfg.ComponentConfigAdapter;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreServiceAdapter;
+import org.onosproject.event.DefaultEventSinkRegistry;
+import org.onosproject.event.Event;
+import org.onosproject.event.EventDeliveryService;
+import org.onosproject.event.EventSink;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.packet.DefaultInboundPacket;
+import org.onosproject.net.packet.InboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.net.packet.PacketContext;
+import org.onosproject.net.packet.PacketContextAdapter;
+import org.onosproject.net.packet.PacketProcessor;
+import org.onosproject.net.packet.PacketServiceAdapter;
+import org.onosproject.store.service.AsyncConsistentMap;
+import org.onosproject.store.service.AsyncDistributedSet;
+import org.onosproject.store.service.AtomicCounter;
+import org.onosproject.store.service.ConsistentMap;
+import org.onosproject.store.service.ConsistentMapAdapter;
+import org.onosproject.store.service.ConsistentMapBuilder;
+import org.onosproject.store.service.DistributedSet;
+import org.onosproject.store.service.DistributedSetAdapter;
+import org.onosproject.store.service.DistributedSetBuilder;
+import org.onosproject.store.service.MapEvent;
+import org.onosproject.store.service.MapEventListener;
+import org.onosproject.store.service.SetEventListener;
+import org.onosproject.store.service.StorageServiceAdapter;
+import org.onosproject.store.service.Versioned;
+
+import java.lang.reflect.Field;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.BiFunction;
+
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Mac Learner mock services class.
+ */
+public abstract class TestBaseMacLearner {
+
+    private static final Ip4Address SERVER_IP = Ip4Address.valueOf("10.0.3.253");
+    private static final Ip4Address INTERFACE_IP = Ip4Address.valueOf("10.0.3.254");
+
+    protected MacLearnerManager macLearnerManager;
+    protected ObjectMapper mapper;
+    protected ApplicationId subject;
+
+    protected ComponentConfigService componentConfigService = new MockComponentConfigService();
+    protected MockCoreService coreService = new MockCoreService();
+    protected MockStorageService storageService = new MockStorageService();
+    protected MockPacketService packetService = new MockPacketService();
+
+    public void setUpApp() {
+        macLearnerManager = new MacLearnerManager();
+        macLearnerManager.componentConfigService = this.componentConfigService;
+        macLearnerManager.coreService = this.coreService;
+        macLearnerManager.storageService = this.storageService;
+        macLearnerManager.packetService = this.packetService;
+        injectEventDispatcher(macLearnerManager, new MockEventDispatcher());
+        mapper = new ObjectMapper();
+        subject = macLearnerManager.coreService.registerApplication("org.opencord.maclearner");
+
+        macLearnerManager.activate();
+    }
+
+    /**
+     * Mocks an instance of {@link ApplicationId} so that the application
+     * component under test can query and use its application ID.
+     */
+    private static final class MockApplicationId implements ApplicationId {
+
+        private final short id;
+        private final String name;
+
+        public MockApplicationId(short id, String name) {
+            this.id = id;
+            this.name = name;
+        }
+
+        @Override
+        public short id() {
+            return id;
+        }
+
+        @Override
+        public String name() {
+            return name;
+        }
+    }
+
+    private static final class MockComponentConfigService extends ComponentConfigAdapter {
+
+    }
+
+    /**
+     * Mocks the core services of ONOS so that the application under test can
+     * register and query application IDs.
+     */
+    private static final class MockCoreService extends CoreServiceAdapter {
+
+        private List<ApplicationId> idList = Lists.newArrayList();
+        private Map<String, ApplicationId> idMap = Maps.newHashMap();
+
+        @Override
+        public ApplicationId getAppId(Short id) {
+            if (id >= idList.size()) {
+                return null;
+            }
+            return idList.get(id);
+        }
+
+        @Override
+        public ApplicationId getAppId(String name) {
+            return idMap.get(name);
+        }
+
+        @Override
+        public ApplicationId registerApplication(String name) {
+            ApplicationId appId = idMap.get(name);
+            if (appId == null) {
+                appId = new MockApplicationId((short) idList.size(), name);
+                idList.add(appId);
+                idMap.put(name, appId);
+            }
+            return appId;
+        }
+
+    }
+
+    /**
+     * Mocks the storage service of ONOS so that the application under test can
+     * use consistent maps.
+     */
+    private static class MockStorageService extends StorageServiceAdapter {
+
+        @Override
+        public <K, V> ConsistentMapBuilder<K, V> consistentMapBuilder() {
+            ConsistentMapBuilder<K, V> builder = new ConsistentMapBuilder<K, V>() {
+                @Override
+                public AsyncConsistentMap<K, V> buildAsyncMap() {
+                    return null;
+                }
+
+                @Override
+                public ConsistentMap<K, V> build() {
+                    return new TestConsistentMap<>();
+                }
+            };
+
+            return builder;
+        }
+
+        @Override
+        public <E> DistributedSetBuilder<E> setBuilder() {
+            DistributedSetBuilder<E> builder = new DistributedSetBuilder<E>() {
+                @Override
+                public AsyncDistributedSet<E> build() {
+                    return new DistributedSetAdapter<E>() {
+                        @Override
+                        public DistributedSet<E> asDistributedSet() {
+                            return new TestDistributedSet<>();
+                        }
+                    };
+                }
+            };
+
+            return builder;
+        }
+
+        @Override
+        public AtomicCounter getAtomicCounter(String name) {
+            return new MockAtomicCounter();
+        }
+
+        // Mock ConsistentMap that behaves as a HashMap
+        class TestConsistentMap<K, V> extends ConsistentMapAdapter<K, V> {
+            private Map<K, Versioned<V>> map = new HashMap<>();
+            private Map<MapEventListener<K, V>, Executor> listeners = new HashMap<>();
+
+            public void notifyListeners(MapEvent<K, V> event) {
+                listeners.forEach((c, e) -> e.execute(() -> c.event(event)));
+            }
+
+            @Override
+            public int size() {
+                return map.size();
+            }
+
+            @Override
+            public Versioned<V> put(K key, V value) {
+                Versioned<V> oldValue = map.get(key);
+                Versioned<V> newValue = new Versioned<>(value, oldValue == null ? 0 : oldValue.version() + 1);
+                map.put(key, newValue);
+                notifyListeners(new MapEvent<>(name(), key, newValue, oldValue));
+                return newValue;
+            }
+
+            @Override
+            public Versioned<V> get(K key) {
+                return map.get(key);
+            }
+
+            @Override
+            public Versioned<V> remove(K key) {
+                Versioned<V> oldValue = map.remove(key);
+                notifyListeners(new MapEvent<>(name(), key, oldValue, null));
+                return oldValue;
+            }
+
+            @Override
+            public Versioned<V> computeIfPresent(K key,
+                                                 BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+                Versioned<V> oldValue = map.get(key);
+                Versioned<V> newValue = new Versioned<>(remappingFunction.apply(key, oldValue.value()),
+                        oldValue == null ? 0 : oldValue.version() + 1);
+                map.put(key, newValue);
+                notifyListeners(new MapEvent<>(name(), key, newValue, oldValue));
+                return newValue;
+            }
+
+            @Override
+            public Set<Map.Entry<K, Versioned<V>>> entrySet() {
+                return map.entrySet();
+            }
+
+            @Override
+            public Set<K> keySet() {
+                return map.keySet();
+            }
+
+            @Override
+            public Collection<Versioned<V>> values() {
+                return map.values();
+            }
+
+            @Override
+            public void clear() {
+                map.clear();
+            }
+
+            @Override
+            public void addListener(MapEventListener<K, V> listener, Executor executor) {
+                listeners.put(listener, executor);
+            }
+
+            @Override
+            public void removeListener(MapEventListener<K, V> listener) {
+                listeners.remove(listener);
+            }
+        }
+
+        // Mock DistributedSet that behaves as a HashSet
+        class TestDistributedSet<E> extends HashSet<E> implements DistributedSet<E> {
+
+            @Override
+            public void addListener(SetEventListener<E> listener) {
+            }
+
+            @Override
+            public void removeListener(SetEventListener<E> listener) {
+            }
+
+            @Override
+            public String name() {
+                return null;
+            }
+
+            @Override
+            public Type primitiveType() {
+                return null;
+            }
+        }
+    }
+
+    private static class MockAtomicCounter implements AtomicCounter {
+        long id = 0;
+
+        @Override
+        public long incrementAndGet() {
+            return ++id;
+        }
+
+        @Override
+        public long getAndIncrement() {
+            return id++;
+        }
+
+        @Override
+        public long getAndAdd(long delta) {
+            long oldId = id;
+            id += delta;
+            return oldId;
+        }
+
+        @Override
+        public long addAndGet(long delta) {
+            id += delta;
+            return id;
+        }
+
+        @Override
+        public void set(long value) {
+            id = value;
+        }
+
+        @Override
+        public boolean compareAndSet(long expectedValue, long updateValue) {
+            if (id == expectedValue) {
+                id = updateValue;
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public long get() {
+            return id;
+        }
+
+        @Override
+        public String name() {
+            return "MockAtomicCounter";
+        }
+    }
+
+    /**
+     * Mocks the packet service of ONOS so that the application under test can
+     * observe network packets.
+     */
+    public static class MockPacketService extends PacketServiceAdapter {
+        Set<PacketProcessor> packetProcessors = Sets.newHashSet();
+        OutboundPacket emittedPacket;
+
+        @Override
+        public void addProcessor(PacketProcessor processor, int priority) {
+            packetProcessors.add(processor);
+        }
+
+        public void processPacket(PacketContext packetContext) {
+            packetProcessors.forEach(p -> p.process(packetContext));
+        }
+
+        @Override
+        public void emit(OutboundPacket packet) {
+            this.emittedPacket = packet;
+        }
+    }
+
+    /**
+     * Implements event delivery system that delivers events synchronously, or
+     * in-line with the post method invocation.
+     */
+    public static class MockEventDispatcher extends DefaultEventSinkRegistry
+            implements EventDeliveryService {
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public synchronized void post(Event event) {
+            EventSink sink = getSink(event.getClass());
+            checkState(sink != null, "No sink for event %s", event);
+            sink.process(event);
+        }
+
+        @Override
+        public void setDispatchTimeLimit(long millis) {
+        }
+
+        @Override
+        public long getDispatchTimeLimit() {
+            return 0;
+        }
+    }
+
+    public static void injectEventDispatcher(Object manager, EventDeliveryService svc) {
+        Class mc = manager.getClass();
+        Field[] var3 = mc.getSuperclass().getDeclaredFields();
+
+        for (Field f : var3) {
+            if (f.getType().equals(EventDeliveryService.class)) {
+                try {
+                    TestUtils.setField(manager, f.getName(), svc);
+                    break;
+                } catch (TestUtils.TestUtilsException var8) {
+                    throw new IllegalArgumentException("Unable to inject reference", var8);
+                }
+            }
+        }
+    }
+
+    /**
+     * Generates DHCP REQUEST packet.
+     */
+    protected static class TestDhcpRequestPacketContext extends PacketContextAdapter {
+
+        private InboundPacket inPacket;
+
+        public TestDhcpRequestPacketContext(MacAddress clientMac, VlanId vlanId,
+                                            VlanId qinqQVid,
+                                            ConnectPoint clientCp) {
+            super(0, null, null, false);
+            byte[] dhcpMsgType = new byte[1];
+            dhcpMsgType[0] = (byte) DHCP.MsgType.DHCPREQUEST.getValue();
+
+            DhcpOption dhcpOption = new DhcpOption();
+            dhcpOption.setCode(DHCP.DHCPOptionCode.OptionCode_MessageType.getValue());
+            dhcpOption.setData(dhcpMsgType);
+            dhcpOption.setLength((byte) 1);
+            DhcpOption endOption = new DhcpOption();
+            endOption.setCode(DHCP.DHCPOptionCode.OptionCode_END.getValue());
+
+            DHCP dhcp = new DHCP();
+            dhcp.setHardwareType(DHCP.HWTYPE_ETHERNET);
+            dhcp.setHardwareAddressLength((byte) 6);
+            dhcp.setClientHardwareAddress(clientMac.toBytes());
+            dhcp.setOptions(ImmutableList.of(dhcpOption, endOption));
+
+
+            UDP udp = new UDP();
+            udp.setPayload(dhcp);
+            udp.setSourcePort(UDP.DHCP_CLIENT_PORT);
+            udp.setDestinationPort(UDP.DHCP_SERVER_PORT);
+
+            IPv4 ipv4 = new IPv4();
+            ipv4.setPayload(udp);
+            ipv4.setDestinationAddress(SERVER_IP.toInt());
+            ipv4.setSourceAddress(INTERFACE_IP.toInt());
+
+            Ethernet eth = new Ethernet();
+            eth.setEtherType(Ethernet.TYPE_IPV4)
+                    .setVlanID(vlanId.toShort())
+                    .setQinQVID(qinqQVid.toShort())
+                    .setSourceMACAddress(clientMac)
+                    .setDestinationMACAddress(MacAddress.BROADCAST)
+                    .setPayload(ipv4);
+
+            this.inPacket = new DefaultInboundPacket(clientCp, eth,
+                    ByteBuffer.wrap(eth.serialize()));
+        }
+
+        @Override
+        public InboundPacket inPacket() {
+            return this.inPacket;
+        }
+    }
+
+}
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..87462a3
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2017-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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.onosproject</groupId>
+        <artifactId>onos-dependencies</artifactId>
+        <version>2.2.2</version>
+        <relativePath></relativePath>
+    </parent>
+
+    <groupId>org.opencord</groupId>
+    <artifactId>maclearner</artifactId>
+    <packaging>pom</packaging>
+    <version>1.0.0-SNAPSHOT</version>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <onos.version>2.2.2</onos.version>
+        <onos.app.name>org.opencord.maclearner</onos.app.name>
+        <onos.app.category>Utility</onos.app.category>
+        <onos.app.title>Mac Address by Connected Port Information Service</onos.app.title>
+        <onos.app.origin>ON.Lab</onos.app.origin>
+        <onos.app.url>http://onosproject.org</onos.app.url>
+    </properties>
+
+    <modules>
+        <module>api</module>
+        <module>app</module>
+    </modules>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-api</artifactId>
+            <version>${onos.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.karaf.shell</groupId>
+            <artifactId>org.apache.karaf.shell.console</artifactId>
+        </dependency>
+    </dependencies>
+
+    <repositories>
+        <repository>
+            <id>central</id>
+            <name>Central Repository</name>
+            <url>https://repo.maven.apache.org/maven2</url>
+            <layout>default</layout>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+            <releases>
+                <enabled>true</enabled>
+                <updatePolicy>always</updatePolicy>
+                <checksumPolicy>fail</checksumPolicy>
+            </releases>
+        </repository>
+        <repository>
+            <id>snapshots</id>
+            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
+            <snapshots>
+                <enabled>true</enabled>
+                <updatePolicy>always</updatePolicy>
+                <checksumPolicy>fail</checksumPolicy>
+            </snapshots>
+        </repository>
+    </repositories>
+
+</project>