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    if (LOG.isDebugEnabled()) {
098      LOG.debug("urls: " + Arrays.toString(urls));
099      LOG.debug("system classes: " + systemClasses);
100    }
101    this.parent = parent;
102    if (parent == null) {
103      throw new IllegalArgumentException("No parent classloader!");
104    }
105    // if the caller-specified system classes are null or empty, use the default
106    this.systemClasses = (systemClasses == null || systemClasses.isEmpty()) ?
107        Arrays.asList(StringUtils.getTrimmedStrings(SYSTEM_CLASSES_DEFAULT)) :
108        systemClasses;
109    LOG.info("system classes: " + this.systemClasses);
110  }
111
112  public ApplicationClassLoader(String classpath, ClassLoader parent,
113      List<String> systemClasses) throws MalformedURLException {
114    this(constructUrlsFromClasspath(classpath), parent, systemClasses);
115  }
116
117  static URL[] constructUrlsFromClasspath(String classpath)
118      throws MalformedURLException {
119    List<URL> urls = new ArrayList<URL>();
120    for (String element : classpath.split(File.pathSeparator)) {
121      if (element.endsWith("/*")) {
122        String dir = element.substring(0, element.length() - 1);
123        File[] files = new File(dir).listFiles(JAR_FILENAME_FILTER);
124        if (files != null) {
125          for (File file : files) {
126            urls.add(file.toURI().toURL());
127          }
128        }
129      } else {
130        File file = new File(element);
131        if (file.exists()) {
132          urls.add(new File(element).toURI().toURL());
133        }
134      }
135    }
136    return urls.toArray(new URL[urls.size()]);
137  }
138
139  @Override
140  public URL getResource(String name) {
141    URL url = null;
142    
143    if (!isSystemClass(name, systemClasses)) {
144      url= findResource(name);
145      if (url == null && name.startsWith("/")) {
146        if (LOG.isDebugEnabled()) {
147          LOG.debug("Remove leading / off " + name);
148        }
149        url= findResource(name.substring(1));
150      }
151    }
152
153    if (url == null) {
154      url= parent.getResource(name);
155    }
156
157    if (url != null) {
158      if (LOG.isDebugEnabled()) {
159        LOG.debug("getResource("+name+")=" + url);
160      }
161    }
162    
163    return url;
164  }
165
166  @Override
167  public Class<?> loadClass(String name) throws ClassNotFoundException {
168    return this.loadClass(name, false);
169  }
170
171  @Override
172  protected synchronized Class<?> loadClass(String name, boolean resolve)
173      throws ClassNotFoundException {
174    
175    if (LOG.isDebugEnabled()) {
176      LOG.debug("Loading class: " + name);
177    }
178
179    Class<?> c = findLoadedClass(name);
180    ClassNotFoundException ex = null;
181
182    if (c == null && !isSystemClass(name, systemClasses)) {
183      // Try to load class from this classloader's URLs. Note that this is like
184      // the servlet spec, not the usual Java 2 behaviour where we ask the
185      // parent to attempt to load first.
186      try {
187        c = findClass(name);
188        if (LOG.isDebugEnabled() && c != null) {
189          LOG.debug("Loaded class: " + name + " ");
190        }
191      } catch (ClassNotFoundException e) {
192        if (LOG.isDebugEnabled()) {
193          LOG.debug(e);
194        }
195        ex = e;
196      }
197    }
198
199    if (c == null) { // try parent
200      c = parent.loadClass(name);
201      if (LOG.isDebugEnabled() && c != null) {
202        LOG.debug("Loaded class from parent: " + name + " ");
203      }
204    }
205
206    if (c == null) {
207      throw ex != null ? ex : new ClassNotFoundException(name);
208    }
209
210    if (resolve) {
211      resolveClass(c);
212    }
213
214    return c;
215  }
216
217  /**
218   * Checks if a class should be included as a system class.
219   *
220   * A class is a system class if and only if it matches one of the positive
221   * patterns and none of the negative ones.
222   *
223   * @param name the class name to check
224   * @param systemClasses a list of system class configurations.
225   * @return true if the class is a system class
226   */
227  public static boolean isSystemClass(String name, List<String> systemClasses) {
228    boolean result = false;
229    if (systemClasses != null) {
230      String canonicalName = name.replace('/', '.');
231      while (canonicalName.startsWith(".")) {
232        canonicalName=canonicalName.substring(1);
233      }
234      for (String c : systemClasses) {
235        boolean shouldInclude = true;
236        if (c.startsWith("-")) {
237          c = c.substring(1);
238          shouldInclude = false;
239        }
240        if (canonicalName.startsWith(c)) {
241          if (   c.endsWith(".")                                   // package
242              || canonicalName.length() == c.length()              // class
243              ||    canonicalName.length() > c.length()            // nested
244                 && canonicalName.charAt(c.length()) == '$' ) {
245            if (shouldInclude) {
246              result = true;
247            } else {
248              return false;
249            }
250          }
251        }
252      }
253    }
254    return result;
255  }
256}