blob: dcdecd07a82e4c140d796801749ac0d6fe762f89 [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.core.startup;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.Events;
import org.netbeans.InvalidException;
import org.netbeans.Module;
import org.netbeans.ModuleInstaller;
import org.netbeans.ModuleManager;
import org.netbeans.Stamps;
import org.netbeans.Util;
import org.netbeans.core.startup.layers.ModuleLayeredFileSystem;
import org.openide.filesystems.FileUtil;
import org.openide.modules.Dependency;
import org.openide.modules.ModuleInfo;
import org.openide.modules.ModuleInstall;
import org.openide.modules.SpecificationVersion;
import org.openide.util.BaseUtilities;
import org.openide.util.NbCollections;
import org.openide.util.SharedClassObject;
import org.openide.util.NbBundle;
import org.openide.util.Task;
import org.openide.util.lookup.InstanceContent;
/** Concrete implementation of the module installation functionality.
* This class can pay attention to the details of manifest format,
* details of how to install particular sections and layers and so on.
* It may have a limited UI, i.e. notifying problems with localized
* messages and the like.
* @author Jesse Glick, Jan Pokorsky, Jaroslav Tulach, et al.
*/
final class NbInstaller extends ModuleInstaller {
private static final Logger LOG = Logger.getLogger(NbInstaller.class.getName());
/** set of manifest sections for each module */
private final Map<Module,Set<ManifestSection>> sections = new HashMap<Module,Set<ManifestSection>>(100);
/** ModuleInstall classes for each module that declares one */
private final Map<Module,Class<? extends ModuleInstall>> installs = new HashMap<Module,Class<? extends ModuleInstall>>(100);
/** layer resources for each module that declares one */
private final Map<Module,String> layers = new HashMap<Module,String>(100);
/** exact use of this is hard to explain */
private boolean initializedFolderLookup = false;
/** where to report events to */
private final Events ev;
/** associated controller of module list; needed for handling ModuleInstall ser */
private ModuleList moduleList;
/** associated manager */
private ModuleManager mgr;
/** set of permitted core or package dependencies from a module */
private final Map<Module,Set<String>> kosherPackages = new HashMap<Module,Set<String>>(100);
/** classpath ~ JRE packages to be hidden from a module */
private final Map<Module,List<Module.PackageExport>> hiddenClasspathPackages = new HashMap<Module,List<Module.PackageExport>>();
/** #164510: similar to {@link #hiddenClasspathPackages} but backwards for efficiency */
private final Map<Module.PackageExport,List<Module>> hiddenClasspathPackagesReverse = new HashMap<Module.PackageExport,List<Module>>();
/** caches important values from module manifests */
private final Cache cache = new Cache();
/** Processing @OnStart/@OnStop calls */
private final NbStartStop onStartStop = new NbStartStop(null, null);
/** Create an NbInstaller.
* You should also call {@link #registerManager} and if applicable
* {@link #registerList} to make the installer fully usable.
* @param ev events to log
*/
public NbInstaller(Events ev) {
this.ev = ev;
}
/** Access from ModuleSystem. */
void registerList(ModuleList list) {
if (moduleList != null) throw new IllegalStateException();
moduleList = list;
}
void registerManager(ModuleManager manager) {
if (mgr != null) throw new IllegalStateException();
mgr = manager;
}
// @SuppressWarnings("unchecked")
@Override
public void prepare(Module m) throws InvalidException {
ev.log(Events.PREPARE, m);
checkForHiddenPackages(m);
Set<ManifestSection> mysections = null;
Class<?> clazz = null;
String processSections = cache.findGlobalProperty("processSections", null, "false"); // NOI18N
if (!"false".equals(processSections)) { // NOI18N
// Find and load manifest sections.
Manifest mani = m.getManifest();
if (mani == null) {
throw new InvalidException(m, "no manifest");
}
for (Map.Entry<String,Attributes> entry : mani.getEntries().entrySet()) {
ManifestSection section = ManifestSection.create(entry.getKey(), entry.getValue(), m);
if (section != null) {
if (mysections == null) {
mysections = new HashSet<ManifestSection>(25);
}
mysections.add(section);
}
}
if (mysections != null) {
cache.findGlobalProperty("processSections", "false", "true"); // NOI18N
}
}
String installClass = cache.findProperty(m, "OpenIDE-Module-Install", false); // NOI18N
if (installClass != null) {
String installClassName;
try {
installClassName = Util.createPackageName(installClass);
} catch (IllegalArgumentException iae) {
InvalidException ie = new InvalidException(m, iae.toString());
ie.initCause(iae);
throw ie;
}
if (installClass.endsWith(".ser")) throw new InvalidException(m, "Serialized module installs not supported: " + installClass); // NOI18N
try {
// We could simply load the ModuleInstall right away in all cases.
// However consider a ModuleInstall that has a static or instance
// initializer or initialize() calling NbBundle.getBundle(String). In the
// old module installer that would work and it was never specifically discouraged,
// so that needs to continue working. So do not resolve in most cases;
// if the module specifically has a validate method, then this is clearly
// documented to be called before systemClassLoader is ready, so the onus
// is on the module author to avoid anything dangerous there.
clazz = Class.forName(installClassName, false, m.getClassLoader());
if (clazz.getClassLoader() != m.getClassLoader()) {
ev.log(Events.WRONG_CLASS_LOADER, m, clazz, m.getClassLoader());
}
// Search for a validate() method; if there is none, do not resolve the class now!
Class<?> c;
for (c = clazz; c != ModuleInstall.class && c != Object.class; c = c.getSuperclass()) {
try {
// #13997: do not search in superclasses.
c.getDeclaredMethod("validate"); // NOI18N
// It has one. We are permitted to resolve the class, create
// the installer instance, and validate it.
ModuleInstall install = SharedClassObject.findObject(clazz.asSubclass(ModuleInstall.class), true);
// The following can throw IllegalStateException, which we would
// rethrow as InvalidException below:
install.validate();
} catch (NoSuchMethodException nsme) {
// OK, did not find it here, continue.
}
}
if (c == Object.class) throw new ClassCastException("Should extend ModuleInstall: " + clazz.getName()); // NOI18N
// Did not find any validate() method, so remember the class and resolve later.
} catch (Exception t) {
InvalidException ie = new InvalidException(m, t.toString());
ie.initCause(t);
throw ie;
} catch (LinkageError t) {
InvalidException ie = new InvalidException(m, t.toString());
ie.initCause(t);
throw ie;
}
}
// For layer & help set, validate only that the base-locale resource
// exists, not its contents or anything.
String layerResource = cache.findProperty(m, "OpenIDE-Module-Layer", false); // NOI18N
if (layerResource != null && !m.isNetigso()) {
URL layer = m.getClassLoader().getResource(layerResource);
if (layer == null) throw new InvalidException(m, "Layer not found: " + layerResource); // NOI18N
}
String helpSetName = cache.findProperty(m, "OpenIDE-Module-Description", false); // NOI18N
if (helpSetName != null) {
Util.err.log(Level.WARNING, "Use of OpenIDE-Module-Description in {0} is deprecated.", m.getCodeNameBase());
Util.err.warning("(Please install help using an XML layer instead.)");
}
// We are OK, commit everything to our cache.
if (mysections != null) {
sections.put(m, mysections);
}
if (clazz != null) {
installs.put(m, clazz.asSubclass(ModuleInstall.class));
}
if (layerResource != null) {
layers.put(m, layerResource);
}
}
private void checkForHiddenPackages(Module m) throws InvalidException {
List<Module.PackageExport> hiddenPackages = new ArrayList<Module.PackageExport>();
List<Module> mWithDeps = new LinkedList<Module>();
mWithDeps.add(m);
if (mgr != null) {
mWithDeps.addAll(mgr.getAttachedFragments(m));
for (Dependency d : m.getDependencies()) {
if (d.getType() == Dependency.TYPE_MODULE) {
Module _m = mgr.get((String) Util.parseCodeName(d.getName())[0]);
assert _m != null : d;
mWithDeps.add(_m);
mWithDeps.addAll(mgr.getAttachedFragments(_m));
}
}
}
for (Module _m : mWithDeps) {
String hidden = cache.findProperty(_m, "OpenIDE-Module-Hide-Classpath-Packages", false); // NOI18N
if (hidden != null) {
for (String piece : hidden.trim().split("[ ,]+")) { // NOI18N
try {
if (piece.endsWith(".*")) { // NOI18N
String pkg = piece.substring(0, piece.length() - 2);
Dependency.create(Dependency.TYPE_MODULE, pkg);
if (pkg.lastIndexOf('/') != -1) {
throw new IllegalArgumentException("Illegal OpenIDE-Module-Hide-Classpath-Packages: " + hidden); // NOI18N
}
hiddenPackages.add(new Module.PackageExport(pkg.replace('.', '/') + '/', false));
} else if (piece.endsWith(".**")) { // NOI18N
String pkg = piece.substring(0, piece.length() - 3);
Dependency.create(Dependency.TYPE_MODULE, pkg);
if (pkg.lastIndexOf('/') != -1) {
throw new IllegalArgumentException("Illegal OpenIDE-Module-Hide-Classpath-Packages: " + hidden); // NOI18N
}
hiddenPackages.add(new Module.PackageExport(pkg.replace('.', '/') + '/', true));
} else {
throw new IllegalArgumentException("Illegal OpenIDE-Module-Hide-Classpath-Packages: " + hidden); // NOI18N
}
} catch (IllegalArgumentException x) {
throw new InvalidException(_m, x.getMessage());
}
}
}
}
if (!hiddenPackages.isEmpty()) {
synchronized (hiddenClasspathPackages) {
hiddenClasspathPackages.put(m, hiddenPackages);
for (Module.PackageExport pkg : hiddenPackages) {
List<Module> ms = hiddenClasspathPackagesReverse.get(pkg);
if (ms == null) {
hiddenClasspathPackagesReverse.put(pkg, ms = new LinkedList<Module>());
}
ms.add(m);
}
}
}
}
public void dispose(Module m) {
Util.err.fine("dispose: " + m);
// Events probably not needed here.
Set<ManifestSection> s = sections.remove(m);
if (s != null) {
for (ManifestSection sect : s) {
sect.dispose();
}
}
installs.remove(m);
layers.remove(m);
kosherPackages.remove(m);
synchronized (hiddenClasspathPackages) {
hiddenClasspathPackages.remove(m);
for (List<Module> ms : hiddenClasspathPackagesReverse.values()) {
ms.remove(m);
// could also delete entry if ms.isEmpty()
}
}
}
@Override
protected void classLoaderUp(ClassLoader cl) {
MainLookup.systemClassLoaderChanged(cl);
ev.log(Events.PERF_TICK, "META-INF/services/ additions registered"); // NOI18N
}
final void waitOnStart() {
onStartStop.waitOnStart();
}
@Override
public void load(final List<Module> modules) {
FileUtil.runAtomicAction(new Runnable() {
@Override
public void run() {
loadImpl(modules);
}
});
}
private void loadImpl(List<Module> modules) {
ev.log(Events.START_LOAD, modules);
checkForDeprecations(modules);
loadLayers(modules, true);
ev.log(Events.PERF_TICK, "layers loaded"); // NOI18N
onStartStop.initialize();
ev.log(Events.PERF_TICK, "@OnStart"); // NOI18N
ev.log(Events.PERF_START, "NbInstaller.load - sections"); // NOI18N
ev.log(Events.LOAD_SECTION);
CoreBridge.getDefault().loaderPoolTransaction(true);
try {
for (Module m: modules) {
try {
loadSections(m, true);
} catch (Exception t) {
Util.err.log(Level.SEVERE, null, t);
} catch (LinkageError le) {
Util.err.log(Level.SEVERE, null, le);
}
ev.log(Events.PERF_TICK, "sections for " + m.getCodeName() + " loaded"); // NOI18N
}
} finally {
CoreBridge.getDefault().loaderPoolTransaction(false);
}
ev.log(Events.PERF_END, "NbInstaller.load - sections"); // NOI18N
// Yarda says to put this here.
if (! initializedFolderLookup) {
Util.err.fine("modulesClassPathInitialized");
MainLookup.modulesClassPathInitialized();
initializedFolderLookup = true;
}
// we need to initialize UI before we let modules run ModuleInstall.restore
Main.initUICustomizations();
ev.log(Events.PERF_START, "NbInstaller.load - ModuleInstalls"); // NOI18N
for (Module m: modules) {
try {
loadCode(m, true);
} catch (Exception t) {
Util.err.log(Level.SEVERE, null, t);
} catch (LinkageError le) {
Util.err.log(Level.SEVERE, null, le);
} catch (AssertionError e) {
Util.err.log(Level.SEVERE, null, e);
}
ev.log(Events.PERF_TICK, "ModuleInstall for " + m.getCodeName() + " called"); // NOI18N
}
ev.log(Events.PERF_END, "NbInstaller.load - ModuleInstalls"); // NOI18N
ev.log(Events.FINISH_LOAD, modules);
if (Boolean.getBoolean("netbeans.preresolve.classes")) {
preresolveClasses(modules);
}
}
final void preloadCache(Collection<Module> modules) {
for (Module m : modules) {
// initialize the cache
isShowInAutoUpdateClient(m);
}
}
@Override
public void unload(final List<Module> modules) {
FileUtil.runAtomicAction(new Runnable() {
@Override
public void run() {
unloadImpl(modules);
}
});
}
private void unloadImpl(List<Module> modules) {
ev.log(Events.START_UNLOAD, modules);
for (Module m: modules) {
try {
loadCode(m, false);
} catch (Exception t) {
Util.err.log(Level.SEVERE, null, t);
} catch (LinkageError le) {
Util.err.log(Level.SEVERE, null, le);
}
}
CoreBridge.getDefault().loaderPoolTransaction(true);
try {
for (Module m: modules) {
try {
loadSections(m, false);
} catch (Exception t) {
Util.err.log(Level.SEVERE, null, t);
} catch (LinkageError le) {
Util.err.log(Level.SEVERE, null, le);
}
}
} finally {
try {
CoreBridge.getDefault().loaderPoolTransaction(false);
} catch (RuntimeException e) {
Util.err.log(Level.SEVERE, null, e);
}
}
loadLayers(modules, false);
ev.log(Events.FINISH_UNLOAD, modules);
}
/** Load/unload installer code for a module. */
@SuppressWarnings("deprecation") // old ModuleInstall methods we have to call
private void loadCode(Module m, boolean load) throws Exception {
Class<? extends ModuleInstall> instClazz = installs.get(m);
if (instClazz != null) {
ModuleInstall inst = SharedClassObject.findObject(instClazz, true);
if (load) {
ev.log(Events.RESTORE, m);
inst.restored();
} else {
ev.log(Events.UNINSTALL, m);
inst.uninstalled();
}
}
}
/** Load/unload all manifest sections for a given module. */
@SuppressWarnings("deprecation") // old ManifestSection.* we have to interpret
private void loadSections(Module m, boolean load) throws Exception {
Set<ManifestSection> s = sections.get(m);
if (s == null) {
return;
}
// Whether we yet had occasion to attach to the module actions list.
boolean attachedToMA = false;
try {
ev.log(Events.LOAD_SECTION);
for (ManifestSection sect : s) {
if (sect instanceof ManifestSection.ActionSection) {
if (! attachedToMA) {
// First categorize the actions we will add.
Object category = m.getLocalizedAttribute("OpenIDE-Module-Display-Category"); // NOI18N
if (category == null) {
// uncategorized modules group by themselves
category = m.getCodeNameBase();
}
CoreBridge.getDefault().attachToCategory(category);
attachedToMA = true;
}
CoreBridge.getDefault ().loadActionSection((ManifestSection.ActionSection)sect, load);
} else if (sect instanceof ManifestSection.ClipboardConvertorSection) {
loadGenericSection(sect, load);
} else if (sect instanceof ManifestSection.DebuggerSection) {
loadGenericSection(sect, load);
} else if (sect instanceof ManifestSection.LoaderSection) {
CoreBridge.getDefault().loadLoaderSection((ManifestSection.LoaderSection)sect, load);
} else {
assert false : sect;
}
}
} finally {
if (attachedToMA) {
CoreBridge.getDefault ().attachToCategory (null);
}
}
}
// Load or unload various possible manifest sections.
/** Simple section that can just be passed to lookup.
* The lookup sees the real object, not the section.
* You tell it whether to convert the result to the real
* instance, or just register the section itself.
*/
private void loadGenericSection(ManifestSection s, boolean load) {
CoreBridge.getDefault().loadDefaultSection(s, convertor, load);
}
private final InstanceContent.Convertor<ManifestSection,Object> convertor = new Convertor();
private final class Convertor implements InstanceContent.Convertor<ManifestSection,Object> { // or <ManifestSection,SharedClassObject>?
Convertor() {}
public Object convert(ManifestSection s) {
try {
return s.getInstance();
} catch (Exception e) {
Util.err.log(Level.WARNING, null, e);
// Try to remove it from the pool so it does not continue
// to throw errors over and over. Hopefully it is kosher to
// do this while it is in the process of converting! I.e.
// hopefully InstanceLookup is well-synchronized.
loadGenericSection(s, false);
return null;
}
}
public Class<?> type(ManifestSection s) {
return s.getSuperclass();
}
/** Computes the ID of the resulted object.
* @param obj the registered object
* @return the ID for the object
*/
public String id(ManifestSection obj) {
return obj.toString ();
}
/** The human presentable name for the object.
* @param obj the registered object
* @return the name representing the object for the user
*/
public String displayName(ManifestSection obj) {
return obj.toString ();
}
}
/** Either load or unload the layer, if any, for a set of modules.
* If the parameter load is true, load it, else unload it.
* Locale/branding variants are likewise loaded or unloaded.
* If a module has no declared layer, does nothing.
*/
void loadLayers(List<Module> modules, boolean load) {
ev.log(load ? Events.LOAD_LAYERS : Events.UNLOAD_LAYERS, modules);
// #23609: dependent modules should be able to override:
modules = new ArrayList<Module>(modules);
Collections.reverse(modules);
Map<ModuleLayeredFileSystem,Collection<URL>> urls = new HashMap<ModuleLayeredFileSystem,Collection<URL>>(5);
ModuleLayeredFileSystem userModuleLayer = ModuleLayeredFileSystem.getUserModuleLayer();
ModuleLayeredFileSystem installationModuleLayer = ModuleLayeredFileSystem.getInstallationModuleLayer();
urls.put(userModuleLayer, new LinkedHashSet<URL>(1000));
urls.put(installationModuleLayer, new LinkedHashSet<URL>(1000));
for (Module m: modules) {
// #19458: only put reloadables into the "session layer"
// (where they will not have their layers cached). All others
// should go into "installation layer" (so that they can mask
// layers according to cross-dependencies).
ModuleLayeredFileSystem host = m.isReloadable() ? userModuleLayer : installationModuleLayer;
Collection<URL> theseurls = urls.get(host);
if (theseurls == null) {
theseurls = new LinkedHashSet<URL>(1000);
urls.put(host, theseurls);
}
String s = layers.get(m);
if (s != null) {
Util.err.log(Level.FINE, "loadLayer: {0} load={1}", new Object[] { s, load });
// Actually add a sequence of layers, in locale order.
String base, ext;
int idx = s.lastIndexOf('.'); // NOI18N
if (idx == -1) {
base = s;
ext = ""; // NOI18N
} else {
base = s.substring(0, idx);
ext = s.substring(idx);
}
boolean foundSomething = false;
for (String suffix : NbCollections.iterable(NbBundle.getLocalizingSuffixes())) {
String resource = base + suffix + ext;
Enumeration<URL> en = m.findResources(resource);
if (en.hasMoreElements()) {
URL u = en.nextElement();
theseurls.add(u);
foundSomething = true;
if (en.hasMoreElements()) {
String patchesClassPath = System.getProperty("netbeans.patches." + m.getCodeNameBase()); // NOI18N
assert patchesClassPath != null : "At most one resource per module: " + m; // NOI18N
Util.err.log(Level.INFO, "Using {0} as layer for {1} not {2}", new Object[]{u, m.getCodeNameBase(), en.nextElement()}); // NOI18N
}
}
}
if (! foundSomething) {
// Should never happen (we already checked in prepare() for base layer)...
Util.err.fine("Module layer not found: " + s);
continue;
}
}
Enumeration<URL> e = m.findResources("META-INF/generated-layer.xml"); // NOI18N
while (e.hasMoreElements()) {
URL u = e.nextElement();
theseurls.add(u);
}
}
// Now actually do it.
for (Map.Entry<ModuleLayeredFileSystem,Collection<URL>> entry: urls.entrySet()) {
ModuleLayeredFileSystem host = entry.getKey();
Collection<URL> theseurls = entry.getValue();
Util.err.log(Level.FINE, "Adding/removing layer URLs: host={0} urls={1}", new Object[] { host, theseurls });
try {
if (load) {
host.addURLs(theseurls);
} else {
// #106737: we might have the wrong host, since it switches when reloadable flag is toggled.
// To be safe, remove from both.
userModuleLayer.removeURLs(theseurls);
installationModuleLayer.removeURLs(theseurls);
}
} catch (Exception e) {
Util.err.log(Level.WARNING, null, e);
}
}
}
/** Scan a (nondeprecated) module for direct dependencies on deprecated modules.
* Deprecated modules can quietly depend on other deprecated modules.
* And if the module is not actually enabled, it does not matter.
* Indirect dependencies are someone else's problem.
* Provide-require dependencies do not count either.
* @param modules the modules which are now being turned on
*/
private void checkForDeprecations(List<Module> modules) {
Map<String,Set<String>> depToUsers = new TreeMap<String,Set<String>>();
for (Module m : modules) {
String depr = cache.findProperty(m, "OpenIDE-Module-Deprecated", false); // NOI18N
if (!Boolean.parseBoolean(depr)) {
for (Dependency dep : m.getDependencies()) {
if (dep.getType() == Dependency.TYPE_MODULE) {
String cnb = (String) Util.parseCodeName(dep.getName())[0];
Set<String> users = depToUsers.get(cnb);
if (users == null) {
users = new TreeSet<String>();
depToUsers.put(cnb, users);
}
users.add(m.getCodeNameBase());
}
}
}
}
for (Map.Entry<String,Set<String>> entry : depToUsers.entrySet()) {
String dep = entry.getKey();
Module o = mgr.get(dep);
assert o != null : "No such module: " + dep;
String depr = cache.findProperty(o, "OpenIDE-Module-Deprecated", false); // NOI18N
if (Boolean.parseBoolean(depr)) {
String message = cache.findProperty(o, "OpenIDE-Module-Deprecation-Message", true); // NOI18N
// XXX use NbEvents? I18N?
// For now, assume this is a developer-oriented message that need not be localized or displayed in a pretty fashion.
Set<String> users = entry.getValue();
if (message != null) {
Util.err.log(Level.WARNING, "the modules {0} use {1} which is deprecated: {2}", new Object[] {users, dep, message});
} else {
Util.err.log(Level.WARNING, "the modules {0} use {1} which is deprecated.", new Object[] {users, dep});
}
}
}
}
public boolean closing(List<Module> modules) {
Util.err.fine("closing: " + modules);
for (Module m: modules) {
Class<? extends ModuleInstall> instClazz = installs.get(m);
if (instClazz != null) {
try {
ModuleInstall inst = SharedClassObject.findObject(instClazz, true);
if (! inst.closing()) {
Util.err.fine("Module " + m + " refused to close");
return false;
}
} catch (RuntimeException re) {
Util.err.log(Level.SEVERE, null, re);
// continue, assume it is trash
} catch (LinkageError le) {
Util.err.log(Level.SEVERE, null, le);
}
}
}
return onStartStop.closing(modules);
}
@Override
public void close(List<Module> modules) {
closeAsync(modules).waitFinished();
}
@Override
public Task closeAsync(List<Module> modules) {
Util.err.fine("close: " + modules);
ev.log(Events.CLOSE);
moduleList.shutDown();
List<Task> waitFor = onStartStop.startClose(modules);
// [PENDING] this may need to write out changed ModuleInstall externalized
// forms...is that really necessary to do here, or isn't it enough to
// do right after loading etc.? Currently these are only written when
// a ModuleInstall has just been restored etc. which is probably fine.
for (Module m : modules) {
Class<? extends ModuleInstall> instClazz = installs.get(m);
if (instClazz != null) {
try {
ModuleInstall inst = SharedClassObject.findObject(instClazz, true);
if (inst == null) throw new IllegalStateException("Inconsistent state: " + instClazz); // NOI18N
inst.close();
} catch (ThreadDeath td) {
throw td;
} catch (Throwable t) {
// Catch even the heavy stuff here, we are going away.
Util.err.log(Level.SEVERE, null, t);
// oh well
}
}
}
waitFor.add(WarmUpSupport.waitTask());
return new ProxyTask(waitFor);
}
private static String cacheCnb;
private static Set<Dependency> cacheDeps;
@Override
protected Set<Dependency> loadDependencies(String cnb) {
return cnb.equals(cacheCnb) ? cacheDeps : null;
}
@SuppressWarnings("unchecked")
static void register(String name, Object obj) {
cacheCnb = name;
cacheDeps = (Set<Dependency>)obj;
}
/** Overridden to perform automatic API upgrades.
* That is, should do nothing on new modules, but for older ones will
* automatically make them depend on things they need.
* This is now all handled from declarative configuration files:
* in the system filesystem, ModuleAutoDeps/*.xml may be added
* according to the DTD "-//NetBeans//DTD Module Automatic Dependencies 1.0//EN".
*/
public @Override void refineDependencies(Module m, Set<Dependency> dependencies) {
/* JST-PENDING just tring to comment this out
// All modules implicitly depend on the APIs somehow.
if (!m.getCodeNameBase().equals("org.openide") &&
Util.getModuleDep(dependencies, "org.openide") == null) {
dependencies.addAll(Dependency.create(Dependency.TYPE_MODULE, "org.openide/1 > 0")); // NOI18N
}
*/
if (Boolean.getBoolean("org.netbeans.core.modules.NbInstaller.noAutoDeps")) {
// Skip them all - useful for unit tests.
return;
}
AutomaticDependencies.Report rep = AutomaticDependencies.getDefault().refineDependenciesAndReport(m.getCodeNameBase(), dependencies);
if (rep.isModified()) {
Util.err.warning(rep.toString());
}
}
public @Override String[] refineProvides (Module m) {
if (m.getCodeNameBase ().equals ("org.openide.modules")) { // NOI18N
List<String> arr = new ArrayList<String>(4);
CoreBridge.defineOsTokens(arr);
// module format is now 2
arr.add("org.openide.modules.ModuleFormat1"); // NOI18N
arr.add("org.openide.modules.ModuleFormat2"); // NOI18N
return arr.toArray (new String[arr.size()]);
}
return null;
}
public @Override boolean shouldDelegateResource(Module m, Module parent, String pkg) {
//Util.err.fine("sDR: m=" + m + " parent=" + parent + " pkg=" + pkg);
// Cf. #19622:
if (parent == null) {
// Application classpath checks.
for (String cppkg : CLASSPATH_PACKAGES) {
if (pkg.startsWith(cppkg) && !findKosher(m).contains(cppkg)) {
// Undeclared use of a classpath package. Refuse it.
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Refusing to load classpath package " + pkg + " for " + m.getCodeNameBase() + " without a proper dependency"); // NOI18N
}
return false;
}
}
List<Module.PackageExport> hiddenPackages;
synchronized (hiddenClasspathPackages) {
hiddenPackages = hiddenClasspathPackages.get(m);
}
if (hiddenPackages != null) {
for (Module.PackageExport hidden : hiddenPackages) {
if (hidden.recursive ? pkg.startsWith(hidden.pkg) : pkg.equals(hidden.pkg)) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Refusing to load classpath package " + pkg + " for " + m.getCodeNameBase());
}
return false;
}
}
}
if (!checkBootDelegation(pkg)) {
return false;
}
}
if (LOG.isLoggable(Level.FINER) && /* otherwise ClassCircularityError on LogRecord*/!pkg.equals("java/util/logging/")) {
LOG.finer("Delegating resource " + pkg + " from " + parent + " for " + m.getCodeNameBase());
}
return true;
}
public @Override boolean shouldDelegateClasspathResource(String pkg) {
synchronized (hiddenClasspathPackages) {
for (Map.Entry<Module.PackageExport,List<Module>> entry : hiddenClasspathPackagesReverse.entrySet()) {
Module.PackageExport hidden = entry.getKey();
if (hidden.recursive ? pkg.startsWith(hidden.pkg) : pkg.equals(hidden.pkg)) {
for (Module m : entry.getValue()) {
if (m.isEnabled()) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Refusing to load classpath package " + pkg + " because of " + m.getCodeNameBase());
}
return false;
}
}
}
}
}
return checkBootDelegation(pkg);
}
private boolean checkBootDelegation(String pkg) {
String del = System.getProperty("netbeans.bootdelegation"); // NOI18N
if (del != null) {
if (!pkg.startsWith("java/")) {
boolean allowed = false;
for (String s : del.split(",")) {
s = s.trim();
if (s.endsWith(".*")) {
s = s.substring(0, s.length() - 2).replace('.', '/') + '/';
if (pkg.startsWith(s) && pkg.length() > s.length()) {
allowed = true;
break;
}
} else {
s = s.replace('.', '/') + '/';
if (pkg.equals(s)) {
allowed = true;
break;
}
}
}
if (!allowed) {
return false;
}
}
}
return true;
}
private static final String[] CLASSPATH_PACKAGES = {
// core.jar shall be inaccessible
"org/netbeans/core/startup/",
// Do not add JRE packages here! See issue #96711 for the alternative.
};
private Set<String> findKosher(Module m) {
Set<String> s = kosherPackages.get(m);
if (s == null) {
s = new HashSet<String>();
Set<Dependency> deps = m.getDependencies();
SpecificationVersion openide = Util.getModuleDep(deps, "org.openide"); // NOI18N
boolean pre27853 = (openide == null || openide.compareTo(new SpecificationVersion("1.3.12")) < 0); // NOI18N
for (Dependency dep : deps) {
// Extend this for other classpath modules:
if (dep.getType() == Dependency.TYPE_MODULE &&
dep.getName().equals("org.netbeans.core.startup/1")) { // NOI18N
// Various modules (incl. o.n.core) dep on this as friends and need to access it.
s.add("org/netbeans/core/startup/"); // NOI18N
} else if (pre27853 && dep.getType() == Dependency.TYPE_MODULE) {
// Module dependency. If a package was kosher for A and B depends
// on A, we let B use it undeclared. Cf. javacvs -> vcscore & RE.
// But #27853: only do this for old modules.
String name = dep.getName();
int idx = name.indexOf('/');
if (idx != -1) {
name = name.substring(0, idx);
}
Module other = mgr.get(name);
if (other == null) throw new IllegalStateException("Should have found dep " + dep + " from " + m); // NOI18N
s.addAll(findKosher(other));
} else if (dep.getType() == Dependency.TYPE_PACKAGE) {
String depname = dep.getName();
String req;
int idx = depname.indexOf('['); // NOI18N
if (idx == -1) {
// depname = org.apache.xerces.parsers
// req = org/apache/xerces/parsers/
req = depname.replace('.', '/').concat("/"); // NOI18N
} else if (idx == 0) {
// depname = [org.apache.xerces.parsers.DOMParser]
// req = org/apache/xerces/parsers/
int idx2 = depname.lastIndexOf('.');
req = depname.substring(1, idx2).replace('.', '/').concat("/"); // NOI18N
} else {
// depname = org.apache.xerces.parsers[DOMParser]
// req = org/apache/xerces/parsers/
req = depname.substring(0, idx).replace('.', '/').concat("/"); // NOI18N
}
for (String cppkg : CLASSPATH_PACKAGES) {
if (req.startsWith(cppkg)) {
// Module requested this exact package or some subpackage or
// a class in one of these packages; it is kosher.
s.add(cppkg);
}
}
}
}
if (s.isEmpty()) s = Collections.<String>emptySet();
kosherPackages.put(m, s);
}
return s;
}
/** Information about contents of some JARs on the startup classpath (both lib/ and lib/ext/).
* The first item in each is a prefix for JAR filenames.
* For a JAR matching such a prefix, the remaining items are entries
* from CLASSPATH_PACKAGES.
* <p>In this case, the JAR is only on a module's effective classpath in case
* {@link #findKosher} reports that at least one of the packages is requested -
* in which case that package and any descendants, but not other packages in the JAR,
* are included in the effective classpath.
* <p>JARs which are not listed here but are in lib/ or lib/ext/ are assumed to be
* in the effective classpath of every module, automatically.
* <p>Note that updater.jar is *not* on the startup classpath and is explicitly
* added to autoupdate.jar via Class-Path, so it need not concern us here -
* it should appear on the effective classpath of autoupdate only.
* @see "#22466"
*/
private static final String[][] CLASSPATH_JARS = {
{"core", "org/netbeans/core/", "org/netbeans/beaninfo/"}, // NOI18N
// No one ought to be using boot.jar:
{"boot"}, // NOI18N
};
/** Get the effective "classpath" used by a module.
* Specific syntax: classpath entries as usual, but
* they may be qualified with packages, e.g.:
* <code>/path/to/api-module.jar[org.netbeans.api.foo.*,org.netbeans.spi.foo.**]:/path/to/wide-open.jar</code>
* @see ModuleSystem#getEffectiveClasspath
* @see "#22466"
*/
String getEffectiveClasspath(Module m) {
if (!m.isEnabled()) {
// For disabled modules, we do not know for sure what it can load.
return ""; // NOI18N
}
// The classpath entries - each is a filename possibly followed by package qualifications.
List<String> l = new ArrayList<String>(100);
// Start with boot classpath.
createBootClassPath(l);
// Move on to "startup classpath", qualified by applicable package deps etc.
// Fixed classpath modules don't get restricted in this way.
Set<String> kosher = m.isFixed() ? null : findKosher(m);
StringTokenizer tok = new StringTokenizer(System.getProperty("java.class.path", ""), File.pathSeparator);
while (tok.hasMoreTokens()) {
addStartupClasspathEntry(new File(tok.nextToken()), l, kosher);
}
// See org.netbeans.Main for actual computation of the dynamic classpath.
tok = new StringTokenizer(System.getProperty("netbeans.dynamic.classpath", ""), File.pathSeparator);
while (tok.hasMoreTokens()) {
addStartupClasspathEntry(new File(tok.nextToken()), l, kosher);
}
// Finally include this module and its dependencies recursively.
// Modules whose direct classpath has already been added to the list:
Set<Module> modulesConsidered = new HashSet<Module>(50);
// Code names of modules on which this module has an impl dependency
// (so can use any package):
Set<String> implDeps = new HashSet<String>(10);
for (Dependency dep : m.getDependencies()) {
// Remember, provide-require deps do not affect classpath!
if (dep.getType() == Dependency.TYPE_MODULE && dep.getComparison() == Dependency.COMPARE_IMPL) {
// We can assume the impl dep has the correct version;
// otherwise this module could not have been enabled to begin with.
implDeps.add(dep.getName());
}
}
SpecificationVersion openide = Util.getModuleDep(m.getDependencies(), "org.openide"); // NOI18N
boolean pre27853 = (openide == null || openide.compareTo(new SpecificationVersion("1.3.12")) < 0); // NOI18N
// #27853: only make recursive for old modules.
addModuleClasspathEntries(m, m, modulesConsidered, implDeps, l, pre27853 ? Integer.MAX_VALUE : 1);
// Done, package it all up as a string.
StringBuilder buf = new StringBuilder(l.size() * 100 + 1);
for (String s: l) {
if (buf.length() > 0) {
buf.append(File.pathSeparatorChar);
}
buf.append(s);
}
return buf.toString();
}
// Copied from NbClassPath:
private static void createBootClassPath(List<String> l) {
// boot
String boot = System.getProperty("sun.boot.class.path"); // NOI18N
if (boot != null) {
StringTokenizer tok = new StringTokenizer(boot, File.pathSeparator);
while (tok.hasMoreTokens()) {
l.add(tok.nextToken());
}
}
// std extensions
String extensions = System.getProperty("java.ext.dirs"); // NOI18N
if (extensions != null) {
for (StringTokenizer st = new StringTokenizer(extensions, File.pathSeparator); st.hasMoreTokens();) {
File dir = new File(st.nextToken());
File[] entries = dir.listFiles();
if (entries != null) {
for (File f : entries) {
String name = f.getName().toLowerCase(Locale.US);
if (name.endsWith(".zip") || name.endsWith(".jar")) { // NOI18N
l.add(f.getAbsolutePath());
}
}
}
}
}
}
/** Add a classpath entry from the lib/ or lib/ext/ dirs, if appropriate.
* @param entry a classpath entry; either a directory, or a JAR file which might be controlled
* @param cp the classpath (<code>List&lt;String&gt;</code>) to add to
* @param kosher known packages which may be accessed (<code>Set&lt;String&gt;</code>), or null for no restrictions
* @see "#22466"
*/
private static void addStartupClasspathEntry(File cpEntry, List<String> cp, Set<String> kosher) {
if (cpEntry.isDirectory()) {
cp.add(cpEntry.getAbsolutePath());
return;
}
// JAR or ZIP. Check whether we can access it.
String name = cpEntry.getName();
for (String[] cpjar : CLASSPATH_JARS) {
if (kosher != null && name.startsWith(cpjar[0])) {
// Restricted JAR.
StringBuilder entry = null; // will be set if there are any packages
for (int k = 1; k < cpjar.length; k++) {
String pkg = cpjar[k];
if (kosher.contains(pkg)) {
// Module is permitted to use this package.
if (entry == null) {
entry = new StringBuilder(100);
entry.append(cpEntry.getAbsolutePath());
entry.append('['); // NOI18N
} else {
entry.append(','); // NOI18N
}
// pkg format: org/foo/bar/
entry.append(pkg.replace('/', '.')); // NOI18N
entry.append("**"); // NOI18N
}
}
if (entry != null) {
// Had >= 1 packages available from this JAR.
entry.append(']'); // NOI18N
cp.add(entry.toString());
}
return;
}
}
// Did not match any, assumed to be on classpath.
cp.add(cpEntry.getAbsolutePath());
}
/** Recursively build a classpath based on the module dependency tree.
* @param m the current module whose JAR(s) might be added to the classpath
* @param orig the module whose classpath we are ultimately trying to compute
* @param considered modules we have already handled
* @param implDeps module code names on which the orig module has an impl dep
* @param cp the classpath to add to
* @param depth the recursion depth to go to: 0 = only m's JAR, 1 = m + its parents, ..., MAX_INT = full recursion
* @see "#22466"
*/
private void addModuleClasspathEntries(Module m, Module orig, Set<Module> considered, Set<String> implDeps, List<String> cp, int depth) {
// Head recursion so that baser modules are added to the front of the classpath:
if (!considered.add(m)) return;
for (Dependency dep : m.getDependencies()) {
if (dep.getType() == Dependency.TYPE_MODULE) {
String cnb = (String) Util.parseCodeName(dep.getName())[0];
Module next = mgr.get(cnb);
if (next == null) throw new IllegalStateException("No such module: " + cnb); // NOI18N
if (depth > 0) {
addModuleClasspathEntries(next, orig, considered, implDeps, cp, depth - 1);
}
}
}
// Now add entries from m, if applicable.
// We are friendly if we are considering the original module itself,
// or if the original module has a (direct) impl dep on this module.
boolean friend = (m == orig) || implDeps.contains(m.getCodeName());
// Friend modules export everything to the classpath.
// Otherwise, there may or may not be package restrictions.
Module.PackageExport[] exports = friend ? null : m.getPublicPackages();
String qualification = ""; // NOI18N
if (exports != null) {
if (exports.length == 0) {
// Everything is blocked.
return;
}
// Only certain packages are exported, so list them explicitly.
StringBuilder b = new StringBuilder(100);
b.append('['); // NOI18N
for (int i = 0; i < exports.length; i++) {
if (i > 0) {
b.append(','); // NOI18N
}
b.append(exports[i].pkg.replace('/', '.')); // NOI18N
b.append(exports[i].recursive ? "**" : "*"); // NOI18N
}
b.append(']'); // NOI18N
qualification = b.toString();
}
for (File jar : m.getAllJars()) {
cp.add(jar.getAbsolutePath() + qualification);
}
}
/** Check all module classes to make sure there are no unresolvable compile-time
* dependencies. Turn on this mode with
* <code>-J-Dnetbeans.preresolve.classes=true</code>
* May be more useful to run org.netbeans.core.ValidateClassLinkageTest.
* @param modules a list of modules, newly enabled, to check; fixed modules will be ignored
*/
private void preresolveClasses(List<Module> modules) {
Util.err.info("Pre-resolving classes for all loaded modules...be sure you have not specified -J-Xverify:none in ide.cfg");
for (Module m : modules) {
if (m.isFixed()) continue;
if (m.getJarFile() == null) continue;
File jar = m.getJarFile();
// Note: extension JARs not checked.
try {
JarFile j = new JarFile(jar, true);
try {
for (JarEntry entry : NbCollections.iterable(j.entries())) {
String name = entry.getName();
if (name.endsWith(".class")) { // NOI18N
String clazz = name.substring(0, name.length() - 6).replace('/', '.'); // NOI18N
Throwable t = null;
try {
Class.forName(clazz, false, m.getClassLoader());
} catch (ClassNotFoundException cnfe) { // e.g. "Will not load classes from default package" from ProxyClassLoader
t = cnfe;
} catch (LinkageError le) {
t = le;
} catch (RuntimeException re) { // e.g. IllegalArgumentException from package defs
t = re;
}
if (t != null) {
// XXX #106153: consider excluding mobility/ant-ext classes
Util.err.log(Level.WARNING, "From " + clazz + " in " + m.getCodeNameBase() + " with effective classpath " + getEffectiveClasspath(m), t);
}
}
}
} finally {
j.close();
}
} catch (IOException ioe) {
Util.err.log(Level.WARNING, null, ioe);
}
}
}
final boolean isShowInAutoUpdateClient(ModuleInfo m) {
String show = cache.findProperty(m, "AutoUpdate-Show-In-Client", false); // NOI18N
if (show != null) {
return Boolean.parseBoolean(show);
}
// OSGi bundles should be considered invisible by default since they are typically autoloads.
// (NB modules get AutoUpdate-Show-In-Client inserted into the JAR by the build process.)
if (m instanceof Module) {
return !((Module)m).isNetigso();
}
return true;
}
/** Cache important attributes from module manifests */
static class Cache implements Stamps.Updater {
private static final String CACHE = "all-installer.dat"; // NOI18N
private final boolean modulePropertiesCached;
private final Properties moduleProperties;
public Cache() {
InputStream is = Stamps.getModulesJARs().asStream(CACHE);
IF:
if (is != null) {
Properties p = new Properties();
try {
p.load(is);
is.close();
} catch (IOException ex) {
LOG.log(Level.INFO, "Can't load all-installer.dat", ex);
break IF;
}
moduleProperties = p;
modulePropertiesCached = true;
return;
}
moduleProperties = new Properties();
modulePropertiesCached = false;
}
final String findProperty(ModuleInfo m, String name, boolean localized) {
final String fullName = m.getCodeNameBase() + '.' + name;
final String nullValue = "\u0000"; // NOI18N
if (modulePropertiesCached) {
String val = moduleProperties.getProperty(fullName);
if (nullValue.equals(val)) {
return null;
}
if (val != null) {
return val;
}
LOG.log(Level.FINE, "not cached value: {0} for {1}", new Object[]{name, m});
}
Object p = localized ? m.getLocalizedAttribute(name) : m.getAttribute(name);
if (p == null) {
moduleProperties.setProperty(fullName, nullValue);
Stamps.getModulesJARs().scheduleSave(this, CACHE, false);
return null;
}
String prop = p instanceof String ? (String)p : null;
if (prop != null) {
moduleProperties.setProperty(fullName, prop);
Stamps.getModulesJARs().scheduleSave(this, CACHE, false);
}
return prop;
}
final String findGlobalProperty(String name, String expValue, String replaceValue) {
assert name != null;
assert replaceValue != null;
if (modulePropertiesCached) {
return moduleProperties.getProperty(name);
} else {
final Object prevValue = moduleProperties.get(name);
if (BaseUtilities.compareObjects(expValue, prevValue)) {
moduleProperties.put(name, replaceValue);
}
Stamps.getModulesJARs().scheduleSave(this, CACHE, false);
return null;
}
}
@Override
public void flushCaches(DataOutputStream os) throws IOException {
moduleProperties.store(os, null);
}
@Override
public void cacheReady() {
}
} // end of Cache
}