1 /*
  2  * Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 package org.openjdk.skara.bot;
 24 
 25 import org.openjdk.skara.census.*;
 26 import org.openjdk.skara.host.*;
 27 import org.openjdk.skara.host.network.URIBuilder;
 28 import org.openjdk.skara.json.*;
 29 import org.openjdk.skara.vcs.*;
 30 
 31 import java.io.*;
 32 import java.net.URI;
 33 import java.nio.file.*;
 34 import java.time.Duration;
 35 import java.util.*;
 36 import java.util.logging.*;
 37 import java.util.regex.Pattern;
 38 
 39 public class BotRunnerConfiguration {
 40     private final Logger log;
 41     private final JSONObject config;
 42     private final Map<String, Host> hosts;
 43     private final Map<String, HostedRepository> repositories;
 44 
 45     private BotRunnerConfiguration(JSONObject config, Path cwd) throws ConfigurationError {
 46         this.config = config;
 47         log = Logger.getLogger("org.openjdk.skara.bot");
 48 
 49         hosts = parseHosts(config, cwd);
 50         repositories = parseRepositories(config);
 51     }
 52 
 53     private Map<String, Host> parseHosts(JSONObject config, Path cwd) throws ConfigurationError {
 54         Map<String, Host> ret = new HashMap<>();
 55 
 56         if (!config.contains("hosts")) {
 57             return ret;
 58         }
 59 
 60         for (var entry : config.get("hosts").fields()) {
 61             if (entry.value().contains("gitlab")) {
 62                 var gitlab = entry.value().get("gitlab");
 63                 var uri = URIBuilder.base(gitlab.get("url").asString()).build();
 64                 var pat = new PersonalAccessToken(gitlab.get("username").asString(), gitlab.get("pat").asString());
 65                 ret.put(entry.name(), HostFactory.createGitLabHost(uri, pat));
 66             } else if (entry.value().contains("github")) {
 67                 var github = entry.value().get("github");
 68                 URI uri;
 69                 if (github.contains("url")) {
 70                     uri = URIBuilder.base(github.get("url").asString()).build();
 71                 } else {
 72                     uri = URIBuilder.base("https://github.com/").build();
 73                 }
 74                 Pattern webUriPattern = null;
 75                 String webUriReplacement = null;
 76                 if (github.contains("weburl")) {
 77                     webUriPattern = Pattern.compile(github.get("weburl").get("pattern").asString());
 78                     webUriReplacement = github.get("weburl").get("replacement").asString();
 79                 }
 80 
 81                 if (github.contains("app")) {
 82                     var keyFile = cwd.resolve(github.get("app").get("key").asString());
 83                     ret.put(entry.name(), HostFactory.createGitHubHost(uri, webUriPattern, webUriReplacement, keyFile.toString(),
 84                                                                        github.get("app").get("id").asString(),
 85                                                                        github.get("app").get("installation").asString()));
 86                 } else {
 87                     var pat = new PersonalAccessToken(github.get("username").asString(), github.get("pat").asString());
 88                     ret.put(entry.name(), HostFactory.createGitHubHost(uri, pat));
 89                 }
 90             } else {
 91                 throw new ConfigurationError("Host " + entry.name());
 92             }
 93         }
 94 
 95         return ret;
 96     }
 97 
 98     private Map<String, HostedRepository> parseRepositories(JSONObject config) throws ConfigurationError {
 99         Map<String, HostedRepository> ret = new HashMap<>();
100 
101         if (!config.contains("repositories")) {
102             return ret;
103         }
104 
105         for (var entry : config.get("repositories").fields()) {
106             var hostName = entry.value().get("host").asString();
107             if (!hosts.containsKey(hostName)) {
108                 throw new ConfigurationError("Repository " + entry.name() + " uses undefined host '" + hostName + "'");
109             }
110             var host = hosts.get(hostName);
111             var repo = host.getRepository(entry.value().get("repository").asString());
112             ret.put(entry.name(), repo);
113         }
114 
115         return ret;
116     }
117 
118     private static class RepositoryEntry {
119         HostedRepository repository;
120         String ref;
121     }
122 
123     private RepositoryEntry parseRepositoryEntry(String entry) throws ConfigurationError {
124         var ret = new RepositoryEntry();
125         var refSeparatorIndex = entry.indexOf(':');
126         if (refSeparatorIndex >= 0) {
127             ret.ref = entry.substring(refSeparatorIndex + 1);
128             entry = entry.substring(0, refSeparatorIndex);
129         }
130         var hostSeparatorIndex = entry.indexOf('/');
131         if (hostSeparatorIndex >= 0) {
132             var hostName = entry.substring(0, hostSeparatorIndex);
133             var host = hosts.get(hostName);
134             if (!hosts.containsKey(hostName)) {
135                 throw new ConfigurationError("Repository entry " + entry + " uses undefined host '" + hostName + "'");
136             }
137             var repositoryName = entry.substring(hostSeparatorIndex + 1);
138             ret.repository = host.getRepository(repositoryName);
139         } else {
140             if (!repositories.containsKey(entry)) {
141                 throw new ConfigurationError("Repository " + entry + " is not defined!");
142             }
143             ret.repository = repositories.get(entry);
144         }
145 
146         if (ret.ref == null) {
147             ret.ref = ret.repository.getRepositoryType() == VCS.GIT ? "master" : "default";
148         }
149 
150         return ret;
151     }
152 
153     public static BotRunnerConfiguration parse(JSONObject config, Path cwd) throws ConfigurationError {
154         return new BotRunnerConfiguration(config, cwd);
155     }
156 
157     public static BotRunnerConfiguration parse(JSONObject config) throws ConfigurationError {
158         return parse(config, Paths.get("."));
159     }
160 
161     public BotConfiguration perBotConfiguration(String botName) throws ConfigurationError {
162         if (!config.contains(botName)) {
163             throw new ConfigurationError("No configuration for bot name: " + botName);
164         }
165 
166         return new BotConfiguration() {
167             @Override
168             public Path storageFolder() {
169                 if (!config.contains("storage") || !config.get("storage").contains("path")) {
170                     try {
171                         return Files.createTempDirectory("storage-" + botName);
172                     } catch (IOException e) {
173                         throw new UncheckedIOException(e);
174                     }
175                 }
176                 return Paths.get(config.get("storage").get("path").asString()).resolve(botName);
177             }
178 
179             @Override
180             public HostedRepository repository(String name) {
181                 try {
182                     var entry = parseRepositoryEntry(name);
183                     return entry.repository;
184                 } catch (ConfigurationError configurationError) {
185                     throw new RuntimeException("Couldn't find repository with name: " + name, configurationError);
186                 }
187             }
188 
189             @Override
190             public String repositoryRef(String name) {
191                 try {
192                     var entry = parseRepositoryEntry(name);
193                     return entry.ref;
194                 } catch (ConfigurationError configurationError) {
195                     throw new RuntimeException("Couldn't find repository with name: " + name, configurationError);
196                 }
197             }
198 
199             @Override
200             public JSONObject specific() {
201                 return config.get(botName).asObject();
202             }
203         };
204     }
205 
206     /**
207      * The amount of time to wait between each invocation of Bot.getPeriodicItems.
208      * @return
209      */
210     Duration scheduledExecutionPeriod() {
211         if (!config.contains("runner") || !config.get("runner").contains("interval")) {
212             log.info("No WorkItem invocation period defined, using default value");
213             return Duration.ofSeconds(10);
214         } else {
215             return Duration.parse(config.get("runner").get("interval").asString());
216         }
217     }
218 
219     /**
220      * Number of WorkItems to execute in parallel.
221      * @return
222      */
223     Integer concurrency() {
224         if (!config.contains("runner") || !config.get("runner").contains("concurrency")) {
225             log.info("WorkItem concurrency not defined, using default value");
226             return 2;
227         } else {
228             return config.get("runner").get("concurrency").asInt();
229         }
230     }
231 
232     /**
233      * Folder that WorkItems may use to store temporary data.
234      * @return
235      */
236     Path scratchFolder() {
237         if (!config.contains("scratch") || !config.get("scratch").contains("path")) {
238             try {
239                 log.warning("No scratch folder defined, creating a temporary folder");
240                 return Files.createTempDirectory("botrunner");
241             } catch (IOException e) {
242                 throw new UncheckedIOException(e);
243             }
244         }
245         return Paths.get(config.get("scratch").get("path").asString());
246     }
247 
248     Optional<Integer> restReceiverPort() {
249         if (!config.contains("webhooks")) {
250             return Optional.empty();
251         }
252         return Optional.of(config.get("webhooks").get("port").asInt());
253     }
254 }