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