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.Repository;
 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 HostedRepository getRepository(String name) throws ConfigurationError {
119         if (!repositories.containsKey(name)) {
120             throw new ConfigurationError("Repository " + name + " is not defined!");
121         }
122         return repositories.get(name);
123     }
124 
125     public static BotRunnerConfiguration parse(JSONObject config, Path cwd) throws ConfigurationError {
126         return new BotRunnerConfiguration(config, cwd);
127     }
128 
129     public static BotRunnerConfiguration parse(JSONObject config) throws ConfigurationError {
130         return parse(config, Paths.get("."));
131     }
132 
133     public BotConfiguration perBotConfiguration(String botName) throws ConfigurationError {
134 
135         if (!config.contains(botName)) {
136             throw new ConfigurationError("No configuration for bot name: " + botName);
137         }
138 
139         return new BotConfiguration() {
140             @Override
141             public Path storageFolder() {
142                 if (!config.contains("storage") || !config.get("storage").contains("path")) {
143                     try {
144                         return Files.createTempDirectory("storage-" + botName);
145                     } catch (IOException e) {
146                         throw new UncheckedIOException(e);
147                     }
148                 }
149                 return Paths.get(config.get("storage").get("path").asString()).resolve(botName);
150             }
151 
152             @Override
153             public HostedRepository repository(String name) {
154                 try {
155                     return getRepository(name);
156                 } catch (ConfigurationError configurationError) {
157                     throw new RuntimeException("Couldn't find repository with name: " + name, configurationError);
158                 }
159             }
160 
161             @Override
162             public JSONObject specific() {
163                 return config.get(botName).asObject();
164             }
165         };
166     }
167 
168     /**
169      * The amount of time to wait between each invocation of Bot.getPeriodicItems.
170      * @return
171      */
172     Duration scheduledExecutionPeriod() {
173         if (!config.contains("runner") || !config.get("runner").contains("interval")) {
174             log.info("No WorkItem invocation period defined, using default value");
175             return Duration.ofSeconds(10);
176         } else {
177             return Duration.parse(config.get("runner").get("interval").asString());
178         }
179     }
180 
181     /**
182      * Number of WorkItems to execute in parallel.
183      * @return
184      */
185     Integer concurrency() {
186         if (!config.contains("runner") || !config.get("runner").contains("concurrency")) {
187             log.info("WorkItem concurrency not defined, using default value");
188             return 2;
189         } else {
190             return config.get("runner").get("concurrency").asInt();
191         }
192     }
193 
194     /**
195      * Folder that WorkItems may use to store temporary data.
196      * @return
197      */
198     Path scratchFolder() {
199         if (!config.contains("scratch") || !config.get("scratch").contains("path")) {
200             try {
201                 log.warning("No scratch folder defined, creating a temporary folder");
202                 return Files.createTempDirectory("botrunner");
203             } catch (IOException e) {
204                 throw new UncheckedIOException(e);
205             }
206         }
207         return Paths.get(config.get("scratch").get("path").asString());
208     }
209 
210     Optional<Integer> restReceiverPort() {
211         if (!config.contains("webhooks")) {
212             return Optional.empty();
213         }
214         return Optional.of(config.get("webhooks").get("port").asInt());
215     }
216 }