/*
* Copyright 2015 MovingBlocks
*
* 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.terasology.module;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.SetMultimap;
import org.reflections.Reflections;
import org.reflections.ReflectionsException;
import org.reflections.util.ConfigurationBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.module.filesystem.ModuleFileSystemProvider;
import org.terasology.module.sandbox.BytecodeInjector;
import org.terasology.module.sandbox.ModuleClassLoader;
import org.terasology.module.sandbox.ObtainClassloader;
import org.terasology.module.sandbox.PermissionProviderFactory;
import org.terasology.naming.Name;
import javax.annotation.Nullable;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
* An environment composed of a set of modules. A chain of class loaders is created for each module that isn't on the classpath, such that dependencies appear before
* dependants. Classes of interest can then be discovered by the types they inherit or annotations they have.
* <p>
* When the environment is no longer in use it should be closed - this closes all the class loaders. Memory used by the ClassLoaders will then be available for garbage
* collection once the last instance of a class loaded from it is freed.
* </p>
*
* @author Immortius
*/
public class ModuleEnvironment implements AutoCloseable, Iterable<Module> {
private static final Logger logger = LoggerFactory.getLogger(ModuleEnvironment.class);
private final ImmutableMap<Name, Module> modules;
private final ClassLoader apiClassLoader;
private final ClassLoader finalClassLoader;
private final ImmutableList<ModuleClassLoader> managedClassLoaders;
private final ImmutableSetMultimap<Name, Name> moduleDependencies;
private final Reflections fullReflections;
private final ImmutableList<Module> modulesOrderByDependencies;
private final ImmutableList<Name> moduleIdsOrderedByDependencies;
private final FileSystem fileSystem;
/**
* @param modules The modules this environment should encompass.
* @param permissionProviderFactory A factory for producing a PermissionProvider for each loaded module
* @throws java.lang.IllegalArgumentException if the Iterable contains multiple modules with the same id.
*/
public ModuleEnvironment(Iterable<Module> modules, PermissionProviderFactory permissionProviderFactory) {
this(modules, permissionProviderFactory, Collections.<BytecodeInjector>emptyList());
}
/**
* @param modules The modules this environment should encompass.
* @param permissionProviderFactory A factory for producing a PermissionProvider for each loaded module
* @param injectors Any Bytecode Injectors that should be run over any loaded module class.
* @throws java.lang.IllegalArgumentException if the Iterable contains multiple modules with the same id.
*/
public ModuleEnvironment(Iterable<Module> modules, PermissionProviderFactory permissionProviderFactory, Iterable<BytecodeInjector> injectors) {
this(modules, permissionProviderFactory, injectors, ModuleEnvironment.class.getClassLoader());
}
/**
* @param modules The modules this environment should encompass.
* @param permissionProviderFactory A factory for producing a PermissionProvider for each loaded module
* @param injectors Any Bytecode Injectors that should be run over any loaded module class.
* @param apiClassLoader The base classloader the module environment should build upon.
* @throws java.lang.IllegalArgumentException if the Iterable contains multiple modules with the same id.
*/
public ModuleEnvironment(Iterable<Module> modules, final PermissionProviderFactory permissionProviderFactory,
final Iterable<BytecodeInjector> injectors, ClassLoader apiClassLoader) {
Map<Name, Reflections> reflectionsByModule = Maps.newLinkedHashMap();
this.modules = buildModuleMap(modules);
this.apiClassLoader = apiClassLoader;
this.modulesOrderByDependencies = calculateModulesOrderedByDependencies();
this.moduleIdsOrderedByDependencies = ImmutableList.copyOf(Collections2.transform(modulesOrderByDependencies, new Function<Module, Name>() {
@Override
public Name apply(Module input) {
return input.getId();
}
}));
ImmutableList.Builder<ModuleClassLoader> managedClassLoaderListBuilder = ImmutableList.builder();
ClassLoader lastClassLoader = apiClassLoader;
List<Module> orderedModules = getModulesOrderedByDependencies();
for (final Module module : orderedModules) {
if (module.isCodeModule()) {
Optional<ModuleClassLoader> classLoader = determineClassloaderFor(module, lastClassLoader, permissionProviderFactory, injectors);
if (classLoader.isPresent()) {
managedClassLoaderListBuilder.add(classLoader.get());
lastClassLoader = classLoader.get();
}
reflectionsByModule.put(module.getId(), module.getReflectionsFragment());
}
}
this.finalClassLoader = lastClassLoader;
this.fullReflections = buildFullReflections(reflectionsByModule);
this.managedClassLoaders = managedClassLoaderListBuilder.build();
this.moduleDependencies = buildModuleDependencies();
this.fileSystem = new ModuleFileSystemProvider().newFileSystem(this);
}
/**
* Builds a map of modules, keyed by id, from an iterable.
*
* @param moduleList The list of modules to map
* @return The final map
*/
private ImmutableMap<Name, Module> buildModuleMap(Iterable<Module> moduleList) {
ImmutableMap.Builder<Name, Module> builder = ImmutableMap.builder();
for (Module module : moduleList) {
builder.put(module.getId(), module);
}
return builder.build();
}
/**
* @param module The module to determine the classloader for
* @param parent The classloader to parent any new classloader off of
* @param permissionProviderFactory The provider of api information
* @param injectors Any Bytecode Injectors that should be run over any loaded module class.
* @return The new module classloader to use for this module, or absent if the parent classloader should be used.
*/
private Optional<ModuleClassLoader> determineClassloaderFor(final Module module, final ClassLoader parent,
final PermissionProviderFactory permissionProviderFactory, final Iterable<BytecodeInjector> injectors) {
if (!module.isOnClasspath()) {
ModuleClassLoader moduleClassloader = AccessController.doPrivileged(new PrivilegedAction<ModuleClassLoader>() {
@Override
public ModuleClassLoader run() {
return new ModuleClassLoader(module.getId(), module.getClasspaths().toArray(new URL[module.getClasspaths().size()]),
parent, permissionProviderFactory.createPermissionProviderFor(module), injectors);
}
});
return Optional.of(moduleClassloader);
} else {
return Optional.empty();
}
}
/**
* Builds Reflections information over the entire module environment, combining the information of all individual modules
*
* @param reflectionsByModule A map of reflection information for each module
*/
private Reflections buildFullReflections(Map<Name, Reflections> reflectionsByModule) {
ConfigurationBuilder fullBuilder = new ConfigurationBuilder()
.addClassLoader(apiClassLoader)
.addClassLoader(finalClassLoader);
Reflections reflections = new Reflections(fullBuilder);
for (Reflections moduleReflection : reflectionsByModule.values()) {
reflections.merge(moduleReflection);
}
return reflections;
}
private ImmutableSetMultimap<Name, Name> buildModuleDependencies() {
SetMultimap<Name, Name> moduleDependenciesBuilder = HashMultimap.create();
for (Module module : getModulesOrderedByDependencies()) {
for (DependencyInfo dependency : module.getMetadata().getDependencies()) {
moduleDependenciesBuilder.put(module.getId(), dependency.getId());
moduleDependenciesBuilder.putAll(module.getId(), moduleDependenciesBuilder.get(dependency.getId()));
}
}
return ImmutableSetMultimap.copyOf(moduleDependenciesBuilder);
}
private ImmutableList<Module> calculateModulesOrderedByDependencies() {
List<Module> result = Lists.newArrayList();
List<Module> alphabeticallyOrderedModules = Lists.newArrayList(modules.values());
Collections.sort(alphabeticallyOrderedModules, new Comparator<Module>() {
@Override
public int compare(Module o1, Module o2) {
return o1.getId().compareTo(o2.getId());
}
});
for (Module module : alphabeticallyOrderedModules) {
addModuleAfterDependencies(module, result);
}
return ImmutableList.copyOf(result);
}
private void addModuleAfterDependencies(Module module, List<Module> out) {
if (!out.contains(module)) {
List<Name> dependencies = Lists.newArrayList(Collections2.transform(module.getMetadata().getDependencies(), new Function<DependencyInfo, Name>() {
@Nullable
@Override
public Name apply(@Nullable DependencyInfo input) {
if (input != null) {
return input.getId();
}
return null;
}
}));
Collections.sort(dependencies);
for (Name dependency : dependencies) {
Module dependencyModule = modules.get(dependency);
if (dependencyModule != null) {
addModuleAfterDependencies(dependencyModule, out);
}
}
out.add(module);
}
}
@Override
public void close() {
for (ModuleClassLoader classLoader : managedClassLoaders) {
try {
classLoader.close();
} catch (IOException e) {
logger.error("Failed to close classLoader for module '" + classLoader.getModuleId() + "'", e);
}
}
}
/**
* @param id The id of the module to return
* @return The desired module, or null if it is not part of the environment
*/
public Module get(Name id) {
return modules.get(id);
}
/**
* The resulting list is sorted so that dependencies appear before modules that depend on them. Additionally,
* modules are alphabetically ordered where there are no dependencies.
*
* @return A list of modules in the environment, sorted so any dependencies appear before a module
*/
public final List<Module> getModulesOrderedByDependencies() {
return modulesOrderByDependencies;
}
/**
* @return A list of modules in the environment, sorted so any dependencies appear before a module
*/
public final List<Name> getModuleIdsOrderedByDependencies() {
return moduleIdsOrderedByDependencies;
}
/**
* Determines the module from which the give class originates from.
*
* @param type The type to find the module for
* @return The module providing the class, or null if it doesn't come from a module.
*/
public Name getModuleProviding(Class<?> type) {
ClassLoader classLoader = AccessController.doPrivileged(new ObtainClassloader(type));
if (classLoader instanceof ModuleClassLoader) {
return ((ModuleClassLoader) classLoader).getModuleId();
}
try {
Path sourceLocation = Paths.get(type.getProtectionDomain().getCodeSource().getLocation().toURI());
for (Module module : modules.values()) {
if (module.isCodeModule()) {
for (URL classpath : module.getClasspaths()) {
if (Paths.get(classpath.toURI()).equals(sourceLocation)) {
return module.getId();
}
}
}
}
} catch (URISyntaxException e) {
logger.error("Failed to convert url to uri for comparison", e);
}
return null;
}
/**
* @param moduleId The id of the module to get the dependencies
* @return The ids of the dependencies of the desired module
*/
public Set<Name> getDependencyNamesOf(Name moduleId) {
return moduleDependencies.get(moduleId);
}
/**
* @param type The type to find subtypes of
* @param <U> The type to find subtypes of
* @return A Iterable over all subtypes of type that appear in the module environment
*/
public <U> Iterable<Class<? extends U>> getSubtypesOf(Class<U> type) {
try {
return fullReflections.getSubTypesOf(type);
} catch (ReflectionsException e) {
throw new ReflectionsException("Could not obtain subtypes of '" + type + "' - possible subclass without permission", e);
}
}
/**
* @param type The type to find subtypes of
* @param <U> The type to find subtypes of
* @param filter A filter to apply to the returned subtypes
* @return A Iterable over all subtypes of type that appear in the module environment
*/
public <U> Iterable<Class<? extends U>> getSubtypesOf(Class<U> type, Predicate<Class<?>> filter) {
return Collections2.filter(fullReflections.getSubTypesOf(type), filter);
}
/**
* @param annotation The annotation of interest
* @return All types in the environment that are either marked by the given annotation, or are subtypes of a type marked with the annotation if the annotation is marked
* as @Inherited
*/
public Iterable<Class<?>> getTypesAnnotatedWith(Class<? extends Annotation> annotation) {
return fullReflections.getTypesAnnotatedWith(annotation, true);
}
/**
* @param annotation The annotation of interest
* @param filter Further filter on the returned types
* @return All types in the environment that are either marked by the given annotation, or are subtypes of a type marked with the annotation if the annotation is marked
* as @Inherited
*/
public Iterable<Class<?>> getTypesAnnotatedWith(Class<? extends Annotation> annotation, Predicate<Class<?>> filter) {
return Collections2.filter(fullReflections.getTypesAnnotatedWith(annotation, true), filter);
}
@Override
public Iterator<Module> iterator() {
return modules.values().iterator();
}
public FileSystem getFileSystem() {
return fileSystem;
}
}