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    }