001 /*
002 * SonarQube, open source software quality management tool.
003 * Copyright (C) 2008-2014 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * SonarQube is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * SonarQube is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
019 */
020 package org.sonar.batch.bootstrap;
021
022 import com.google.common.annotations.VisibleForTesting;
023 import com.google.common.base.Strings;
024 import org.slf4j.Logger;
025 import org.slf4j.LoggerFactory;
026 import org.sonar.api.utils.SonarException;
027 import org.sonar.home.cache.FileCache;
028
029 import java.io.File;
030 import java.io.IOException;
031 import java.io.InputStream;
032 import java.net.MalformedURLException;
033 import java.net.URL;
034 import java.net.URLClassLoader;
035
036 /**
037 * Contains and provides class loader extended with the JDBC Driver hosted on the server-side.
038 */
039 public class JdbcDriverHolder {
040
041 private static final Logger LOG = LoggerFactory.getLogger(JdbcDriverHolder.class);
042
043 private ServerClient serverClient;
044 private AnalysisMode analysisMode;
045 private FileCache fileCache;
046
047 // initialized in start()
048 private JdbcDriverClassLoader classLoader = null;
049
050 public JdbcDriverHolder(FileCache fileCache, AnalysisMode analysisMode, ServerClient serverClient) {
051 this.serverClient = serverClient;
052 this.analysisMode = analysisMode;
053 this.fileCache = fileCache;
054 }
055
056 public void start() {
057 if (!analysisMode.isPreview()) {
058 try {
059 LOG.info("Install JDBC driver");
060 String[] nameAndHash = downloadJdbcDriverIndex();
061 if (nameAndHash.length > 0) {
062 String filename = nameAndHash[0];
063 String hash = nameAndHash[1];
064
065 File jdbcDriver = fileCache.get(filename, hash, new FileCache.Downloader() {
066 public void download(String filename, File toFile) throws IOException {
067 String url = "/deploy/" + filename;
068 if (LOG.isDebugEnabled()) {
069 LOG.debug("Download {} to {}", url, toFile.getAbsolutePath());
070 } else {
071 LOG.info("Download {}", filename);
072 }
073 serverClient.download(url, toFile);
074 }
075 });
076 classLoader = initClassloader(jdbcDriver);
077 }
078 } catch (SonarException e) {
079 throw e;
080 } catch (Exception e) {
081 throw new SonarException("Fail to install JDBC driver", e);
082 }
083 }
084 }
085
086 @VisibleForTesting
087 JdbcDriverClassLoader getClassLoader() {
088 return classLoader;
089 }
090
091 @VisibleForTesting
092 static JdbcDriverClassLoader initClassloader(File jdbcDriver) {
093 JdbcDriverClassLoader classLoader;
094 try {
095 ClassLoader parentClassLoader = JdbcDriverHolder.class.getClassLoader();
096 classLoader = new JdbcDriverClassLoader(jdbcDriver.toURI().toURL(), parentClassLoader);
097
098 } catch (MalformedURLException e) {
099 throw new SonarException("Fail to get URL of : " + jdbcDriver.getAbsolutePath(), e);
100 }
101
102 // set as the current context classloader for hibernate, else it does not find the JDBC driver.
103 Thread.currentThread().setContextClassLoader(classLoader);
104 return classLoader;
105 }
106
107 /**
108 * This method automatically invoked by PicoContainer and unregisters JDBC drivers, which were forgotten.
109 * <p>
110 * Dynamically loaded JDBC drivers can not be simply used and this is a well known problem of {@link java.sql.DriverManager},
111 * so <a href="http://stackoverflow.com/questions/288828/how-to-use-a-jdbc-driver-from-an-arbitrary-location">workaround is to use proxy</a>.
112 * However DriverManager also contains memory leak, thus not only proxy, but also original driver must be unregistered,
113 * otherwise our class loader would be kept in memory.
114 * </p>
115 * <p>
116 * This operation contains unnecessary complexity because:
117 * <ul>
118 * <li>DriverManager checks the class loader of the calling class. Thus we can't simply ask it about deregistration.</li>
119 * <li>We can't use reflection against DriverManager, since it would create a dependency on DriverManager implementation,
120 * which can be changed (like it was done - compare Java 1.5 and 1.6).</li>
121 * <li>So we use companion - {@link JdbcLeakPrevention}. But we can't just create an instance,
122 * since it will be loaded by parent class loader and again will not pass DriverManager's check.
123 * So, we load the bytes via our parent class loader, but define the class with this class loader
124 * thus JdbcLeakPrevention looks like our class to the DriverManager.</li>
125 * </li>
126 * </p>
127 */
128 public void stop() {
129 if (classLoader != null) {
130 classLoader.clearReferencesJdbc();
131 if (Thread.currentThread().getContextClassLoader() == classLoader) {
132 Thread.currentThread().setContextClassLoader(classLoader.getParent());
133 }
134 classLoader = null;
135 }
136 }
137
138 private String[] downloadJdbcDriverIndex() {
139 String url = "/deploy/jdbc-driver.txt";
140 try {
141 LOG.debug("Download index of jdbc-driver");
142 String indexContent = serverClient.request(url);
143 // File is empty when H2 is used
144 if (Strings.isNullOrEmpty(indexContent)) {
145 return new String[] {};
146 }
147 return indexContent.split("\\|");
148 } catch (Exception e) {
149 throw new SonarException("Fail to download jdbc-driver index: " + url, e);
150 }
151 }
152
153 static class JdbcDriverClassLoader extends URLClassLoader {
154
155 public JdbcDriverClassLoader(URL jdbcDriver, ClassLoader parent) {
156 super(new URL[] {jdbcDriver}, parent);
157 }
158
159 public void clearReferencesJdbc() {
160 InputStream is = getResourceAsStream("org/sonar/batch/bootstrap/JdbcLeakPrevention.class");
161 byte[] classBytes = new byte[2048];
162 int offset = 0;
163 try {
164 int read = is.read(classBytes, offset, classBytes.length - offset);
165 while (read > -1) {
166 offset += read;
167 if (offset == classBytes.length) {
168 // Buffer full - double size
169 byte[] tmp = new byte[classBytes.length * 2];
170 System.arraycopy(classBytes, 0, tmp, 0, classBytes.length);
171 classBytes = tmp;
172 }
173 read = is.read(classBytes, offset, classBytes.length - offset);
174 }
175
176 Class<?> lpClass = defineClass("org.sonar.batch.bootstrap.JdbcLeakPrevention", classBytes, 0, offset, this.getClass().getProtectionDomain());
177 Object obj = lpClass.newInstance();
178
179 obj.getClass().getMethod("unregisterDrivers").invoke(obj);
180 } catch (Exception e) {
181 LOG.warn("JDBC driver deregistration failed", e);
182 } finally {
183 if (is != null) {
184 try {
185 is.close();
186 } catch (IOException ioe) {
187 LOG.warn(ioe.getMessage(), ioe);
188 }
189 }
190 }
191 }
192 }
193
194 }