blob: 5d40bbd5389157ef66c1e47088c96fb57848aa11 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.netbeans;
import java.io.*;
import java.util.*;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openide.util.*;
import org.openide.modules.*;
/** Static utility methods for use within this package.
* @author Jesse Glick
*/
public final class Util {
/** Log everything happening in the module system. */
public static final Logger err = Logger.getLogger("org.netbeans.core.modules"); // NOI18N
// Prevent accidental subclassing.
private Util() {
}
/** Similar to {@link NbBundle#getLocalizingSuffixes} but optimized.
* @since JST-PENDING: Called from InstalledFileLocatorImpl
*/
public static synchronized String[] getLocalizingSuffixesFast() {
return LocaleVariants.getLocalizingSuffixesFast();
}
/**
* Make a temporary copy of a JAR file.
*/
static File makeTempJar(File moduleFile) throws IOException {
String prefix = moduleFile.getName();
if (prefix.endsWith(".jar") || prefix.endsWith(".JAR")) { // NOI18N
prefix = prefix.substring(0, prefix.length() - 4);
}
if (prefix.length() < 3) prefix += '.';
if (prefix.length() < 3) prefix += '.';
if (prefix.length() < 3) prefix += '.';
String suffix = "-test.jar"; // NOI18N
File physicalModuleFile = File.createTempFile(prefix, suffix);
physicalModuleFile.deleteOnExit();
InputStream is = new FileInputStream(moduleFile);
try {
OutputStream os = new FileOutputStream(physicalModuleFile);
try {
byte[] buf = new byte[4096];
int i;
while ((i = is.read(buf)) != -1) {
os.write(buf, 0, i);
}
} finally {
os.close();
}
} finally {
is.close();
}
err.fine("Made " + physicalModuleFile);
return physicalModuleFile;
}
// XXX ought to be some way to get localized messages for these...
/** Check whether a simple dependency is met.
* Only applicable to Java dependencies.
*/
static boolean checkJavaDependency(Dependency dep) throws IllegalArgumentException {
// Note that "any" comparison is not possible for this type.
if (dep.getType() == Dependency.TYPE_JAVA) {
if (dep.getName().equals(Dependency.JAVA_NAME)) {
if (dep.getComparison() == Dependency.COMPARE_SPEC) {
return new SpecificationVersion(dep.getVersion()).compareTo(Dependency.JAVA_SPEC) <= 0;
} else {
return dep.getVersion().equals(Dependency.JAVA_IMPL);
}
} else {
if (dep.getComparison() == Dependency.COMPARE_SPEC) {
return new SpecificationVersion(dep.getVersion()).compareTo(Dependency.VM_SPEC) <= 0;
} else {
return dep.getVersion().equals(Dependency.VM_IMPL);
}
}
} else {
throw new IllegalArgumentException();
}
}
/** Check whether a package dependency is met.
* A classloader must be supplied to check in.
* @param dep a module dependency
* @param cl a package-accessible classloader
* @return true if a package dependency is met
* @throws IllegalArgumentException
* @since 2.14
*/
public static boolean checkPackageDependency(Dependency dep, ClassLoader cl) throws IllegalArgumentException {
if (dep.getType() != Dependency.TYPE_PACKAGE) {
throw new IllegalArgumentException("Not a package dependency"); // NOI18N
}
if (! (cl instanceof ProxyClassLoader) && cl != Util.class.getClassLoader()) {
throw new IllegalArgumentException("Not a package-accessible classloader: " + cl); // NOI18N
}
String name = dep.getName();
String version = dep.getVersion();
int comparison = dep.getComparison();
String packageName, sampleName;
int idx = name.indexOf('[');
if (idx == -1) {
packageName = name;
sampleName = null;
} else if (idx == 0) {
packageName = null;
sampleName = name.substring(1, name.length() - 1);
} else {
packageName = name.substring(0, idx);
sampleName = name.substring(idx + 1, name.length() - 1);
if (sampleName.indexOf('.') == -1) {
// Unqualified class name; prefix it automatically.
sampleName = packageName + '.' + sampleName;
}
}
if (sampleName != null) {
try {
cl.loadClass(sampleName);
} catch (ClassNotFoundException cnfe) {
if (packageName == null) {
// This was all we were relying on, so it is an error.
err.log(Level.FINE, null, cnfe);
err.fine("Probed class could not be found");
return false;
}
// Else let the regular package check take care of it;
// this was only run to enforce that the package defs were loaded.
} catch (RuntimeException e) {
// SecurityException, etc. Package exists but is corrupt.
err.log(Level.WARNING, null, e);
err.fine("Assuming package " + packageName + " is corrupt");
return false;
} catch (LinkageError le) {
// NoClassDefFoundError, etc. Package exists but is corrupt.
err.log(Level.WARNING, null, le);
err.fine("Assuming package " + packageName + " is corrupt");
return false;
}
}
if (packageName != null) {
Package pkg;
if (cl instanceof ProxyClassLoader) {
pkg = ((ProxyClassLoader) cl).getPackage(packageName);
} else {
pkg = Package.getPackage(packageName);
}
if (pkg == null) {
err.fine("No package with the name " + packageName + " found");
return false;
}
if (comparison == Dependency.COMPARE_ANY) {
return true;
} else if (comparison == Dependency.COMPARE_SPEC) {
if (pkg.getSpecificationVersion() == null) {
err.fine("Package " + packageName + " did not give a specification version");
return false;
} else {
try {
SpecificationVersion versionSpec = new SpecificationVersion(version);
SpecificationVersion pkgSpec = new SpecificationVersion(pkg.getSpecificationVersion().trim());
if (versionSpec.compareTo(pkgSpec) <= 0) {
return true;
} else {
err.fine("Loaded package " + packageName + " was only of version " + pkgSpec + " but " + versionSpec + " was requested");
return false;
}
} catch (NumberFormatException nfe) {
err.log(Level.WARNING, null, nfe);
err.fine("Will not honor a dependency on non-numeric package spec version");
return false;
}
}
} else {
// COMPARE_IMPL
if (pkg.getImplementationVersion() == null) {
err.fine("Package " + packageName + " had no implementation version");
return false;
} else if (! pkg.getImplementationVersion().trim().equals(version)) {
err.fine("Package " + packageName + " had the wrong impl version: " + pkg.getImplementationVersion());
return false;
} else {
return true;
}
}
} else {
// Satisfied sample class.
return true;
}
}
/**
* Interface for a classloader to declare that it comes from a module.
* @since 2.1
*/
public interface ModuleProvider {
Module getModule();
}
/**
* Enumerate (direct) interdependencies among a set of modules.
* If used in a topological sort, the result will be a reverse-order
* list of modules (suitable for disabling; reverse for enabling).
* @param modules some modules
* @param modulesByName map from module cnbs to modules (may contain unrelated modules)
* @param providersOf map from tokens to sets of modules providing them (may mention unrelated modules)
* @return a map from modules to lists of modules they depend on
* @see Utilities#topologicalSort
* JST-PENDING needed from tests
*/
public static Map<Module,List<Module>> moduleDependencies(Collection<Module> modules, Map<String,Module> modulesByName, Map<String,Set<Module>> _providersOf) {
return moduleDependencies(modules, modulesByName, _providersOf, Collections.<String, Collection<Module>>emptyMap());
}
static Map<Module,List<Module>> moduleDependencies(Collection<Module> modules, Map<String,Module> modulesByName, Map<String,Set<Module>> _providersOf,
Map<String, Collection<Module>> fragments) {
Set<Module> modulesSet = (modules instanceof Set) ? (Set<Module>)modules : new HashSet<Module>(modules);
Map<String,List<Module>> providersOf = new HashMap<String,List<Module>>(_providersOf.size() * 2 + 1);
for (Map.Entry<String, Set<Module>> entry: _providersOf.entrySet()) {
Set<Module> providers = entry.getValue();
if (providers != null) {
List<Module> availableProviders = new LinkedList<Module>(providers);
availableProviders.retainAll(modulesSet);
if (!availableProviders.isEmpty()) {
providersOf.put(entry.getKey(), availableProviders);
}
}
}
Map<Module,List<Module>> m = new HashMap<Module,List<Module>>();
for (Module m1: modules) {
List<Module> l = null;
for (Dependency dep : m1.getDependenciesArray()) {
if (dep.getType() == Dependency.TYPE_REQUIRES) {
List<Module> providers = providersOf.get(dep.getName());
if (providers != null) {
l = fillMapSlot(m, m1);
l.addAll(providers);
}
}
else if (dep.getType() == Dependency.TYPE_MODULE) {
String cnb = (String) parseCodeName(dep.getName())[0];
Module m2 = modulesByName.get(cnb);
if (m2 != null && modulesSet.contains(m2)) {
l = fillMapSlot(m, m1);
l.add(m2);
}
}
}
// include module fragment _contents_ into the module dependencies,
// so the dependent modules are enabled before the host+fragment merged
// classloader will activate
Collection<Module> frags = fragments.get(m1.getCodeNameBase());
if (frags != null && !frags.isEmpty()) {
frags = new HashSet<>(frags);
frags.retainAll(modules);
for (Module f : frags) {
List<Module> fragmentDep = fillMapSlot(m, f);
fragmentDep.add(m1);
for (Dependency dep : f.getDependenciesArray()) {
if (dep.getType() == Dependency.TYPE_REQUIRES) {
List<Module> providers = providersOf.get(dep.getName());
if (providers != null) {
l = fillMapSlot(m, m1);
l.addAll(providers);
}
}
else if (dep.getType() == Dependency.TYPE_MODULE) {
String cnb = (String) parseCodeName(dep.getName())[0];
Module m2 = modulesByName.get(cnb);
if (m2 != null && modulesSet.contains(m2)) {
l = fillMapSlot(m, m1);
l.add(m2);
}
}
}
}
if (l != null) {
l.remove(m1);
}
}
if (l != null) {
m.put(m1, l);
}
}
return m;
}
private static List<Module> fillMapSlot(Map<Module, List<Module>> map, Module module) {
List<Module> l = map.get(module);
if (l == null) {
l = new LinkedList<>();
map.put(module, l);
}
return l;
}
/**
* Get dependencies forward or backwards starting from one module.
* @see #moduleDependencies
* @see ModuleManager#getModuleInterdependencies
*/
static Set<Module> moduleInterdependencies(Module m, boolean reverse, boolean transitive, boolean considerNeeds,
Set<Module> modules, Map<String,Module> modulesByName, Map<String,Set<Module>> providersOf) {
// XXX these algorithms could surely be made faster using standard techniques
// for now the speed is not critical however
if (reverse) {
Set<Module> s = new HashSet<Module>();
for (Module m2: modules) {
if (m2 == m) {
continue;
}
if (moduleInterdependencies(m2, false, transitive, considerNeeds, modules, modulesByName, providersOf).contains(m)) {
s.add(m2);
}
}
return s;
} else {
Set<Module> s = new HashSet<Module>();
for (Dependency dep : m.getDependenciesArray()) {
boolean needsProvider = dep.getType() == Dependency.TYPE_REQUIRES ||
considerNeeds && dep.getType() == Dependency.TYPE_NEEDS;
if (m instanceof NetigsoModule && dep.getType() == Dependency.TYPE_RECOMMENDS) {
needsProvider = true;
}
if (needsProvider) {
Set<Module> providers = providersOf.get(dep.getName());
if (providers != null) {
s.addAll(providers);
}
} else if (dep.getType() == Dependency.TYPE_MODULE) {
String cnb = (String)parseCodeName(dep.getName())[0];
Module m2 = modulesByName.get(cnb);
if (m2 != null) {
s.add(m2);
}
}
}
s.remove(m);
if (transitive) {
Set<Module> toAdd;
do {
toAdd = new HashSet<Module>();
for (Module m2: s) {
Set<Module> s2 = moduleInterdependencies(m2, false, false, considerNeeds, modules, modulesByName, providersOf);
s2.remove(m);
s2.removeAll(s);
toAdd.addAll(s2);
}
s.addAll(toAdd);
} while (!toAdd.isEmpty());
}
return s;
}
}
/** Get a filter for JAR files. */
static FilenameFilter jarFilter() {
return new JarFilter();
}
private static final class JarFilter implements FilenameFilter {
JarFilter() {}
public boolean accept(File dir, String name) {
String n = name.toLowerCase(Locale.US);
return n.endsWith(".jar"); // NOI18N
}
}
/** Convert a class file name to a resource name suitable for Beans.instantiate.
* @param name resource name of class file
* @return class name without the <code>.class</code>/<code>.ser</code> extension, and using dots as package separator
* @throws IllegalArgumentException if the name did not have a valid extension, or originally contained dots outside the extension, etc.
* @since JST-PENDING: used from NbInstaller
*/
public static String createPackageName(String name) throws IllegalArgumentException {
String clExt = ".class"; // NOI18N
if (!name.endsWith(clExt)) {
// try different extension
clExt = ".ser"; // NOI18N
}
if (name.endsWith(clExt)) {
String bareName = name.substring(0, name.length() - clExt.length());
if (bareName.length() == 0) { // ".class" // NOI18N
throw new IllegalArgumentException("Bad class file name: " + name); // NOI18N
}
if (bareName.charAt(0) == '/') { // "/foo/bar.class" // NOI18N
throw new IllegalArgumentException("Bad class file name: " + name); // NOI18N
}
if (bareName.charAt(bareName.length() - 1) == '/') { // "foo/bar/.class" // NOI18N
throw new IllegalArgumentException("Bad class file name: " + name); // NOI18N
}
if (bareName.indexOf('.') != -1) { // "foo.bar.class" // NOI18N
throw new IllegalArgumentException("Bad class file name: " + name); // NOI18N
}
return bareName.replace('/', '.'); // NOI18N
} else { // "foo/bar" or "foo.bar" // NOI18N
throw new IllegalArgumentException("Bad class file name: " + name); // NOI18N
}
}
/** A lookup implementation specialized for modules.
* Its primary advantage over e.g. AbstractLookup is that
* it is possible to add modules to the set at one time and
* fire changes in the set of modules later on. ModuleManager
* uses this to add modules immediately in create() and destroy(),
* but only fire lookup events later and asynchronously, from the
* read mutex.
*/
static final class ModuleLookup extends Lookup {
ModuleLookup() {}
private final Set<Module> modules = new HashSet<Module>(100);
private final Set<ModuleResult> results = new WeakSet<ModuleResult>(10);
/** Add a module to the set. */
public void add(Module m) {
synchronized (modules) {
modules.add(m);
}
}
/** Remove a module from the set. */
public void remove(Module m) {
synchronized (modules) {
modules.remove(m);
}
}
/** Fire changes to all result listeners. */
public void changed() {
synchronized (results) {
for (ModuleResult moduleResult : results) {
moduleResult.changed();
}
}
}
public <T> T lookup(Class<T> clazz) {
if ((clazz == Module.class || clazz == ModuleInfo.class || clazz == Object.class || clazz == null)
&& ! modules.isEmpty()) {
synchronized (modules) {
return clazz.cast(modules.iterator().next());
}
} else {
return null;
}
}
@SuppressWarnings("unchecked")
public <T> Lookup.Result<T> lookup(Lookup.Template<T> t) {
Class<T> clazz = t.getType();
if (clazz == Module.class || clazz == ModuleInfo.class ||
clazz == Object.class || clazz == null) {
return (Lookup.Result<T>)(Object) new ModuleResult((Lookup.Template<Module>) t);
}
else {
return Lookup.EMPTY.lookup(t);
}
}
public @Override String toString() {
synchronized (modules) {
return "ModuleLookup" + modules; // NOI18N
}
}
private final class ModuleResult extends Lookup.Result<Module> {
private final Lookup.Template<? super Module> t;
private final Set<LookupListener> listeners = new HashSet<LookupListener>(10);
public ModuleResult(Lookup.Template<? super Module> t) {
this.t = t;
synchronized (results) {
results.add(this);
}
}
public void addLookupListener(LookupListener l) {
synchronized (listeners) {
listeners.add(l);
}
}
public void removeLookupListener(LookupListener l) {
synchronized (listeners) {
listeners.remove(l);
}
}
public void changed() {
LookupListener[] _listeners;
synchronized (listeners) {
if (listeners.isEmpty()) {
return;
}
_listeners = listeners.toArray(new LookupListener[listeners.size()]);
}
LookupEvent ev = new LookupEvent(this);
for (int i = 0; i < _listeners.length; i++) {
_listeners[i].resultChanged(ev);
}
}
public Collection<Module> allInstances() {
synchronized (modules) {
String id = t.getId();
Object inst = t.getInstance();
if (id != null) {
Iterator<Module> it = modules.iterator();
while (it.hasNext()) {
Module m = it.next();
if (id.equals(ModuleItem.PREFIX + m.getCodeNameBase())) {
if (inst == null || inst == m) {
return Collections.<Module>singleton(m);
}
}
}
return Collections.<Module>emptySet();
} else if (inst != null) {
return modules.contains(inst) ? Collections.<Module>singleton(Module.class.cast(inst)) : Collections.<Module>emptySet();
} else {
// Regular lookup based on type.
return new HashSet<Module>(modules);
}
}
}
public @Override Set<Class<? extends Module>> allClasses() {
return Collections.<Class<? extends Module>>singleton(Module.class);
}
public @Override Collection<? extends Lookup.Item<Module>> allItems() {
Collection<Module> insts = allInstances();
ArrayList<ModuleItem> list = new ArrayList<ModuleItem>(Math.max(1, insts.size()));
for (Module m: insts) {
list.add(new ModuleItem(m));
}
return list;
}
public @Override String toString() {
return "ModuleResult:" + t; // NOI18N
}
}
private static final class ModuleItem extends Lookup.Item<Module> {
public static final String PREFIX = "Module["; // NOI18N
private final Module item;
public ModuleItem(Module item) {
this.item = item;
}
public Module getInstance() {
return item;
}
public Class<? extends Module> getType() {
return Module.class;
}
public String getId() {
return PREFIX + item.getCodeNameBase();
}
public String getDisplayName() {
return item.getDisplayName();
}
}
}
// OK to not release this memory; module deletion is rare: holds 45kB for 173 modules (June 2005)
private static final Map<String,Object[]> codeNameParseCache = new HashMap<String,Object[]>(200); // Map<String,[String,int]>
/** Find the code name base and major release version from a code name.
* Caches these parses. Thread-safe (i.e. OK from read mutex).
* @return an array consisting of the code name base (String) followed by the release version (Integer or null)
* followed by another end-range version (Integer or null)
* @throws NumberFormatException if the release version is mangled
* @since JST-PENDING: used from NbInstaller
*/
public static Object[] parseCodeName(String cn) throws NumberFormatException {
synchronized (codeNameParseCache) {
Object[] r = codeNameParseCache.get(cn);
if (r == null) {
r = new Object[3];
int i = cn.lastIndexOf('/');
if (i == -1) {
r[0] = cn;
} else {
r[0] = cn.substring(0, i).intern();
String end = cn.substring(i + 1);
int j = end.indexOf('-');
if (j == -1) {
r[1] = Integer.valueOf(end);
} else {
r[1] = Integer.valueOf(end.substring(0, j));
r[2] = Integer.valueOf(end.substring(j + 1));
}
}
codeNameParseCache.put(cn.intern(), r);
}
return r;
}
}
/** Get API module dependency, if any, for a module.
* @param dependencies module dependencies
* @param cnb code name base of API module
* @return a fake spec version (0.x.y if x.y w/ no major release, else r.x.y); or null if no dep
* @since JST-PENDING: used from NbInstaller
*/
public static SpecificationVersion getModuleDep(Set<Dependency> dependencies, String cnb) {
for (Dependency d : dependencies) {
if (d.getType() == Dependency.TYPE_MODULE &&
d.getComparison() == Dependency.COMPARE_SPEC) {
try {
Object[] p = parseCodeName(d.getName());
if (!p[0].equals(cnb)) {
continue;
}
int rel = ((Integer)p[1]).intValue(); // ignore any end range, consider only start
if (rel == -1) rel = 0; // XXX will this lead to incorrect semantics?
return new SpecificationVersion("" + rel + "." + d.getVersion()); // NOI18N
} catch (NumberFormatException nfe) {
Util.err.log(Level.WARNING, null, nfe);
return null;
}
}
}
return null;
}
/**
* Transitively fill out a set of modules with all of its module dependencies.
* Dependencies on missing modules are silently ignored, but dependencies
* on present but uninstallable (problematic) modules are included.
* @param mgr the manager
* @param modules a mutable set of modules
* @since JST-PENDING: used from NbInstaller
*/
public static void transitiveClosureModuleDependencies(ModuleManager mgr, Set<Module> modules) {
Set<Module> nue = null; // Set of newly appended modules
while (nue == null || !nue.isEmpty()) {
nue = new HashSet<Module>();
for (Module m: modules) {
for (Dependency dep : m.getDependenciesArray()) {
if (dep.getType() != Dependency.TYPE_MODULE) {
continue;
}
Module other = mgr.get((String)parseCodeName(dep.getName())[0]);
if (other != null && !modules.contains(other)) {
nue.add(other);
}
}
}
modules.addAll(nue);
}
}
}