initial code for the mac-learning app

Change-Id: I04f3b17c453e502f20477e83cb7cdc796b4160ae
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;