You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Bootstrapper.java 9.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. /*
  2. * Sonar Runner - API
  3. * Copyright (C) 2011 SonarSource
  4. * dev@sonar.codehaus.org
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public
  17. * License along with this program; if not, write to the Free Software
  18. * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
  19. */
  20. package org.sonar.runner;
  21. import org.apache.commons.io.FileUtils;
  22. import org.apache.commons.io.IOUtils;
  23. import java.io.File;
  24. import java.io.FileOutputStream;
  25. import java.io.IOException;
  26. import java.io.InputStream;
  27. import java.io.InputStreamReader;
  28. import java.io.Reader;
  29. import java.net.ConnectException;
  30. import java.net.HttpURLConnection;
  31. import java.net.MalformedURLException;
  32. import java.net.URL;
  33. import java.net.UnknownHostException;
  34. import java.util.ArrayList;
  35. import java.util.List;
  36. import java.util.regex.Matcher;
  37. import java.util.regex.Pattern;
  38. /**
  39. * Bootstrapper used to download everything from the server and create the correct classloader required to execute a Sonar analysis in isolation.
  40. */
  41. class Bootstrapper {
  42. static final String VERSION_PATH = "/api/server/version";
  43. static final String BATCH_PATH = "/batch/";
  44. static final String BOOTSTRAP_INDEX_PATH = "/batch_bootstrap/index";
  45. static final int CONNECT_TIMEOUT_MILLISECONDS = 30000;
  46. static final int READ_TIMEOUT_MILLISECONDS = 60000;
  47. private static final Pattern CHARSET_PATTERN = Pattern.compile("(?i)\\bcharset=\\s*\"?([^\\s;\"]*)");
  48. private static final String[] UNSUPPORTED_VERSIONS_FOR_CACHE = {"1", "2", "3.0", "3.1", "3.2", "3.3", "3.4"};
  49. private File bootDir;
  50. private String serverUrl;
  51. private String productToken;
  52. private String serverVersion;
  53. private SonarCache cache;
  54. /**
  55. * @param productToken part of User-Agent request-header field - see http://tools.ietf.org/html/rfc1945#section-10.15
  56. */
  57. Bootstrapper(String productToken, String serverUrl, File workDir, SonarCache cache) {
  58. this.productToken = productToken;
  59. this.cache = cache;
  60. bootDir = new File(workDir, "batch");
  61. bootDir.mkdirs();
  62. if (serverUrl.endsWith("/")) {
  63. this.serverUrl = serverUrl.substring(0, serverUrl.length() - 1);
  64. } else {
  65. this.serverUrl = serverUrl;
  66. }
  67. }
  68. /**
  69. * @return server url
  70. */
  71. String getServerUrl() {
  72. return serverUrl;
  73. }
  74. /**
  75. * @return server version
  76. */
  77. String getServerVersion() {
  78. if (serverVersion == null) {
  79. try {
  80. serverVersion = remoteContent(VERSION_PATH);
  81. } catch (ConnectException e) {
  82. Logs.error("Sonar server '" + serverUrl + "' can not be reached");
  83. throw new RunnerException("Fail to request server version", e);
  84. } catch (UnknownHostException e) {
  85. Logs.error("Sonar server '" + serverUrl + "' can not be reached");
  86. throw new RunnerException("Fail to request server version", e);
  87. } catch (IOException e) {
  88. throw new RunnerException("Fail to request server version", e);
  89. }
  90. }
  91. return serverVersion;
  92. }
  93. /**
  94. * Download batch files from server and creates {@link BootstrapClassLoader}.
  95. * To use this method version of Sonar should be at least 2.6.
  96. *
  97. * @param urls additional URLs for loading classes and resources
  98. * @param parent parent ClassLoader
  99. * @param unmaskedPackages only classes and resources from those packages would be available for loading from parent
  100. */
  101. BootstrapClassLoader createClassLoader(URL[] urls, ClassLoader parent, String... unmaskedPackages) {
  102. BootstrapClassLoader classLoader = new BootstrapClassLoader(parent, unmaskedPackages);
  103. List<File> files = downloadBatchFiles();
  104. for (URL url : urls) {
  105. classLoader.addURL(url);
  106. }
  107. for (File file : files) {
  108. try {
  109. classLoader.addURL(file.toURI().toURL());
  110. } catch (MalformedURLException e) {
  111. throw new IllegalStateException("Fail to create classloader", e);
  112. }
  113. }
  114. return classLoader;
  115. }
  116. private void remoteContentToFile(String path, File toFile) {
  117. InputStream input = null;
  118. FileOutputStream output = null;
  119. String fullUrl = serverUrl + path;
  120. if (Logs.isDebugEnabled()) {
  121. Logs.debug("Downloading " + fullUrl + " to " + toFile.getAbsolutePath());
  122. }
  123. // Don't log for old versions without cache to not pollute logs
  124. else if (!isUnsupportedVersionForCache(getServerVersion())) {
  125. Logs.info("Downloading " + path.substring(path.lastIndexOf('/') + 1));
  126. }
  127. try {
  128. HttpURLConnection connection = newHttpConnection(new URL(fullUrl));
  129. output = new FileOutputStream(toFile, false);
  130. input = connection.getInputStream();
  131. IOUtils.copyLarge(input, output);
  132. } catch (IOException e) {
  133. IOUtils.closeQuietly(output);
  134. FileUtils.deleteQuietly(toFile);
  135. throw new IllegalStateException("Fail to download the file: " + fullUrl, e);
  136. } finally {
  137. IOUtils.closeQuietly(input);
  138. IOUtils.closeQuietly(output);
  139. }
  140. }
  141. String remoteContent(String path) throws IOException {
  142. String fullUrl = serverUrl + path;
  143. HttpURLConnection conn = newHttpConnection(new URL(fullUrl));
  144. String charset = getCharsetFromContentType(conn.getContentType());
  145. if (charset == null || "".equals(charset)) {
  146. charset = "UTF-8";
  147. }
  148. Reader reader = new InputStreamReader(conn.getInputStream(), charset);
  149. try {
  150. int statusCode = conn.getResponseCode();
  151. if (statusCode != HttpURLConnection.HTTP_OK) {
  152. throw new IOException("Status returned by url : '" + fullUrl + "' is invalid : " + statusCode);
  153. }
  154. return IOUtils.toString(reader);
  155. } finally {
  156. IOUtils.closeQuietly(reader);
  157. conn.disconnect();
  158. }
  159. }
  160. /**
  161. * By convention, the product tokens are listed in order of their significance for identifying the application.
  162. */
  163. String getUserAgent() {
  164. return "sonar-bootstrapper/" + Version.getVersion() + " " + productToken;
  165. }
  166. HttpURLConnection newHttpConnection(URL url) throws IOException {
  167. HttpURLConnection connection = (HttpURLConnection) url.openConnection();
  168. connection.setConnectTimeout(CONNECT_TIMEOUT_MILLISECONDS);
  169. connection.setReadTimeout(READ_TIMEOUT_MILLISECONDS);
  170. connection.setInstanceFollowRedirects(true);
  171. connection.setRequestMethod("GET");
  172. connection.setRequestProperty("User-Agent", getUserAgent());
  173. return connection;
  174. }
  175. private List<File> downloadBatchFiles() {
  176. try {
  177. List<File> files = new ArrayList<File>();
  178. if (isUnsupportedVersionForCache(getServerVersion())) {
  179. getBootstrapFilesFromOldURL(files);
  180. }
  181. else {
  182. getBootstrapFiles(files);
  183. }
  184. return files;
  185. } catch (Exception e) {
  186. throw new IllegalStateException("Fail to download libraries from server", e);
  187. }
  188. }
  189. private void getBootstrapFilesFromOldURL(List<File> files) throws IOException {
  190. String libs = remoteContent(BATCH_PATH);
  191. for (String lib : libs.split(",")) {
  192. File file = new File(bootDir, lib);
  193. remoteContentToFile(BATCH_PATH + lib, file);
  194. files.add(file);
  195. }
  196. }
  197. private void getBootstrapFiles(List<File> files) throws IOException {
  198. String libs = remoteContent(BOOTSTRAP_INDEX_PATH);
  199. String[] lines = libs.split("[\r\n]+");
  200. for (String line : lines) {
  201. line = line.trim();
  202. if ("".equals(line)) {
  203. continue;
  204. }
  205. String[] libAndMd5 = line.split("\\|");
  206. String libName = libAndMd5[0];
  207. String remoteMd5 = libAndMd5.length > 0 ? libAndMd5[1] : null;
  208. File libInCache = null;
  209. if (remoteMd5 != null && !"".equals(remoteMd5)) {
  210. libInCache = cache.getFileFromCache(libName, remoteMd5);
  211. }
  212. if (libInCache == null) {
  213. File tmpLocation = cache.getTemporaryFile();
  214. remoteContentToFile(BATCH_PATH + libName, tmpLocation);
  215. String md5 = cache.cacheFile(tmpLocation, libName);
  216. libInCache = cache.getFileFromCache(libName, md5);
  217. if (!md5.equals(remoteMd5)) {
  218. throw new RunnerException("INVALID CHECKSUM: File " + libInCache.getAbsolutePath() + " was expected to have checksum " + remoteMd5
  219. + " but was downloaded with checksum " + md5);
  220. }
  221. }
  222. files.add(libInCache);
  223. }
  224. }
  225. static boolean isUnsupportedVersionForCache(String version) {
  226. return VersionUtils.isUnsupportedVersion(version, UNSUPPORTED_VERSIONS_FOR_CACHE);
  227. }
  228. /**
  229. * Parse out a charset from a content type header.
  230. *
  231. * @param contentType e.g. "text/html; charset=EUC-JP"
  232. * @return "EUC-JP", or null if not found. Charset is trimmed and uppercased.
  233. */
  234. static String getCharsetFromContentType(String contentType) {
  235. if (contentType == null) {
  236. return null;
  237. }
  238. Matcher m = CHARSET_PATTERN.matcher(contentType);
  239. if (m.find()) {
  240. return m.group(1).trim().toUpperCase();
  241. }
  242. return null;
  243. }
  244. }