/*
* Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 3 of the License, or (at your option)
* any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, see http://www.gnu.org/licenses/
*/
package com.bc.ceres.core.runtime.internal;
import com.bc.ceres.core.Assert;
import com.bc.ceres.core.runtime.*;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* A strategy which resolves module dependencies.
*/
public class ModuleResolver {
private ClassLoader moduleParentClassLoader;
private boolean resolvingLibs;
private Stack<String> moduleStack;
/**
* Constructs a new module resolver.
*
* @param moduleParentClassLoader the parent class loader for the module
* @param resolvingLibs if true, libs are resolved
*/
public ModuleResolver(ClassLoader moduleParentClassLoader, boolean resolvingLibs) {
Assert.notNull(moduleParentClassLoader, "moduleParentClassLoader");
this.moduleParentClassLoader = moduleParentClassLoader;
this.resolvingLibs = resolvingLibs;
this.moduleStack = new Stack<String>();
}
public void resolve(ModuleImpl module) throws ResolveException {
Assert.notNull(module, "module");
ModuleState previousState = module.getState();
initModuleDependencies(module);
if (module.getState() == ModuleState.RESOLVED) {
initModuleClassLoader(module);
initRefCount(module);
}
if (module.hasResolveErrors()) {
module.setState(previousState);
String msg = String.format("Failed to resolve module [%s].", module.getSymbolicName());
throw new ResolveException(msg);
}
}
static class DependencyItem {
ModuleImpl module;
boolean optional;
public DependencyItem(ModuleImpl module, boolean optional) {
this.module = module;
this.optional = optional;
}
}
private void initModuleDependencies(ModuleImpl module) {
if (module.hasResolveErrors()) {
return;
}
if (module.getState() == ModuleState.INSTALLED) {
if (module.getModuleDependencies() == null) {
ModuleImpl[] resolvedModules = resolveModuleDependencies(module);
module.setModuleDependencies(resolvedModules);
}
if (module.getDeclaredLibs() == null) {
String[] declaredLibs = findDeclaredLibs(module);
module.setDeclaredLibs(declaredLibs);
}
if (module.getLibDependencies() == null) {
URL[] libDependencies = findLibDependencies(module);
module.setLibDependencies(libDependencies);
}
if (!module.hasResolveErrors()) {
module.setState(ModuleState.RESOLVED);
}
}
}
private ModuleImpl[] resolveModuleDependencies(ModuleImpl module) {
String moduleKey = module.getSymbolicName() + ":" + module.getVersion();
if (moduleStack.contains(moduleKey)) {
String message = createCyclicDependecyExceptionMessage(module);
module.addResolveError(new ResolveException(message));
return new ModuleImpl[0];
}
try {
moduleStack.push(moduleKey);
return resolveModuleDependenciesImpl(module);
} finally {
moduleStack.pop();
}
}
private String createCyclicDependecyExceptionMessage(ModuleImpl module) {
StringBuilder trace = new StringBuilder();
for (String s : moduleStack) {
trace.append('[').append(s).append(']');
}
return MessageFormat.format("Cyclic dependencies detected for module [{0}], trace: {1}",
module.getSymbolicName(), trace);
}
private ModuleImpl[] resolveModuleDependenciesImpl(ModuleImpl module) {
DependencyItem[] moduleDependencies = findModuleDependencies(module);
List<ModuleImpl> resolvedModules = new ArrayList<ModuleImpl>(moduleDependencies.length);
for (DependencyItem dependencyItem : moduleDependencies) {
initModuleDependencies(dependencyItem.module);
if (dependencyItem.module.getState() == ModuleState.RESOLVED) {
resolvedModules.add(dependencyItem.module);
}
ResolveException[] resolveErrors = dependencyItem.module.getResolveErrors();
if (resolveErrors.length > 0) {
for (ResolveException resolveError : resolveErrors) {
if (dependencyItem.optional) {
module.addResolveWarning(resolveError);
} else {
module.addResolveError(resolveError);
}
}
}
ResolveException[] resolveWarnings = dependencyItem.module.getResolveWarnings();
if (resolveWarnings.length > 0) {
for (ResolveException resolveWarning : resolveWarnings) {
module.addResolveWarning(resolveWarning);
}
}
}
return resolvedModules.toArray(new ModuleImpl[resolvedModules.size()]);
}
private void initModuleClassLoader(ModuleImpl module) {
if (module.getClassLoader() == null) {
URL[] nativeLibs = getNativeLibs(module);
URL[] dependencyLibs = module.getLibDependencies();
if (dependencyLibs == null) {
dependencyLibs = new URL[0];
}
ClassLoader[] dependencyClassLoaders = getDependencyClassLoaders(module);
module.setClassLoader(new ModuleClassLoader(dependencyClassLoaders, dependencyLibs,
nativeLibs, moduleParentClassLoader));
}
}
private ClassLoader[] getDependencyClassLoaders(ModuleImpl module) {
List<ClassLoader> dependencyCl = new ArrayList<ClassLoader>();
for (ModuleImpl moduleDependency : module.getModuleDependencies()) {
if (moduleDependency.getState() == ModuleState.RESOLVED) {
if (moduleDependency.getClassLoader() == null) {
// Enter recursion
initModuleClassLoader(moduleDependency);
}
dependencyCl.add(moduleDependency.getClassLoader());
}
}
return dependencyCl.toArray(new ClassLoader[dependencyCl.size()]);
}
private static URL[] getNativeLibs(ModuleImpl module) {
List<URL> libPaths = new ArrayList<URL>();
if (module.isNative()) {
File moduleDir = UrlHelper.urlToFile(module.getLocation());
if (moduleDir.isDirectory()) {
String[] impliciteNativeLibs = module.getImpliciteNativeLibs();
for (String libPath : impliciteNativeLibs) {
File libFile = new File(moduleDir, libPath);
if (libFile.isFile() && libFile.canRead()) {
libPaths.add(UrlHelper.fileToUrl(libFile));
} else {
String msg = String.format("Native library [%s] found in module [%s] is not accessible.",
libFile, module.getSymbolicName());
module.addResolveWarning(new ResolveException(msg));
}
}
}
}
return libPaths.toArray(new URL[libPaths.size()]);
}
private static void initRefCount(ModuleImpl module) {
module.incrementRefCount();
if (module.getModuleDependencies() != null) {
ModuleImpl[] moduleDependencies = module.getModuleDependencies();
for (ModuleImpl moduleDependency : moduleDependencies) {
// enter recursion
initRefCount(moduleDependency);
}
}
}
private URL[] findLibDependencies(ModuleImpl module) {
if (module.getLocation() == null) {
throw new IllegalStateException("module.getLocation() == null");
}
if (module.getModuleDependencies() == null) {
throw new IllegalStateException("module.getModuleDependencies() == null");
}
if (module.getDeclaredLibs() == null) {
throw new IllegalStateException("module.getDeclaredLibs() == null");
}
if (module.getImpliciteLibs() == null) {
throw new IllegalStateException("module.getImpliciteLibs() == null");
}
File moduleFile = UrlHelper.urlToFile(module.getLocation());
if (moduleFile == null) {
return new URL[0];
}
List<URL> libDependencies = new ArrayList<URL>(16);
// add this module to the classpath
collectLibDependency(module, moduleFile, libDependencies);
// add all declared libs to the classpath
resolveLibs(module,
moduleFile, resolvingLibs, libDependencies);
// add all implicite libs to the classpath
for (String impliciteLib : module.getImpliciteLibs()) {
collectLibDependency(module, new File(moduleFile, impliciteLib), libDependencies);
}
return libDependencies.toArray(new URL[0]);
}
private static void resolveLibs(ModuleImpl module, File moduleFile,
boolean resolvingLibs, List<URL> libDependencies) {
Dependency[] declaredDependencies = module.getDeclaredDependencies();
for (Dependency dependency : declaredDependencies) {
if (dependency.getLibName() != null) {
boolean libResolved = false;
// look in this modules location
File file = resolveFile(moduleFile, dependency.getLibName(), resolvingLibs);
if (file != null) {
collectLibDependency(module, file, libDependencies);
libResolved = true;
} else {
for (ModuleImpl moduleDependency : module.getModuleDependencies()) {
// look in the modules dependencies' location
File moduleDependencyFile = UrlHelper.urlToFile(moduleDependency.getLocation());
if (moduleDependencyFile != null) {
File file2 = resolveFile(moduleDependencyFile, dependency.getLibName(), resolvingLibs);
if (file2 != null) {
collectLibDependency(module, file2, libDependencies);
libResolved = true;
break;
}
}
}
}
// library is not resolved and we must resolve dependecies
if (!libResolved && resolvingLibs) {
if (!dependency.isOptional()) {
String msg = String.format("Mandatory library [%s] declared by module [%s] not found.",
dependency.getLibName(),
module.getSymbolicName());
module.addResolveError(new ResolveException(msg));
}
}
}
}
}
private static File resolveFile(File parent, String libPath, boolean checkExists) {
if (parent.isDirectory()) {
File file = new File(parent, libPath);
if (!checkExists || file.exists()) {
return file;
}
}
return null;
}
private static DependencyItem[] findModuleDependencies(ModuleImpl module) {
List<DependencyItem> list = new ArrayList<DependencyItem>(16);
collectDeclaredModuleDependencies(module, list);
collectImpliciteModuleDependencies(module, list);
return list.toArray(new DependencyItem[0]);
}
private static void collectDeclaredModuleDependencies(ModuleImpl module, List<DependencyItem> list) {
Dependency[] dependencies = module.getDeclaredDependencies();
for (Dependency dependency : dependencies) {
if (dependency.getModuleSymbolicName() != null) {
ModuleImpl[] dependencyModules = module.getRegistry().getModules(dependency.getModuleSymbolicName());
if (dependencyModules.length > 0) {
if (dependency.getVersion() != null) {
Version requiredVersion = Version.parseVersion(dependency.getVersion());
ModuleImpl dependencyModule = findBestMatchingModuleVersion(requiredVersion, dependencyModules);
if (dependencyModule != null && requiredVersion.compareTo(dependencyModule.getVersion()) <= 0) {
collectDependencyModule(module, dependencyModule, dependency.isOptional(), list);
} else if (!dependency.isOptional()) {
String msg = String.format(
"Mandatory dependency [%s:%s] declared by module [%s] not found.",
dependency.getModuleSymbolicName(),
dependency.getVersion(),
module.getSymbolicName());
module.addResolveError(new ResolveException(msg));
}
} else {
if (dependencyModules.length > 0) {
ModuleImpl dependencyModule = findLatestModuleVersion(dependencyModules);
collectDependencyModule(module, dependencyModule, dependency.isOptional(), list);
}
}
} else if (!dependency.isOptional()) {
String msg = String.format("Mandatory dependency [%s] declared by module [%s] not found.",
dependency.getModuleSymbolicName(),
module.getSymbolicName());
module.addResolveError(new ResolveException(msg));
}
}
}
}
private static void collectImpliciteModuleDependencies(ModuleImpl module, List<DependencyItem> list) {
Extension[] extensions = module.getExtensions();
for (Extension extension : extensions) {
ExtensionPoint extensionPoint = extension.getExtensionPoint();
if (extensionPoint != null) {
collectDependencyModule(module, (ModuleImpl) extensionPoint.getDeclaringModule(), true, list);
} else {
String msg = String.format(
"Extension point [%s] used by module [%s] not found. Extension will be ignored.",
extension.getPoint(),
module.getSymbolicName());
module.addResolveWarning(new ResolveException(msg));
}
}
}
private static ModuleImpl findLatestModuleVersion(ModuleImpl[] modules) {
ModuleImpl latestModule = modules[0];
Version latestVersion = latestModule.getVersion();
for (int i = 1; i < modules.length; i++) {
ModuleImpl module = modules[i];
Version version = module.getVersion();
if (version.compareTo(latestVersion) > 0) {
latestModule = module;
latestVersion = version;
}
}
return latestModule;
}
private static Version[] getVersions(Module[] modules) {
Version[] versions = new Version[modules.length];
for (int i = 0; i < modules.length; i++) {
versions[i] = modules[i].getVersion();
}
return versions;
}
private static ModuleImpl findBestMatchingModuleVersion(Version requiredVersion, ModuleImpl[] modules) {
Version[] versions = getVersions(modules);
for (int i = 0; i < modules.length; i++) {
ModuleImpl module = modules[i];
if (versions[i].compareTo(requiredVersion) == 0) {
return module;
}
}
ModuleImpl bestModule = findLatestModuleVersion(modules);
Version bestVersion = bestModule.getVersion();
for (int i = 0; i < modules.length; i++) {
ModuleImpl module = modules[i];
Version version = versions[i];
if (version.compareTo(requiredVersion) == 0) {
bestModule = module;
break;
} else if (version.compareTo(requiredVersion) > 0
&& version.compareTo(bestVersion) < 0) {
bestModule = module;
bestVersion = version;
}
}
return bestModule;
}
private static String[] findDeclaredLibs(ModuleImpl module) {
Dependency[] dependencies = module.getDeclaredDependencies();
ArrayList<String> libNames = new ArrayList<String>(dependencies.length);
for (Dependency dependency : dependencies) {
// if dependency.getLibName() is not null we have a declared JAR dependency,
// otherwise it is expected that dependency.getModuleId() will return a non-null value
// which means we have a declared module dependency.
if (dependency.getLibName() != null) {
if (!libNames.contains(dependency.getLibName())) {
libNames.add(dependency.getLibName());
}
}
}
return libNames.toArray(new String[0]);
}
private static void collectDependencyModule(ModuleImpl module, ModuleImpl dependencyModule, boolean optional,
List<DependencyItem> dependencyItems) {
if (dependencyModule == module) {
return;
}
for (DependencyItem dependencyItem : dependencyItems) {
if (dependencyModule == dependencyItem.module) {
return;
}
}
dependencyItems.add(new DependencyItem(dependencyModule, optional));
}
private static void collectLibDependency(ModuleImpl module, File lib, List<URL> list) {
try {
URL url = convertToURL(lib);
if (!list.contains(url)) {
list.add(url);
}
} catch (MalformedURLException e) {
String msg = String.format("Library file path [%s] used by module [%s] cannot be converted to an URL.",
lib.getPath(),
module.getSymbolicName());
module.addResolveError(new ResolveException(msg, e));
}
}
private static URL convertToURL(File lib) throws MalformedURLException {
return lib.toURI().toURL();
}
}