001    /*
002     * Sonar, open source software quality management tool.
003     * Copyright (C) 2008-2012 SonarSource
004     * mailto:contact AT sonarsource DOT com
005     *
006     * Sonar 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     * Sonar 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
017     * License along with Sonar; if not, write to the Free Software
018     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
019     */
020    package org.sonar.batch.bootstrap;
021    
022    import org.slf4j.Logger;
023    import org.slf4j.LoggerFactory;
024    import org.sonar.api.utils.SonarException;
025    
026    import java.io.File;
027    import java.io.IOException;
028    import java.io.InputStream;
029    import java.net.MalformedURLException;
030    import java.net.URL;
031    import java.net.URLClassLoader;
032    import java.util.List;
033    
034    /**
035     * Contains and provides class loader extended with the JDBC Driver hosted on the server-side.
036     */
037    public class JdbcDriverHolder {
038    
039      private static final Logger LOG = LoggerFactory.getLogger(JdbcDriverHolder.class);
040      private JdbcDriverClassLoader classLoader;
041    
042      public JdbcDriverHolder(ArtifactDownloader extensionDownloader) {
043        this(extensionDownloader.downloadJdbcDriver());
044      }
045    
046      JdbcDriverHolder(File jdbcDriver) {
047        try {
048          ClassLoader parentClassLoader = JdbcDriverHolder.class.getClassLoader();
049          classLoader = new JdbcDriverClassLoader(jdbcDriver.toURI().toURL(), parentClassLoader);
050    
051        } catch (MalformedURLException e) {
052          throw new SonarException("Fail to get URL of : " + jdbcDriver.getAbsolutePath(), e);
053        }
054      }
055    
056      public URLClassLoader getClassLoader() {
057        return classLoader;
058      }
059    
060      /**
061       * This method automatically invoked by PicoContainer and deregisters JDBC drivers, which were forgotten.
062       * <p>
063       * Dynamically loaded JDBC drivers can not be simply used and this is a well known problem of {@link java.sql.DriverManager},
064       * so <a href="http://stackoverflow.com/questions/288828/how-to-use-a-jdbc-driver-from-an-arbitrary-location">workaround is to use proxy</a>.
065       * However DriverManager also contains memory leak, thus not only proxy, but also original driver must be deregistered,
066       * otherwise our class loader would be kept in memory.
067       * </p>
068       * <p>
069       * This operation contains unnecessary complexity because:
070       * <ul>
071       * <li>DriverManager checks the class loader of the calling class. Thus we can't simply ask it about deregistration.</li>
072       * <li>We can't use reflection against DriverManager, since it would create a dependency on DriverManager implementation,
073       * which can be changed (like it was done - compare Java 1.5 and 1.6).</li>
074       * <li>So we use companion - {@link JdbcLeakPrevention}. But we can't just create an instance,
075       * since it will be loaded by parent class loader and again will not pass DriverManager's check.
076       * So, we load the bytes via our parent class loader, but define the class with this class loader
077       * thus JdbcLeakPrevention looks like our class to the DriverManager.</li>
078       * </li>
079       * </p>
080       */
081      public void stop() {
082        classLoader.clearReferencesJdbc();
083        classLoader = null;
084      }
085    
086      private static class JdbcDriverClassLoader extends URLClassLoader {
087    
088        public JdbcDriverClassLoader(URL jdbcDriver, ClassLoader parent) {
089          super(new URL[] { jdbcDriver }, parent);
090        }
091    
092        public void clearReferencesJdbc() {
093          InputStream is = getResourceAsStream("org/sonar/batch/bootstrap/JdbcLeakPrevention.class");
094          byte[] classBytes = new byte[2048];
095          int offset = 0;
096          try {
097            int read = is.read(classBytes, offset, classBytes.length - offset);
098            while (read > -1) {
099              offset += read;
100              if (offset == classBytes.length) {
101                // Buffer full - double size
102                byte[] tmp = new byte[classBytes.length * 2];
103                System.arraycopy(classBytes, 0, tmp, 0, classBytes.length);
104                classBytes = tmp;
105              }
106              read = is.read(classBytes, offset, classBytes.length - offset);
107            }
108    
109            Class<?> lpClass = defineClass("org.sonar.batch.bootstrap.JdbcLeakPrevention", classBytes, 0, offset, this.getClass().getProtectionDomain());
110            Object obj = lpClass.newInstance();
111    
112            @SuppressWarnings("unchecked")
113            List<String> driverNames = (List<String>) obj.getClass().getMethod("clearJdbcDriverRegistrations").invoke(obj);
114    
115            for (String name : driverNames) {
116              LOG.debug("To prevent a memory leak, the JDBC Driver [{}] has been forcibly deregistered", name);
117            }
118          } catch (Exception e) {
119            LOG.warn("JDBC driver deregistration failed", e);
120          } finally {
121            if (is != null) {
122              try {
123                is.close();
124              } catch (IOException ioe) {
125                LOG.warn(ioe.getMessage(), ioe);
126              }
127            }
128          }
129        }
130      }
131    
132    }