First implementation of BNG application

The application offers network level APIs for the BNG data plane.

An attachment (BngAttachment) at network level is identified by an arbitrary string.
The exposed APIs are key-value like APIs.

Change-Id: If0e484f487ea16dd8c7dd99642f75686e1dbc29a
diff --git a/app/src/main/java/org/opencord/bng/cli/AddAttachment.java b/app/src/main/java/org/opencord/bng/cli/AddAttachment.java
new file mode 100644
index 0000000..bea0c1f
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/AddAttachment.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.opencord.bng.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.Option;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.cli.net.DeviceIdCompleter;
+import org.onosproject.cli.net.PortNumberCompleter;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.ConnectPoint;
+import org.opencord.bng.BngAttachment;
+import org.opencord.bng.BngManager;
+import org.opencord.bng.BngService;
+import org.opencord.bng.BngUtils;
+import org.opencord.bng.PppoeBngAttachment;
+
+@Service
+@Command(scope = "bng", name = "attachment-add",
+        description = "Add an attachment on the BNG in disabled state")
+public class AddAttachment extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "macAddress", description = "Mac Address of the attachment", required = true)
+    String macAddressString = null;
+
+    @Argument(index = 1, name = "ipAddress", description = "IP Address of the attachment", required = true)
+    String ipAddressString = null;
+
+    @Argument(index = 2, name = "cTag", description = "VLAN C-TAG of the attachment", required = true)
+    short cTag = 0;
+
+    @Argument(index = 3, name = "sTag", description = "VLAN S-TAG of the attachment", required = true)
+    short sTag = 0;
+
+    @Argument(index = 4, name = "pppoeSessionId",
+            description = "PPPoE session ID of the attachment", required = true)
+    short pppoeSessionId = 0;
+
+    @Argument(index = 5, name = "oltDeviceID",
+            description = "OLT device ID the attachment is connected to", required = true)
+    @Completion(DeviceIdCompleter.class)
+    String oltDeviceId = null;
+
+    @Argument(index = 6, name = "portNumber",
+            description = "Port number on the OLT device", required = true)
+    @Completion(PortNumberCompleter.class)
+    String oltPortNumber = null;
+
+    @Argument(index = 7, name = "onuSerial",
+            description = "Serial number for the ONU of the attachment", required = true)
+    @Completion(OnuSerialCompleter.class)
+    String onuSerial = null;
+
+    @Option(name = "-d", aliases = "--disable", description = "Disable the specified attachment",
+            required = false, multiValued = false)
+    boolean disable = false;
+
+
+    @Override
+    protected void doExecute() throws Exception {
+        CoreService coreService = get(CoreService.class);
+
+        ApplicationId appId = coreService.getAppId(BngManager.BNG_APP);
+        ConnectPoint uniCp = ConnectPoint.fromString(oltDeviceId + "/" + oltPortNumber);
+        MacAddress macAddress = MacAddress.valueOf(macAddressString);
+        IpAddress ipAddress = IpAddress.valueOf(ipAddressString);
+
+        String attachmentKey = "CLI" + "/" +
+                BngUtils.calculateBngAttachmentKey(onuSerial, VlanId.vlanId(cTag),
+                                                   VlanId.vlanId(sTag), uniCp, ipAddress,
+                                                   macAddress);
+
+        BngAttachment newAttachment = PppoeBngAttachment.builder()
+                .withPppoeSessionId(pppoeSessionId)
+                .withApplicationId(appId)
+                .withMacAddress(macAddress)
+                .withCTag(VlanId.vlanId(cTag))
+                .withSTag(VlanId.vlanId(sTag))
+                .withIpAddress(ipAddress)
+                .withOltConnectPoint(uniCp)
+                .withOnuSerial(onuSerial)
+                .lineActivated(!disable)
+                .build();
+        BngService bngService = get(BngService.class);
+        bngService.setupAttachment(attachmentKey, newAttachment);
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/cli/AttachmentKeyCompleter.java b/app/src/main/java/org/opencord/bng/cli/AttachmentKeyCompleter.java
new file mode 100644
index 0000000..4a6cdff
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/AttachmentKeyCompleter.java
@@ -0,0 +1,46 @@
+/*
+ * 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.bng.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.bng.BngService;
+
+import java.util.List;
+import java.util.SortedSet;
+
+/**
+ * Attachment Key completer.
+ */
+@Service
+public class AttachmentKeyCompleter implements Completer {
+
+    @Override
+    public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+        // Delegate string completer
+        StringsCompleter delegate = new StringsCompleter();
+
+        BngService service = AbstractShellCommand.get(BngService.class);
+        SortedSet<String> strings = delegate.getStrings();
+        service.getAttachments().keySet().forEach(strings::add);
+        // Now let the completer do the work for figuring out what to offer.
+        return delegate.complete(session, commandLine, candidates);
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/opencord/bng/cli/AttachmentStats.java b/app/src/main/java/org/opencord/bng/cli/AttachmentStats.java
new file mode 100644
index 0000000..346f554
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/AttachmentStats.java
@@ -0,0 +1,69 @@
+/*
+ * 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.bng.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.opencord.bng.BngAttachment;
+import org.opencord.bng.BngService;
+import org.opencord.bng.BngStatsService;
+
+import static java.util.Map.Entry.comparingByKey;
+
+@Service
+@Command(scope = "bng", name = "attachment-stats",
+        description = "Get the stats (registers) of the attachments")
+public class AttachmentStats extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "attachmentKey", description = "Attachment Key. No ID or 0 means ALL")
+    @Completion(AttachmentKeyCompleter.class)
+    String attachmentKey = null;
+
+    @Override
+    protected void doExecute() throws Exception {
+        BngService bngService = AbstractShellCommand.get(BngService.class);
+
+        print("STATISTICS");
+        if (attachmentKey == null) {
+            // Print the statistics for all the registered attachments
+            bngService.getAttachments().forEach(this::printAttachmentStats);
+        } else {
+            printAttachmentStats(attachmentKey, bngService.getAttachment(attachmentKey));
+        }
+    }
+
+    private void printAttachmentStats(String attachmentKey, BngAttachment attachment) {
+        if (attachment != null) {
+            BngStatsService bngStatsService = AbstractShellCommand.get(BngStatsService.class);
+            print("MAC: " + attachment.macAddress().toString()
+                          + "\nC_TAG: " + attachment.cTag().toShort()
+                          + "\nS_TAG: " + attachment.sTag().toString()
+                          + "\nIP: " + attachment.ipAddress());
+            bngStatsService.getStats(attachmentKey).entrySet().stream().sorted(comparingByKey())
+                    .forEach(
+                            (entry) -> {
+                                print(BngCliUtils.niceCounterName(entry.getKey()));
+                                print("\tPackets:" + entry.getValue().packets());
+                                print("\tBytes:\t" + entry.getValue().bytes());
+                            }
+                    );
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/opencord/bng/cli/Attachments.java b/app/src/main/java/org/opencord/bng/cli/Attachments.java
new file mode 100644
index 0000000..52203d4
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/Attachments.java
@@ -0,0 +1,36 @@
+/*
+ * 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.bng.cli;
+
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractShellCommand;
+import org.opencord.bng.BngService;
+
+@Service
+@Command(scope = "bng", name = "attachments",
+        description = "Get the list of registered attachment")
+public class Attachments extends AbstractShellCommand {
+
+    @Override
+    protected void doExecute() throws Exception {
+        BngService bngService = AbstractShellCommand.get(BngService.class);
+        var attachments = bngService.getAttachments();
+        print("Registered attachments (size: " + attachments.size() + "):");
+        print(attachments.toString());
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/cli/BngCliUtils.java b/app/src/main/java/org/opencord/bng/cli/BngCliUtils.java
new file mode 100644
index 0000000..3776117
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/BngCliUtils.java
@@ -0,0 +1,51 @@
+/*
+ * 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.bng.cli;
+
+import org.onosproject.net.behaviour.BngProgrammable.BngCounterType;
+
+/**
+ * Utilities to print counter names in CLI output.
+ */
+public final class BngCliUtils {
+
+    private BngCliUtils() {
+    }
+
+    /**
+     * Prints a nicer counter name on CLI output.
+     *
+     * @param counterName The counter type to be converted
+     * @return The string representing the counter
+     */
+    static String niceCounterName(BngCounterType counterName) {
+        switch (counterName) {
+            case CONTROL_PLANE:
+                return "Upstream Control";
+            case DOWNSTREAM_RX:
+                return "Downstream Received";
+            case DOWNSTREAM_TX:
+                return "Downstream Transmitted";
+            case UPSTREAM_DROPPED:
+                return "Upstream Dropped";
+            case UPSTREAM_TX:
+                return "Upstream Terminated";
+            default:
+                return "UNKNOWN";
+        }
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/cli/ControlPacketsStats.java b/app/src/main/java/org/opencord/bng/cli/ControlPacketsStats.java
new file mode 100644
index 0000000..a552fc4
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/ControlPacketsStats.java
@@ -0,0 +1,41 @@
+/*
+ * 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.bng.cli;
+
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.pi.runtime.PiCounterCellData;
+import org.opencord.bng.BngStatsService;
+
+@Service
+@Command(scope = "bng", name = "control-stats",
+        description = "Retrieve statistics of control plane packets of un-registered attachments")
+public class ControlPacketsStats extends AbstractShellCommand {
+
+    @Override
+    protected void doExecute() throws Exception {
+        BngStatsService bngStatsService = AbstractShellCommand.get(BngStatsService.class);
+        PiCounterCellData stats = bngStatsService.getControlStats();
+        if (stats != null) {
+            print("Packets: " + stats.packets());
+            print("Bytes:\t" + stats.bytes());
+        } else {
+            print("No BNG user plane device configured");
+        }
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/cli/EnableAttachment.java b/app/src/main/java/org/opencord/bng/cli/EnableAttachment.java
new file mode 100644
index 0000000..d209c76
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/EnableAttachment.java
@@ -0,0 +1,76 @@
+/*
+ * 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.bng.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.Option;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.behaviour.BngProgrammable;
+import org.opencord.bng.BngAttachment;
+import org.opencord.bng.BngService;
+import org.opencord.bng.PppoeBngAttachment;
+
+@Service
+@Command(scope = "bng", name = "attachment-enable",
+        description = "Enable/Disable an attachment")
+public class EnableAttachment extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "attachmentKey", description = "Attachment Key", required = true)
+    @Completion(AttachmentKeyCompleter.class)
+    String attachmentKey = null;
+
+    @Option(name = "-d", aliases = "--disable", description = "Disable the specified attachment",
+            required = false, multiValued = false)
+    boolean disable = false;
+
+    @Override
+    protected void doExecute() throws Exception {
+        BngService bngService = AbstractShellCommand.get(BngService.class);
+
+        BngAttachment attachment = bngService.getAttachment(attachmentKey);
+
+        if (attachment == null) {
+            print("Attachment " + attachmentKey.toString() + "not found!");
+            return;
+        }
+        if (attachment.lineActive() == !disable) {
+            print("Attachment is already " + (disable ? "disabled" : "enabled"));
+            return;
+        }
+        if (!attachment.type().equals(BngProgrammable.Attachment.AttachmentType.PPPoE)) {
+            print((disable ? "Disable" : "Enable") + " supported only for PPPoE attachment");
+            return;
+        }
+
+        BngAttachment newAttachment = PppoeBngAttachment.builder()
+                .withPppoeSessionId(attachment.pppoeSessionId())
+                .withApplicationId(attachment.appId())
+                .withMacAddress(attachment.macAddress())
+                .withCTag(attachment.cTag())
+                .withSTag(attachment.sTag())
+                .withIpAddress(attachment.ipAddress())
+                .withOltConnectPoint(attachment.oltConnectPoint())
+                .withOnuSerial(attachment.onuSerial())
+                .lineActivated(!disable)
+                .build();
+        print(disable ? "Disabling" : "Enabling" + " attachment: " + newAttachment.toString());
+        bngService.setupAttachment(attachmentKey, newAttachment);
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/opencord/bng/cli/OnuSerialCompleter.java b/app/src/main/java/org/opencord/bng/cli/OnuSerialCompleter.java
new file mode 100644
index 0000000..6f87a3d
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/OnuSerialCompleter.java
@@ -0,0 +1,53 @@
+/*
+ * 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.bng.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.onosproject.net.device.DeviceService;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * ONU serial number completer.
+ */
+@Service
+public class OnuSerialCompleter implements Completer {
+
+    @Override
+    public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+        // Delegate string completer
+        StringsCompleter delegate = new StringsCompleter();
+
+        DeviceService deviceService = AbstractShellCommand.get(DeviceService.class);
+
+        Set<String> candidateOnuSerials = delegate.getStrings();
+        deviceService.getDevices()
+                .forEach(device -> deviceService.getPorts(
+                        device.id()).stream()
+                        .map(port -> port.annotations().value("portName"))
+                        .forEach(candidateOnuSerials::add)
+                );
+        // Now let the completer do the work for figuring out what to offer.
+        return delegate.complete(session, commandLine, candidates);
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/opencord/bng/cli/RemoveAttachment.java b/app/src/main/java/org/opencord/bng/cli/RemoveAttachment.java
new file mode 100644
index 0000000..00f0527
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/RemoveAttachment.java
@@ -0,0 +1,46 @@
+/*
+ * 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.bng.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.opencord.bng.BngService;
+
+@Service
+@Command(scope = "bng", name = "attachment-remove",
+        description = "Remove an attachment from the BNG")
+public class RemoveAttachment extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "attachmentKey", description = "Attachment Key. No Key or 0 means ALL")
+    @Completion(AttachmentKeyCompleter.class)
+    String attachmentKey = null;
+
+    @Override
+    protected void doExecute() throws Exception {
+        BngService bngService = AbstractShellCommand.get(BngService.class);
+        if (attachmentKey == null) {
+            bngService.getAttachments().keySet()
+                    .forEach(bngService::removeAttachment);
+            return;
+        }
+        bngService.removeAttachment(attachmentKey);
+        print("Attachment removed");
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/cli/package-info.java b/app/src/main/java/org/opencord/bng/cli/package-info.java
new file mode 100644
index 0000000..68bbf80
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * BNG CLI package.
+ */
+package org.opencord.bng.cli;
\ No newline at end of file