SLING-8311 - Investigate creating a Sling CLI tool for development task automation

Implement command for generating release result email, still WIP.
diff --git a/README.md b/README.md
index af04f3f..eabf20c 100644
--- a/README.md
+++ b/README.md
@@ -18,10 +18,16 @@
     
 This invocation produces a list of available subcommands.
 
-Currently the only implemented command is generating the release vote email, for instance
+## Commands
+
+Generating a release vote email
 
     docker run --env-file=./docker-env apache/sling-cli release prepare-email $STAGING_REPOSITORY_ID
     
+Generating a release vote result email
+
+    docker run --env-file=./docker-env apache/sling-cli release tally-votes $STAGING_REPOSITORY_ID
+    
 ## Assumptions
 
 This tool assumes that the name of the staging repository matches the one of the version in Jira. For instance, the
diff --git a/docker-env.sample b/docker-env.sample
index 15454cf..7a2a892 100644
--- a/docker-env.sample
+++ b/docker-env.sample
@@ -12,4 +12,3 @@
 # ----------------------------------------------------------------------------------------
 ASF_USERNAME=changeme
 ASF_PASSWORD=changeme
-RELEASE_ID=42
diff --git a/src/main/java/org/apache/sling/cli/impl/mail/Email.java b/src/main/java/org/apache/sling/cli/impl/mail/Email.java
new file mode 100644
index 0000000..54ec66e
--- /dev/null
+++ b/src/main/java/org/apache/sling/cli/impl/mail/Email.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cli.impl.mail;
+
+public class Email {
+    private String from;
+    private String subject;
+    private String body;
+
+    public String getFrom() {
+        return from;
+    }
+
+    public void setFrom(String from) {
+        this.from = from;
+    }
+
+    public String getSubject() {
+        return subject;
+    }
+
+    public void setSubject(String subject) {
+        this.subject = subject;
+    }
+
+    public String getBody() {
+        return body;
+    }
+
+    public void setBody(String body) {
+        this.body = body;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/cli/impl/mail/EmailThread.java b/src/main/java/org/apache/sling/cli/impl/mail/EmailThread.java
new file mode 100644
index 0000000..03ac673
--- /dev/null
+++ b/src/main/java/org/apache/sling/cli/impl/mail/EmailThread.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cli.impl.mail;
+
+import java.util.List;
+
+public class EmailThread {
+    private List<Email> emails;
+
+    public List<Email> getEmails() {
+        return emails;
+    }
+
+    public void setEmails(List<Email> emails) {
+        this.emails = emails;
+    }
+}
diff --git a/src/main/java/org/apache/sling/cli/impl/mail/VoteThreadFinder.java b/src/main/java/org/apache/sling/cli/impl/mail/VoteThreadFinder.java
new file mode 100644
index 0000000..0d39968
--- /dev/null
+++ b/src/main/java/org/apache/sling/cli/impl/mail/VoteThreadFinder.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cli.impl.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.osgi.service.component.annotations.Component;
+
+import com.google.gson.Gson;
+
+@Component(service = VoteThreadFinder.class)
+public class VoteThreadFinder {
+    
+    public EmailThread findVoteThread(String releaseName) throws IOException {
+        try ( CloseableHttpClient client = HttpClients.createDefault() ) {
+            
+            URI uri = new URIBuilder("https://lists.apache.org/api/stats.lua")
+                .addParameter("domain", "sling.apache.org")
+                .addParameter("list", "dev")
+                .addParameter("d", "lte=1M")
+                .addParameter("q", "[VOTE] Release " + releaseName)
+                .build();
+            
+            HttpGet get = new HttpGet(uri);
+            try ( CloseableHttpResponse response = client.execute(get)) {
+                try ( InputStream content = response.getEntity().getContent();
+                        InputStreamReader reader = new InputStreamReader(content)) {
+                    if ( response.getStatusLine().getStatusCode() != 200 )
+                        throw new IOException("Status line : " + response.getStatusLine());
+                    Gson gson = new Gson();
+                    return gson.fromJson(reader, EmailThread.class);
+                }
+            }
+        } catch (URISyntaxException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java b/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java
index 690a4d2..8e34d87 100644
--- a/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java
+++ b/src/main/java/org/apache/sling/cli/impl/release/TallyVotesCommand.java
@@ -16,8 +16,18 @@
  */
 package org.apache.sling.cli.impl.release;
 
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
 import org.apache.sling.cli.impl.Command;
+import org.apache.sling.cli.impl.mail.Email;
+import org.apache.sling.cli.impl.mail.EmailThread;
+import org.apache.sling.cli.impl.mail.VoteThreadFinder;
+import org.apache.sling.cli.impl.nexus.StagingRepository;
+import org.apache.sling.cli.impl.nexus.StagingRepositoryFinder;
 import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -27,13 +37,66 @@
     Command.PROPERTY_NAME_SUMMARY+"=Counts votes cast for a release and generates the result email"
 })
 public class TallyVotesCommand implements Command {
-
+    
+    // TODO - move to file
+    private static final String EMAIL_TEMPLATE ="\n" + 
+            "\n" + 
+            "To: \"Sling Developers List\" <dev@sling.apache.org>\n" + 
+            "Subject: [RESULT] [VOTE] Release Apache Sling ##RELEASE_NAME##\n" + 
+            "\n" + 
+            "Hi,\n" + 
+            "\n" + 
+            "The vote has passed with the following result :\n" + 
+            "\n" + 
+            "+1 (binding): ##BINDING_VOTERS##\n" + 
+            "\n" + 
+            "I will copy this release to the Sling dist directory and\n" + 
+            "promote the artifacts to the central Maven repository.\n";
     private final Logger logger = LoggerFactory.getLogger(getClass());
 
+    @Reference
+    private StagingRepositoryFinder repoFinder;
+    
+    @Reference
+    private VoteThreadFinder voteThreadFinder;
+    
     @Override
     public void execute(String target) {
-        logger.info("Tallying votes for release {}", target);
+        try {
+            
+            StagingRepository repository = repoFinder.find(Integer.parseInt(target));
+            // TODO - release name cleanup does not belong here
+            String releaseName = repository.getDescription().replaceFirst(" RC[0-9]+", "");
+            EmailThread voteThread = voteThreadFinder.findVoteThread(releaseName);
 
+            // TODO - validate which voters are binding and list them separately in the email
+            String bindingVoters = voteThread.getEmails().stream()
+                .filter( e -> isPositiveVote(e) )
+                .map ( e -> e.getFrom().replaceAll("<.*>", "").trim() )
+                .collect(Collectors.joining(", "));
+            
+            String email = EMAIL_TEMPLATE
+                .replace("##RELEASE_NAME##", releaseName)
+                .replace("##BINDING_VOTERS##", bindingVoters);
+            
+            logger.info(email);
+            
+        } catch (IOException e) {
+            logger.warn("Command execution failed", e);
+        }
+    }
+
+    // TODO - better detection of '+1' votes
+    private boolean isPositiveVote(Email e) {
+        return cleanup(e.getBody()).contains("+1");
+    }
+
+    private String cleanup(String subject) {
+        String[] lines = subject.split("\\n");
+        return Arrays.stream(lines)
+            .filter( l -> !l.isEmpty() )
+            .filter( l -> !l.startsWith(">"))
+            .collect(Collectors.joining("\n"));
     }
 
 }