/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.preloading;
import java.io.*;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@SuppressWarnings("unchecked")
public class ClassPreloadingUtils {
/**
* Creates a class loader that loads all classes from {@code jarFiles} into memory to make loading faster (avoid skipping through zip archives).
*
* @param jarFiles jars to load all classes from
* @param classCountEstimation an estimated number of classes in a the jars
* @param parentClassLoader parent class loader
* @param handler handler to be notified on class definitions done by this class loader, or null
* @param classesToLoadByParent condition to load some classes via parent class loader
* @return a class loader that reads classes from memory
* @throws IOException on from reading the jar
*/
public static ClassLoader preloadClasses(
Collection<File> jarFiles,
int classCountEstimation,
ClassLoader parentClassLoader,
ClassCondition classesToLoadByParent,
ClassHandler handler
) throws IOException {
Map<String, Object> entries = loadAllClassesFromJars(jarFiles, classCountEstimation, handler);
Collection<File> classpath = mergeClasspathFromManifests(entries);
if (!classpath.isEmpty()) {
parentClassLoader = preloadClasses(classpath, classCountEstimation, parentClassLoader, null, handler);
}
return new MemoryBasedClassLoader(classesToLoadByParent, parentClassLoader, entries, handler, createFallbackClassLoader(jarFiles));
}
private static URLClassLoader createFallbackClassLoader(Collection<File> files) throws IOException {
List<URL> urls = new ArrayList<>(files.size());
for (File file : files) {
urls.add(file.toURI().toURL());
}
return new URLClassLoader(urls.toArray(new URL[urls.size()]), null);
}
public static ClassLoader preloadClasses(
Collection<File> jarFiles, int classCountEstimation, ClassLoader parentClassLoader, ClassCondition classesToLoadByParent
) throws IOException {
return preloadClasses(jarFiles, classCountEstimation, parentClassLoader, classesToLoadByParent, null);
}
private static Collection<File> mergeClasspathFromManifests(Map<String, Object> preloadedResources) throws IOException {
Object manifest = preloadedResources.get(JarFile.MANIFEST_NAME);
if (manifest instanceof ResourceData) {
return extractManifestClasspath((ResourceData) manifest);
}
else if (manifest instanceof ArrayList) {
List<File> result = new ArrayList<>();
for (ResourceData data : (ArrayList<ResourceData>) manifest) {
result.addAll(extractManifestClasspath(data));
}
return result;
}
else {
assert manifest == null : "Resource map should contain ResourceData or ArrayList<ResourceData>: " + manifest;
return Collections.emptyList();
}
}
private static Collection<File> extractManifestClasspath(ResourceData manifestData) throws IOException {
Manifest manifest = new Manifest(new ByteArrayInputStream(manifestData.bytes));
String classpathSpaceSeparated = (String) manifest.getMainAttributes().get(Attributes.Name.CLASS_PATH);
if (classpathSpaceSeparated == null) return Collections.emptyList();
Collection<File> classpath = new ArrayList<>(1);
for (String jar : classpathSpaceSeparated.split(" ")) {
if (".".equals(jar)) continue;
if (!jar.endsWith(".jar")) {
throw new UnsupportedOperationException("Class-Path attribute should only contain paths to JAR files: " + jar);
}
classpath.add(new File(manifestData.jarFile.getParent(), jar));
}
return classpath;
}
/**
* @return a map of name to resources. Each value is either a ResourceData if there's only one instance (in the vast majority of cases)
* or a non-empty ArrayList of ResourceData if there's many
*/
private static Map<String, Object> loadAllClassesFromJars(
Collection<File> jarFiles,
int classNumberEstimate,
ClassHandler handler
) throws IOException {
// 0.75 is HashMap.DEFAULT_LOAD_FACTOR
Map<String, Object> resources = new HashMap<>((int) (classNumberEstimate / 0.75));
for (File jarFile : jarFiles) {
if (handler != null) {
handler.beforeLoadJar(jarFile);
}
FileInputStream fileInputStream = new FileInputStream(jarFile);
try {
byte[] buffer = new byte[10 * 1024];
ZipInputStream stream = new ZipInputStream(new BufferedInputStream(fileInputStream, 1 << 19));
while (true) {
ZipEntry entry = stream.getNextEntry();
if (entry == null) break;
if (entry.isDirectory()) continue;
int size = (int) entry.getSize();
int effectiveSize = size < 0 ? 32 : size;
ByteArrayOutputStream bytes = new ByteArrayOutputStream(effectiveSize);
int count;
while ((count = stream.read(buffer)) > 0) {
bytes.write(buffer, 0, count);
}
String name = entry.getName();
byte[] data = bytes.toByteArray();
if (handler != null) {
data = handler.instrument(name, data);
}
ResourceData resourceData = new ResourceData(jarFile, name, data);
Object previous = resources.get(name);
if (previous == null) {
resources.put(name, resourceData);
}
else if (previous instanceof ResourceData) {
List<ResourceData> list = new ArrayList<>();
list.add((ResourceData) previous);
list.add(resourceData);
resources.put(name, list);
}
else {
assert previous instanceof ArrayList :
"Resource map should contain ResourceData or ArrayList<ResourceData>: " + name;
((ArrayList<ResourceData>) previous).add(resourceData);
}
}
}
finally {
try {
fileInputStream.close();
}
catch (IOException e) {
// Ignore
}
}
if (handler != null) {
handler.afterLoadJar(jarFile);
}
}
for (Object value : resources.values()) {
if (value instanceof ArrayList) {
((ArrayList) value).trimToSize();
}
}
return resources;
}
}