Support import tag in jiri manifest

Change-Id: I73b0d87300824698d9f96853956d8b4c40a46387
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriManifest.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriManifest.java
index 3c68ba1..3b41f1c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriManifest.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriManifest.java
@@ -17,6 +17,7 @@
 import javax.xml.bind.annotation.XmlAttribute;
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlRootElement;
+import org.apache.commons.lang.StringUtils;
 
 /** Refer https://fuchsia.googlesource.com/jiri/+/HEAD/manifest.md for manifest specification. */
 @XmlRootElement(name = "manifest")
@@ -54,7 +55,63 @@
   }
 
   static class Import {
-    // Don't need all fields, as we don't use it
+    @XmlAttribute(required = true)
+    String manifest;
+
+    @XmlAttribute(required = true)
+    String name;
+
+    @XmlAttribute(required = true)
+    String remote;
+
+    @XmlAttribute
+    String revision;
+
+    @XmlAttribute
+    String remotebranch;
+
+    public Import() {
+      manifest = "";
+      name = "";
+      remote = "";
+      revision = "";
+      remotebranch = "";
+    }
+
+    public void fillDefault() {
+      if (remotebranch.isEmpty()) {
+        remotebranch = "master";
+      }
+    }
+
+    public String Key() {
+      return name + "=" + StringUtils.strip(remote, "/");
+    }
+
+    /** @return the manifest */
+    public String getManifest() {
+      return manifest;
+    }
+
+    /** @return the name */
+    public String getName() {
+      return name;
+    }
+
+    /** @return the remote */
+    public String getRemote() {
+      return remote;
+    }
+
+    /** @return the revision */
+    public String getRevision() {
+      return revision;
+    }
+
+    /** @return the remotebranch */
+    public String getRemotebranch() {
+      return remotebranch;
+    }
   }
 
   static class Imports {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriManifestParser.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriManifestParser.java
index 0f80895..55cc503 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriManifestParser.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriManifestParser.java
@@ -14,6 +14,10 @@
 
 package com.googlesource.gerrit.plugins.supermanifest;
 
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_HEADS;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.googlesource.gerrit.plugins.supermanifest.SuperManifestRefUpdatedListener.GerritRemoteReader;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.net.URI;
@@ -28,61 +32,140 @@
 import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.XMLStreamReader;
 import javax.xml.transform.stream.StreamSource;
+import org.apache.commons.lang.StringUtils;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Repository;
 
 class JiriManifestParser {
-  public static JiriProjects getProjects(Repository repo, String ref, String manifest)
-      throws ConfigInvalidException, IOException {
-    Queue<String> q = new LinkedList<>();
-    q.add(manifest);
-    HashSet<String> processedFiles = new HashSet<>();
-    HashMap<String, JiriProjects.Project> projectMap = new HashMap<>();
+  static class ManifestItem {
+    public ManifestItem(
+        String repoKey, String manifest, String ref, String pKey, boolean revisionPinned) {
+      this.repoKey = repoKey;
+      this.manifest = manifest;
+      this.ref = ref;
+      this.revisionPinned = revisionPinned;
+      this.projectKey = pKey;
+    }
 
-    while (q.size() != 0) {
-      String file = q.remove();
-      if (processedFiles.contains(file)) {
-        continue;
-      }
-      processedFiles.add(file);
-      JiriManifest m;
-      try {
-        m = parseManifest(repo, ref, file);
-      } catch (JAXBException | XMLStreamException e) {
-        throw new ConfigInvalidException("XML parse error", e);
-      }
-      if (m.imports.getImports().length != 0) {
-        throw new ConfigInvalidException(
-            String.format("Manifest %s contains remote imports which are not supported", file));
-      }
+    String repoKey;
+    String manifest;
+    String ref;
+    boolean revisionPinned;
 
-      for (JiriProjects.Project project : m.projects.getProjects()) {
-        project.fillDefault();
-        if (projectMap.containsKey(project.Key())) {
-          if (!projectMap.get(project.Key()).equals(project))
-            throw new ConfigInvalidException(
-                String.format(
-                    "Duplicate conflicting project %s in manifest %s\n%s\n%s",
-                    project.Key(),
-                    file,
-                    project.toString(),
-                    projectMap.get(project.Key()).toString()));
-        } else {
-          projectMap.put(project.Key(), project);
-        }
-      }
+    // In jiri if import is pinned to a revision and if
+    // we have a corresponding project in the manifest, jiri would
+    // pin that project to same revision. So passing key to match
+    // project to import tag.
+    //  For Eg, if you have manifest in manifest2 repo
+    //          <manifest><projects><project name="manifest2" .../>
+    //  And If you import this from your main manifest
+    //          <manifest><imports><import name="manifest2" revision="A"... />
+    //  jiri will pin manifest2 project to A as well.
+    String projectKey;
+  }
 
-      URI parentURI;
-      try {
-        parentURI = new URI(file);
-      } catch (URISyntaxException e) {
-        throw new ConfigInvalidException("Invalid parent URI", e);
-      }
-      for (JiriManifest.LocalImport l : m.imports.getLocalImports()) {
-        q.add(parentURI.resolve(l.getFile()).getPath());
+  static class RepoMap<K, V extends Repository> extends HashMap<K, V> implements AutoCloseable {
+    @Override
+    public void close() {
+      for (Repository repo : this.values()) {
+          repo.close();
       }
     }
-    return new JiriProjects(projectMap.values().toArray(new JiriProjects.Project[0]));
+  }
+
+  public static JiriProjects getProjects(
+      GerritRemoteReader reader, String repoKey, String ref, String manifest)
+      throws ConfigInvalidException, IOException {
+
+    try (RepoMap<String, Repository> repoMap = new RepoMap<>()) {
+      repoMap.put(repoKey, reader.openRepository(repoKey));
+      Queue<ManifestItem> q = new LinkedList<>();
+      q.add(new ManifestItem(repoKey, manifest, ref, "", false));
+      HashMap<String, HashSet<String>> processedRepoFiles = new HashMap<>();
+      HashMap<String, JiriProjects.Project> projectMap = new HashMap<>();
+
+      while (q.size() != 0) {
+        ManifestItem mi = q.remove();
+        Repository repo = repoMap.get(mi.repoKey);
+        if (repo == null) {
+          repo = reader.openRepository(mi.repoKey);
+          repoMap.put(mi.repoKey, repo);
+        }
+        HashSet<String> processedFiles = processedRepoFiles.get(mi.repoKey);
+        if (processedFiles == null) {
+          processedFiles = new HashSet<String>();
+          processedRepoFiles.put(mi.repoKey, processedFiles);
+        }
+        if (processedFiles.contains(mi.manifest)) {
+          continue;
+        }
+        processedFiles.add(mi.manifest);
+        JiriManifest m;
+        try {
+          m = parseManifest(repo, mi.ref, mi.manifest);
+        } catch (JAXBException | XMLStreamException e) {
+          throw new ConfigInvalidException("XML parse error", e);
+        }
+
+        for (JiriProjects.Project project : m.projects.getProjects()) {
+          project.fillDefault();
+          if (mi.revisionPinned && project.Key().equals(mi.projectKey)) {
+            project.setRevision(mi.ref);
+          }
+          if (projectMap.containsKey(project.Key())) {
+            if (!projectMap.get(project.Key()).equals(project))
+              throw new ConfigInvalidException(
+                  String.format(
+                      "Duplicate conflicting project %s in manifest %s\n%s\n%s",
+                      project.Key(),
+                      mi.manifest,
+                      project.toString(),
+                      projectMap.get(project.Key()).toString()));
+          } else {
+            projectMap.put(project.Key(), project);
+          }
+        }
+
+        URI parentURI;
+        try {
+          parentURI = new URI(mi.manifest);
+        } catch (URISyntaxException e) {
+          throw new ConfigInvalidException("Invalid parent URI", e);
+        }
+        for (JiriManifest.LocalImport l : m.imports.getLocalImports()) {
+          ManifestItem tw =
+              new ManifestItem(
+                  mi.repoKey,
+                  parentURI.resolve(l.getFile()).getPath(),
+                  mi.ref,
+                  mi.projectKey,
+                  mi.revisionPinned);
+          q.add(tw);
+        }
+
+        for (JiriManifest.Import i : m.imports.getImports()) {
+          i.fillDefault();
+          URI uri;
+          try {
+            uri = new URI(i.getRemote());
+          } catch (URISyntaxException e) {
+            throw new ConfigInvalidException("Invalid URI", e);
+          }
+          String iRepoKey = new Project.NameKey(StringUtils.strip(uri.getPath(), "/")).toString();
+          String iRef = i.getRevision();
+          boolean revisionPinned = true;
+          if (iRef.isEmpty()) {
+            iRef = REFS_HEADS + i.getRemotebranch();
+            revisionPinned = false;
+          }
+
+          ManifestItem tmi =
+              new ManifestItem(iRepoKey, i.getManifest(), iRef, i.Key(), revisionPinned);
+          q.add(tmi);
+        }
+      }
+      return new JiriProjects(projectMap.values().toArray(new JiriProjects.Project[0]));
+    }
   }
 
   private static JiriManifest parseManifest(Repository repo, String ref, String file)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriProjects.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriProjects.java
index 271e52e..31913b2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriProjects.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriProjects.java
@@ -18,6 +18,7 @@
 import java.util.Comparator;
 import javax.xml.bind.annotation.XmlAttribute;
 import javax.xml.bind.annotation.XmlElement;
+import org.apache.commons.lang.StringUtils;
 
 class JiriProjects {
   @XmlElement(name = "project")
@@ -172,7 +173,12 @@
     }
 
     public String Key() {
-      return name + "=" + remote;
+      return name + "=" + StringUtils.strip(remote, "/");
+    }
+
+    /** @param revision the revision to set */
+    public void setRevision(String revision) {
+      this.revision = revision;
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriUpdater.java b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriUpdater.java
index 150fc59..264496f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriUpdater.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/supermanifest/JiriUpdater.java
@@ -249,11 +249,13 @@
   @Override
   public void update(GerritRemoteReader reader, ConfigEntry c, String srcRef)
       throws IOException, GitAPIException, ConfigInvalidException {
-    Repository srcRepo = reader.openRepository(c.getSrcRepoKey().toString());
-    Repository destRepo = reader.openRepository(c.getDestRepoKey().toString());
-    JiriProjects projects = JiriManifestParser.getProjects(srcRepo, srcRef, c.getXmlPath());
-    String targetRef = c.getDestBranch().equals("*") ? srcRef : REFS_HEADS + c.getDestBranch();
-    updateSubmodules(
-        destRepo, targetRef, URI.create(c.getDestRepoKey().toString() + "/"), projects, reader);
+    try (Repository destRepo = reader.openRepository(c.getDestRepoKey().toString())) {
+      JiriProjects projects =
+          JiriManifestParser.getProjects(
+              reader, c.getSrcRepoKey().toString(), srcRef, c.getXmlPath());
+      String targetRef = c.getDestBranch().equals("*") ? srcRef : REFS_HEADS + c.getDestBranch();
+      updateSubmodules(
+          destRepo, targetRef, URI.create(c.getDestRepoKey().toString() + "/"), projects, reader);
+    }
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/supermanifest/JiriSuperManifestIT.java b/src/test/java/com/googlesource/gerrit/plugins/supermanifest/JiriSuperManifestIT.java
index a8b3427..f6f7a55 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/supermanifest/JiriSuperManifestIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/supermanifest/JiriSuperManifestIT.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -32,6 +33,7 @@
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.BlobBasedConfig;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
 
 @TestPlugin(
@@ -172,6 +174,275 @@
     assertThat(branch.file("project3").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
   }
 
+  @Test
+  public void ImportTagWorks() throws Exception {
+    setupTestRepos("project");
+
+    // Make sure the manifest exists so the configuration loads successfully.
+    NameKey manifest1Key = createProject("manifest1");
+    TestRepository<InMemoryRepository> manifest1Repo = cloneProject(manifest1Key, admin);
+
+    NameKey manifest2Key = createProject("manifest2");
+    TestRepository<InMemoryRepository> manifest2Repo = cloneProject(manifest2Key, admin);
+
+    NameKey superKey = createProject("superproject");
+    cloneProject(superKey, admin);
+
+    pushConfig(
+        "[superproject \""
+            + superKey.get()
+            + ":refs/heads/destbranch\"]\n"
+            + "  srcRepo = "
+            + manifest1Key.get()
+            + "\n"
+            + "  srcRef = refs/heads/srcbranch\n"
+            + "  srcPath = default\n"
+            + "  toolType = jiri\n");
+
+    // XML change will trigger commit to superproject.
+    String xml1 =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+            + "<manifest>\n<imports>\n"
+            + "<import name=\""
+            + manifest2Key.get()
+            + "\" manifest=\"default\" remote=\""
+            + canonicalWebUrl.get()
+            + manifest2Key.get()
+            + "\" />\n</imports>"
+            + "<projects>\n"
+            + "<project name=\""
+            + testRepoKeys[0].get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + testRepoKeys[0].get()
+            + "\" path=\"project1\" />\n"
+            + "</projects>\n</manifest>\n";
+
+    String xml2 =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+            + "<manifest>\n<projects>\n"
+            + "<project name=\""
+            + manifest2Key.get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + manifest2Key.get()
+            + "\" path=\"manifest2\" />\n"
+            + "</projects>\n</manifest>\n";
+    pushFactory
+        .create(db, admin.getIdent(), manifest2Repo, "Subject", "default", xml2)
+        .to("refs/heads/master")
+        .assertOkStatus();
+    pushFactory
+        .create(db, admin.getIdent(), manifest1Repo, "Subject", "default", xml1)
+        .to("refs/heads/srcbranch")
+        .assertOkStatus();
+
+    BranchApi branch = gApi.projects().name(superKey.get()).branch("refs/heads/destbranch");
+    assertThat(branch.file("project1").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
+    assertThat(branch.file("manifest2").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
+  }
+
+  @Test
+  public void ImportTagWithRevisionWorks() throws Exception {
+    setupTestRepos("project");
+
+    // Make sure the manifest exists so the configuration loads successfully.
+    NameKey manifest1Key = createProject("manifest1");
+    TestRepository<InMemoryRepository> manifest1Repo = cloneProject(manifest1Key, admin);
+
+    NameKey manifest2Key = createProject("manifest2");
+    TestRepository<InMemoryRepository> manifest2Repo = cloneProject(manifest2Key, admin);
+
+    NameKey superKey = createProject("superproject");
+    cloneProject(superKey, admin);
+
+    pushConfig(
+        "[superproject \""
+            + superKey.get()
+            + ":refs/heads/destbranch\"]\n"
+            + "  srcRepo = "
+            + manifest1Key.get()
+            + "\n"
+            + "  srcRef = refs/heads/srcbranch\n"
+            + "  srcPath = default\n"
+            + "  toolType = jiri\n");
+
+    String xml2 =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+            + "<manifest>\n<projects>\n"
+            + "<project name=\""
+            + manifest2Key.get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + manifest2Key.get()
+            + "\" path=\"manifest2\" />\n"
+            + "</projects>\n</manifest>\n";
+    Result c =
+        pushFactory
+            .create(db, admin.getIdent(), manifest2Repo, "Subject", "default", xml2)
+            .to("refs/heads/master");
+    c.assertOkStatus();
+    RevCommit commit = c.getCommit();
+
+    // Add new project, that should not be imported
+    xml2 =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+            + "<manifest>\n<projects>\n"
+            + "<project name=\""
+            + manifest2Key.get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + manifest2Key.get()
+            + "\" path=\"manifest2\" />\n"
+            + "<project name=\""
+            + testRepoKeys[1].get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + testRepoKeys[1].get()
+            + "\" path=\"project2\" />\n"
+            + "</projects>\n</manifest>\n";
+
+    pushFactory
+        .create(db, admin.getIdent(), manifest2Repo, "Subject", "default", xml2)
+        .to("refs/heads/master")
+        .assertOkStatus();
+
+    // XML change will trigger commit to superproject.
+    String xml1 =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+            + "<manifest>\n<imports>\n"
+            + "<import name=\""
+            + manifest2Key.get()
+            + "\" manifest=\"default\" remote=\""
+            + canonicalWebUrl.get()
+            + manifest2Key.get()
+            + "\" revision=\""
+            + commit.name()
+            + "\"/>\n</imports>"
+            + "<projects>\n"
+            + "<project name=\""
+            + testRepoKeys[0].get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + testRepoKeys[0].get()
+            + "\" path=\"project1\" />\n"
+            + "</projects>\n</manifest>\n";
+
+    pushFactory
+        .create(db, admin.getIdent(), manifest1Repo, "Subject", "default", xml1)
+        .to("refs/heads/srcbranch")
+        .assertOkStatus();
+
+    BranchApi branch = gApi.projects().name(superKey.get()).branch("refs/heads/destbranch");
+    assertThat(branch.file("project1").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
+    assertThat(branch.file("manifest2").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
+    assertThat(branch.file("manifest2").asString()).contains(commit.name());
+    try {
+      branch.file("project2");
+      fail("wanted exception");
+    } catch (ResourceNotFoundException e) {
+      // all fine.
+    }
+  }
+
+  @Test
+  public void ImportTagWithRemoteBranchWorks() throws Exception {
+    setupTestRepos("project");
+
+    // Make sure the manifest exists so the configuration loads successfully.
+    NameKey manifest1Key = createProject("manifest1");
+    TestRepository<InMemoryRepository> manifest1Repo = cloneProject(manifest1Key, admin);
+
+    NameKey manifest2Key = createProject("manifest2");
+    TestRepository<InMemoryRepository> manifest2Repo = cloneProject(manifest2Key, admin);
+
+    NameKey superKey = createProject("superproject");
+    cloneProject(superKey, admin);
+
+    pushConfig(
+        "[superproject \""
+            + superKey.get()
+            + ":refs/heads/destbranch\"]\n"
+            + "  srcRepo = "
+            + manifest1Key.get()
+            + "\n"
+            + "  srcRef = refs/heads/srcbranch\n"
+            + "  srcPath = default\n"
+            + "  toolType = jiri\n");
+
+    String xml2 =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+            + "<manifest>\n<projects>\n"
+            + "<project name=\""
+            + manifest2Key.get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + manifest2Key.get()
+            + "\" path=\"manifest2\" />\n"
+            + "</projects>\n</manifest>\n";
+    pushFactory
+        .create(db, admin.getIdent(), manifest2Repo, "Subject", "default", xml2)
+        .to("refs/heads/b1")
+        .assertOkStatus();
+
+    // Add new project, that should not be imported
+    xml2 =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+            + "<manifest>\n<projects>\n"
+            + "<project name=\""
+            + manifest2Key.get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + manifest2Key.get()
+            + "\" path=\"manifest2\" />\n"
+            + "<project name=\""
+            + testRepoKeys[1].get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + testRepoKeys[1].get()
+            + "\" path=\"project2\" />\n"
+            + "</projects>\n</manifest>\n";
+
+    pushFactory
+        .create(db, admin.getIdent(), manifest2Repo, "Subject", "default", xml2)
+        .to("refs/heads/master")
+        .assertOkStatus();
+
+    // XML change will trigger commit to superproject.
+    String xml1 =
+        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+            + "<manifest>\n<imports>\n"
+            + "<import name=\""
+            + manifest2Key.get()
+            + "\" manifest=\"default\" remote=\""
+            + canonicalWebUrl.get()
+            + manifest2Key.get()
+            + "\" remotebranch=\"b1\" />\n</imports>"
+            + "<projects>\n"
+            + "<project name=\""
+            + testRepoKeys[0].get()
+            + "\" remote=\""
+            + canonicalWebUrl.get()
+            + testRepoKeys[0].get()
+            + "\" path=\"project1\" />\n"
+            + "</projects>\n</manifest>\n";
+
+    pushFactory
+        .create(db, admin.getIdent(), manifest1Repo, "Subject", "default", xml1)
+        .to("refs/heads/srcbranch")
+        .assertOkStatus();
+
+    BranchApi branch = gApi.projects().name(superKey.get()).branch("refs/heads/destbranch");
+    assertThat(branch.file("project1").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
+    assertThat(branch.file("manifest2").getContentType()).isEqualTo("x-git/gitlink; charset=UTF-8");
+    try {
+      branch.file("project2");
+      fail("wanted exception");
+    } catch (ResourceNotFoundException e) {
+      // all fine.
+    }
+  }
+
   private void outer() throws Exception {
     inner();
   }