001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.hadoop.util;
020
021import java.io.File;
022import java.io.FilenameFilter;
023import java.io.IOException;
024import java.io.InputStream;
025import java.net.MalformedURLException;
026import java.net.URL;
027import java.net.URLClassLoader;
028import java.util.ArrayList;
029import java.util.Arrays;
030import java.util.List;
031import java.util.Properties;
032
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.apache.hadoop.classification.InterfaceAudience.Public;
036import org.apache.hadoop.classification.InterfaceStability.Unstable;
037
038/**
039 * A {@link URLClassLoader} for application isolation. Classes from the
040 * application JARs are loaded in preference to the parent loader.
041 */
042@Public
043@Unstable
044public class ApplicationClassLoader extends URLClassLoader {
045  /**
046   * Default value of the system classes if the user did not override them.
047   * JDK classes, hadoop classes and resources, and some select third-party
048   * classes are considered system classes, and are not loaded by the
049   * application classloader.
050   */
051  public static final String SYSTEM_CLASSES_DEFAULT;
052
053  private static final String PROPERTIES_FILE =
054      "org.apache.hadoop.application-classloader.properties";
055  private static final String SYSTEM_CLASSES_DEFAULT_KEY =
056      "system.classes.default";
057
058  private static final Log LOG =
059    LogFactory.getLog(ApplicationClassLoader.class.getName());
060
061  private static final FilenameFilter JAR_FILENAME_FILTER =
062    new FilenameFilter() {
063      @Override
064      public boolean accept(File dir, String name) {
065        return name.endsWith(".jar") || name.endsWith(".JAR");
066      }
067  };
068
069  static {
070    try (InputStream is = ApplicationClassLoader.class.getClassLoader()
071        .getResourceAsStream(PROPERTIES_FILE);) {
072      if (is == null) {
073        throw new ExceptionInInitializerError("properties file " +
074            PROPERTIES_FILE + " is not found");
075      }
076      Properties props = new Properties();
077      props.load(is);
078      // get the system classes default
079      String systemClassesDefault =
080          props.getProperty(SYSTEM_CLASSES_DEFAULT_KEY);
081      if (systemClassesDefault == null) {
082        throw new ExceptionInInitializerError("property " +
083            SYSTEM_CLASSES_DEFAULT_KEY + " is not found");
084      }
085      SYSTEM_CLASSES_DEFAULT = systemClassesDefault;
086    } catch (IOException e) {
087      throw new ExceptionInInitializerError(e);
088    }
089  }
090
091  private final ClassLoader parent;
092  private final List<String> systemClasses;
093
094  public ApplicationClassLoader(URL[] urls, ClassLoader parent,
095      List<String> systemClasses) {
096    super(urls, parent);
097    this.parent = parent;
098    if (parent == null) {
099      throw new IllegalArgumentException("No parent classloader!");
100    }
101    // if the caller-specified system classes are null or empty, use the default
102    this.systemClasses = (systemClasses == null || systemClasses.isEmpty()) ?
103        Arrays.asList(StringUtils.getTrimmedStrings(SYSTEM_CLASSES_DEFAULT)) :
104        systemClasses;
105    LOG.info("classpath: " + Arrays.toString(urls));
106    LOG.info("system classes: " + this.systemClasses);
107  }
108
109  public ApplicationClassLoader(String classpath, ClassLoader parent,
110      List<String> systemClasses) throws MalformedURLException {
111    this(constructUrlsFromClasspath(classpath), parent, systemClasses);
112  }
113
114  static URL[] constructUrlsFromClasspath(String classpath)
115      throws MalformedURLException {
116    List<URL> urls = new ArrayList<URL>();
117    for (String element : classpath.split(File.pathSeparator)) {
118      if (element.endsWith("/*")) {
119        String dir = element.substring(0, element.length() - 1);
120        File[] files = new File(dir).listFiles(JAR_FILENAME_FILTER);
121        if (files != null) {
122          for (File file : files) {
123            urls.add(file.toURI().toURL());
124          }
125        }
126      } else {
127        File file = new File(element);
128        if (file.exists()) {
129          urls.add(new File(element).toURI().toURL());
130        }
131      }
132    }
133    return urls.toArray(new URL[urls.size()]);
134  }
135
136  @Override
137  public URL getResource(String name) {
138    URL url = null;
139    
140    if (!isSystemClass(name, systemClasses)) {
141      url= findResource(name);
142      if (url == null && name.startsWith("/")) {
143        if (LOG.isDebugEnabled()) {
144          LOG.debug("Remove leading / off " + name);
145        }
146        url= findResource(name.substring(1));
147      }
148    }
149
150    if (url == null) {
151      url= parent.getResource(name);
152    }
153
154    if (url != null) {
155      if (LOG.isDebugEnabled()) {
156        LOG.debug("getResource("+name+")=" + url);
157      }
158    }
159    
160    return url;
161  }
162
163  @Override
164  public Class<?> loadClass(String name) throws ClassNotFoundException {
165    return this.loadClass(name, false);
166  }
167
168  @Override
169  protected synchronized Class<?> loadClass(String name, boolean resolve)
170      throws ClassNotFoundException {
171    
172    if (LOG.isDebugEnabled()) {
173      LOG.debug("Loading class: " + name);
174    }
175
176    Class<?> c = findLoadedClass(name);
177    ClassNotFoundException ex = null;
178
179    if (c == null && !isSystemClass(name, systemClasses)) {
180      // Try to load class from this classloader's URLs. Note that this is like
181      // the servlet spec, not the usual Java 2 behaviour where we ask the
182      // parent to attempt to load first.
183      try {
184        c = findClass(name);
185        if (LOG.isDebugEnabled() && c != null) {
186          LOG.debug("Loaded class: " + name + " ");
187        }
188      } catch (ClassNotFoundException e) {
189        if (LOG.isDebugEnabled()) {
190          LOG.debug(e);
191        }
192        ex = e;
193      }
194    }
195
196    if (c == null) { // try parent
197      c = parent.loadClass(name);
198      if (LOG.isDebugEnabled() && c != null) {
199        LOG.debug("Loaded class from parent: " + name + " ");
200      }
201    }
202
203    if (c == null) {
204      throw ex != null ? ex : new ClassNotFoundException(name);
205    }
206
207    if (resolve) {
208      resolveClass(c);
209    }
210
211    return c;
212  }
213
214  /**
215   * Checks if a class should be included as a system class.
216   *
217   * A class is a system class if and only if it matches one of the positive
218   * patterns and none of the negative ones.
219   *
220   * @param name the class name to check
221   * @param systemClasses a list of system class configurations.
222   * @return true if the class is a system class
223   */
224  public static boolean isSystemClass(String name, List<String> systemClasses) {
225    boolean result = false;
226    if (systemClasses != null) {
227      String canonicalName = name.replace('/', '.');
228      while (canonicalName.startsWith(".")) {
229        canonicalName=canonicalName.substring(1);
230      }
231      for (String c : systemClasses) {
232        boolean shouldInclude = true;
233        if (c.startsWith("-")) {
234          c = c.substring(1);
235          shouldInclude = false;
236        }
237        if (canonicalName.startsWith(c)) {
238          if (   c.endsWith(".")                                   // package
239              || canonicalName.length() == c.length()              // class
240              ||    canonicalName.length() > c.length()            // nested
241                 && canonicalName.charAt(c.length()) == '$' ) {
242            if (shouldInclude) {
243              result = true;
244            } else {
245              return false;
246            }
247          }
248        }
249      }
250    }
251    return result;
252  }
253}