2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
6 * The contents of this file are subject to the terms of either the GNU
7 * General Public License Version 2 only ("GPL") or the Common
8 * Development and Distribution License("CDDL") (collectively, the
9 * "License"). You may not use this file except in compliance with the
10 * License. You can obtain a copy of the License at
11 * http://www.netbeans.org/cddl-gplv2.html
12 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13 * specific language governing permissions and limitations under the
14 * License. When distributing the software, include this License Header
15 * Notice in each file and include the License file at
16 * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
17 * particular file as subject to the "Classpath" exception as provided
18 * by Sun in the GPL Version 2 section of the License file that
19 * accompanied this code. If applicable, add the following below the
20 * License Header, with the fields enclosed by brackets [] replaced by
21 * your own identifying information:
22 * "Portions Copyrighted [year] [name of copyright owner]"
26 * The Original Software is NetBeans. The Initial Developer of the Original
27 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
28 * Microsystems, Inc. All Rights Reserved.
30 * If you wish your version of this file to be governed by only the CDDL
31 * or only the GPL Version 2, indicate your decision by adding
32 * "[Contributor] elects to include this software in this distribution
33 * under the [CDDL or GPL Version 2] license." If you do not indicate a
34 * single choice of license, a recipient has the option to distribute
35 * your version of this file under either the CDDL, the GPL Version 2 or
36 * to extend the choice of license to its licensees as provided above.
37 * However, if you add GPL Version 2 code and therefore, elected the GPL
38 * Version 2 license, then the option applies only if the new code is
39 * made subject to such option by the copyright holder.
44 // THIS CLASS OUGHT NOT USE NbBundle NOR org.openide CLASSES
45 // OUTSIDE OF openide-util.jar! UI AND FILESYSTEM/DATASYSTEM
46 // INTERACTIONS SHOULD GO ELSEWHERE.
47 // (NbBundle.getLocalizedValue is OK here.)
50 import java.io.IOException;
51 import java.io.InputStream;
52 import java.security.AllPermission;
53 import java.security.CodeSource;
54 import java.security.PermissionCollection;
55 import java.security.Permissions;
56 import java.util.ArrayList;
57 import java.util.Collections;
58 import java.util.HashMap;
59 import java.util.HashSet;
60 import java.util.List;
61 import java.util.Locale;
63 import java.util.MissingResourceException;
64 import java.util.Properties;
65 import java.util.ResourceBundle;
67 import java.util.StringTokenizer;
68 import java.util.jar.Attributes;
69 import java.util.jar.JarFile;
70 import java.util.jar.Manifest;
71 import java.util.logging.Level;
72 import java.util.zip.ZipEntry;
73 import org.netbeans.Module.PackageExport;
74 import org.openide.modules.Dependency;
75 import org.openide.util.Exceptions;
76 import org.openide.util.NbBundle;
78 /** Object representing one module, possibly installed.
79 * Responsible for opening of module JAR file; reading
80 * manifest; parsing basic information such as dependencies;
81 * and creating a classloader for use by the installer.
82 * Methods not defined in ModuleInfo must be called from within
83 * the module manager's read mutex as a rule.
86 final class StandardModule extends Module {
88 /** JAR file holding the module */
89 private final File jar;
90 /** if reloadable, temporary JAR file actually loaded from */
91 private File physicalJar = null;
92 private Manifest manifest;
94 /** Map from extension JARs to sets of JAR that load them via Class-Path.
95 * Used only for debugging purposes, so that a warning is printed if two
96 * different modules try to load the same extension (which would cause them
97 * to both load their own private copy, which may not be intended).
99 private static final Map<File,Set<File>> extensionOwners = new HashMap<File,Set<File>>();
100 /** Simple registry of JAR files used as modules.
101 * Used only for debugging purposes, so that we can be sure
102 * that no one is using Class-Path to refer to other modules.
104 private static final Set<File> moduleJARs = new HashSet<File>();
106 /** Set of locale-variants JARs for this module (or null).
107 * Added explicitly to classloader, and can be used by execution engine.
109 private Set<File> localeVariants = null;
110 /** Set of extension JARs that this module loads via Class-Path (or null).
111 * Can be used e.g. by execution engine. (#9617)
113 private Set<File> plainExtensions = null;
114 /** Set of localized extension JARs derived from plainExtensions (or null).
115 * Used to add these to the classloader. (#9348)
116 * Can be used e.g. by execution engine.
118 private Set<File> localeExtensions = null;
119 /** Patches added at the front of the classloader (or null).
120 * Files are assumed to be JARs; directories are themselves.
122 private Set<File> patches = null;
124 /** localized properties, only non-null if requested from disabled module */
125 private Properties localizedProps;
127 /** Use ModuleManager.create as a factory. */
128 public StandardModule(ModuleManager mgr, Events ev, File jar, Object history, boolean reloadable, boolean autoload, boolean eager) throws IOException {
129 super(mgr, ev, history, reloadable, autoload, eager);
133 findExtensionsAndVariants(manifest);
134 // Check if some other module already listed this one in Class-Path.
135 // For the chronologically reverse case, see findExtensionsAndVariants().
136 Set<File> bogoOwners = extensionOwners.get(jar);
137 if (bogoOwners != null) {
138 Util.err.warning("module " + jar + " was incorrectly placed in the Class-Path of other JARs " + bogoOwners + "; please use OpenIDE-Module-Module-Dependencies instead");
143 public @Override Manifest getManifest() {
144 if (manifest == null) {
147 } catch (IOException x) {
148 Util.err.log(Level.WARNING, "While loading manifest for " + this, x);
149 manifest = new Manifest();
155 public @Override void releaseManifest() {
159 /** Get a localized attribute.
160 * First, if OpenIDE-Module-Localizing-Bundle was given, the specified
161 * bundle file (in all locale JARs as well as base JAR) is searched for
162 * a key of the specified name.
163 * Otherwise, the manifest's main attributes are searched for an attribute
164 * with the specified name, possibly with a locale suffix.
165 * If the attribute name contains a slash, and there is a manifest section
166 * named according to the part before the last slash, then this section's attributes
167 * are searched instead of the main attributes, and for the attribute listed
168 * after the slash. Currently this would only be useful for localized filesystem
169 * names. E.g. you may request the attribute org/foo/MyFileSystem.class/Display-Name.
170 * In the future certain attributes known to be dangerous could be
171 * explicitly suppressed from this list; should only be used for
172 * documented localizable attributes such as OpenIDE-Module-Name etc.
174 public Object getLocalizedAttribute(String attr) {
175 String locb = getManifest().getMainAttributes().getValue("OpenIDE-Module-Localizing-Bundle"); // NOI18N
176 boolean usingLoader = false;
178 if (classloader != null) {
179 if (locb.endsWith(".properties")) { // NOI18N
181 String basename = locb.substring(0, locb.length() - 11).replace('/', '.');
183 ResourceBundle bundle = NbBundle.getBundle(basename, Locale.getDefault(), classloader);
185 return bundle.getString(attr);
186 } catch (MissingResourceException mre) {
189 } catch (MissingResourceException mre) {
190 Util.err.log(Level.WARNING, null, mre);
193 Util.err.warning("cannot efficiently load non-*.properties OpenIDE-Module-Localizing-Bundle: " + locb);
197 if (localizedProps == null) {
198 Util.err.log(Level.FINE, "Trying to get localized attr {0} from disabled module {1}", new Object[] {attr, getCodeNameBase()});
200 // check if the jar file still exists (see issue 82480)
201 if (jar != null && jar.isFile ()) {
202 JarFile jarFile = new JarFile(jar, false);
204 loadLocalizedProps(jarFile, getManifest());
209 Util.err.log(Level.FINE, "Cannot get localized attr {0} from module {1} (missing or deleted JAR file: {2})", new Object[] {attr, getCodeNameBase(), jar});
211 } catch (IOException ioe) {
212 Util.err.log(Level.WARNING, jar.getAbsolutePath(), ioe);
215 if (localizedProps != null) {
216 String val = localizedProps.getProperty(attr);
223 // Try in the manifest now.
224 int idx = attr.lastIndexOf('/'); // NOI18N
226 // Simple main attribute.
227 return NbBundle.getLocalizedValue(getManifest().getMainAttributes(), new Attributes.Name(attr));
229 // Attribute of a manifest section.
230 String section = attr.substring(0, idx);
231 String realAttr = attr.substring(idx + 1);
232 Attributes attrs = getManifest().getAttributes(section);
234 return NbBundle.getLocalizedValue(attrs, new Attributes.Name(realAttr));
241 public boolean owns(Class clazz) {
242 ClassLoader cl = clazz.getClassLoader();
243 if (cl instanceof Util.ModuleProvider) {
244 return ((Util.ModuleProvider) cl).getModule() == this;
250 public boolean isFixed() {
254 /** Get the JAR this module is packaged in.
255 * May be null for modules installed specially, e.g.
256 * automatically from the classpath.
259 public File getJarFile() {
263 /** Create a temporary test JAR if necessary.
264 * This is primarily necessary to work around a Java bug,
265 * #4405789, which is marked as fixed so might be obsolete.
267 private void ensurePhysicalJar() throws IOException {
268 if (reloadable && physicalJar == null) {
269 physicalJar = Util.makeTempJar(jar);
272 private void destroyPhysicalJar() {
273 if (physicalJar != null) {
274 if (physicalJar.isFile()) {
275 if (! physicalJar.delete()) {
276 Util.err.warning("temporary JAR " + physicalJar + " not currently deletable.");
278 Util.err.fine("deleted: " + physicalJar);
283 Util.err.fine("no physicalJar to delete for " + this);
287 /** Open the JAR, load its manifest, and do related things. */
288 private void loadManifest() throws IOException {
289 Util.err.fine("loading manifest of " + jar);
290 File jarBeingOpened = null; // for annotation purposes
293 // Never try to cache reloadable JARs.
294 jarBeingOpened = physicalJar; // might be null
296 jarBeingOpened = physicalJar; // might have changed
297 JarFile jarFile = new JarFile(physicalJar, false);
299 Manifest m = jarFile.getManifest();
300 if (m == null) throw new IOException("No manifest found in " + physicalJar); // NOI18N
306 jarBeingOpened = jar;
307 manifest = getManager().loadManifest(jar);
309 } catch (IOException e) {
310 if (jarBeingOpened != null) {
311 Exceptions.attachMessage(e,
312 "While loading manifest from: " +
313 jarBeingOpened); // NOI18N
319 /** Find any extensions loaded by the module, as well as any localized
320 * variants of the module or its extensions.
322 private void findExtensionsAndVariants(Manifest m) {
323 assert jar != null : "Cannot load extensions from classpath module " + getCodeNameBase();
324 localeVariants = null;
325 List<File> l = Util.findLocaleVariantsOf(jar);
327 localeVariants = new HashSet<File>(l);
329 plainExtensions = null;
330 localeExtensions = null;
331 String classPath = m.getMainAttributes().getValue(Attributes.Name.CLASS_PATH);
332 if (classPath != null) {
333 StringTokenizer tok = new StringTokenizer(classPath);
334 while (tok.hasMoreTokens()) {
335 String ext = tok.nextToken();
336 if (new File(ext).isAbsolute() || ext.indexOf("../") != -1) { // NOI18N
337 Util.err.warning("Class-Path value " + ext + " from " + jar + " is illegal according to the Java Extension Mechanism: must be relative and not move up directories");
339 File extfile = new File(jar.getParentFile(), ext.replace('/', File.separatorChar));
340 if (! extfile.exists()) {
341 // Ignore unloadable extensions.
342 Util.err.warning("Class-Path value " + ext + " from " + jar + " cannot be found at " + extfile);
345 //No need to sync on extensionOwners - we are in write mutex
346 Set<File> owners = extensionOwners.get(extfile);
347 if (owners == null) {
348 owners = new HashSet<File>(2);
350 extensionOwners.put(extfile, owners);
351 } else if (! owners.contains(jar)) {
353 events.log(Events.EXTENSION_MULTIPLY_LOADED, extfile, owners);
354 } // else already know about it (OK or warned)
355 // Also check to make sure it is not a module JAR! See constructor for the reverse case.
356 if (moduleJARs.contains(extfile)) {
357 Util.err.warning("Class-Path value " + ext + " from " + jar + " illegally refers to another module; use OpenIDE-Module-Module-Dependencies instead");
359 if (plainExtensions == null) plainExtensions = new HashSet<File>();
360 plainExtensions.add(extfile);
361 l = Util.findLocaleVariantsOf(extfile);
363 if (localeExtensions == null) {
364 localeExtensions = new HashSet<File>();
366 localeExtensions.addAll(l);
370 // #9273: load any modules/patches/this-code-name/*.jar files first:
371 File patchdir = new File(new File(jar.getParentFile(), "patches"), // NOI18N
372 getCodeNameBase().replace('.', '-')); // NOI18N
373 scanForPatches(patchdir);
374 // Use of the following system property is not supported, but is used
375 // by e.g. XTest to influence installed modules without changing the build.
376 // Format is -Dnetbeans.patches.org.nb.mods.foo=/path/to.file.jar:/path/to/dir
377 String patchesClassPath = System.getProperty("netbeans.patches." + getCodeNameBase()); // NOI18N
378 if (patchesClassPath != null) {
379 StringTokenizer tokenizer = new StringTokenizer(patchesClassPath, File.pathSeparator);
380 while (tokenizer.hasMoreTokens()) {
381 String element = tokenizer.nextToken();
382 File fileElement = new File(element);
383 if (fileElement.exists()) {
384 if (patches == null) {
385 patches = new HashSet<File>(15);
387 patches.add(fileElement);
391 Util.err.fine("localeVariants of " + jar + ": " + localeVariants);
392 Util.err.fine("plainExtensions of " + jar + ": " + plainExtensions);
393 Util.err.fine("localeExtensions of " + jar + ": " + localeExtensions);
394 Util.err.fine("patches of " + jar + ": " + patches);
395 if (patches != null) {
396 for (File patch : patches) {
397 events.log(Events.PATCH, patch);
402 /** Scans a directory for possible patch JARs. */
403 private void scanForPatches(File patchdir) {
404 if (!patchdir.isDirectory()) {
407 File[] jars = patchdir.listFiles(Util.jarFilter());
409 for (File patchJar : jars) {
410 if (patches == null) {
411 patches = new HashSet<File>(5);
413 patches.add(patchJar);
416 Util.err.warning("Could not search for patches in " + patchdir);
420 /** Check if there is any need to load localized properties.
421 * If so, try to load them. Throw an exception if they cannot
422 * be loaded for some reason. Uses an open JAR file for the
423 * base module at least, though may also open locale variants
425 * Note: due to #19698, this cache is not usually used; only if you
426 * specifically go to look at the display properties of a disabled module.
427 * @see <a href="http://www.netbeans.org/issues/show_bug.cgi?id=12549">#12549</a>
429 private void loadLocalizedProps(JarFile jarFile, Manifest m) throws IOException {
430 String locbundle = m.getMainAttributes().getValue("OpenIDE-Module-Localizing-Bundle"); // NOI18N
431 if (locbundle != null) {
432 // Something requested, read it in.
433 // locbundle is a resource path.
435 ZipEntry bundleFile = jarFile.getEntry(locbundle);
436 // May not be present in base JAR: might only be in e.g. default locale variant.
437 if (bundleFile != null) {
438 localizedProps = new Properties();
439 InputStream is = jarFile.getInputStream(bundleFile);
441 localizedProps.load(is);
448 // Check also for localized variant JARs and load in anything from them as needed.
449 // Note we need to go in the reverse of the usual search order, so as to
450 // overwrite less specific bundles with more specific.
451 int idx = locbundle.lastIndexOf('.'); // NOI18N
457 name = locbundle.substring(0, idx);
458 ext = locbundle.substring(idx);
460 List<Util.FileWithSuffix> pairs = Util.findLocaleVariantsWithSuffixesOf(jar);
461 Collections.reverse(pairs);
462 for (Util.FileWithSuffix pair : pairs) {
463 File localeJar = pair.file;
464 String suffix = pair.suffix;
465 String rsrc = name + suffix + ext;
466 JarFile localeJarFile = new JarFile(localeJar, false);
468 ZipEntry bundleFile = localeJarFile.getEntry(rsrc);
469 // Need not exist in all locale variants.
470 if (bundleFile != null) {
471 if (localizedProps == null) {
472 localizedProps = new Properties();
473 } // else append and overwrite base-locale values
474 InputStream is = localeJarFile.getInputStream(bundleFile);
476 localizedProps.load(is);
482 localeJarFile.close();
486 if (localizedProps == null) {
487 // We should have loaded from at least some bundle in there...
488 throw new IOException("Could not find localizing bundle: " + locbundle); // NOI18N
490 /* Don't log; too large and annoying:
491 if (Util.err.isLoggable(ErrorManager.INFORMATIONAL)) {
492 Util.err.fine("localizedProps=" + localizedProps);
498 /** Get all JARs loaded by this module.
499 * Includes the module itself, any locale variants of the module,
500 * any extensions specified with Class-Path, any locale variants
501 * of those extensions.
502 * The list will be in classpath order (patches first).
503 * Currently the temp JAR is provided in the case of test modules, to prevent
504 * sporadic ZIP file exceptions when background threads (like Java parsing) tries
505 * to open libraries found in the library path.
506 * JARs already present in the classpath are <em>not</em> listed.
507 * @return a <code>List<File></code> of JARs
509 public List<File> getAllJars() {
510 List<File> l = new ArrayList<File>();
511 if (patches != null) l.addAll(patches);
512 if (physicalJar != null) {
514 } else if (jar != null) {
517 if (plainExtensions != null) l.addAll (plainExtensions);
518 if (localeVariants != null) l.addAll (localeVariants);
519 if (localeExtensions != null) l.addAll (localeExtensions);
523 /** Set whether this module is supposed to be reloadable.
524 * Has no immediate effect, only impacts what happens the
525 * next time it is enabled (after having been disabled if
527 * Must be called from within a write mutex.
528 * @param r whether the module should be considered reloadable
530 public void setReloadable(boolean r) {
531 getManager().assertWritable();
532 if (reloadable != r) {
534 getManager().fireReloadable(this);
538 /** Used as a flag to tell if this module was really successfully released.
539 * Currently does not work, so if it cannot be made to work, delete it.
540 * (Someone seems to be holding a strong reference to the classloader--who?!)
542 private transient boolean released;
543 /** Count which release() call is really being checked. */
544 private transient int releaseCount = 0;
546 /** Reload this module. Access from ModuleManager.
547 * If an exception is thrown, the module is considered
548 * to be in an invalid state.
550 public void reload() throws IOException {
551 // Probably unnecessary but just in case:
552 destroyPhysicalJar();
553 String codeNameBase1 = getCodeNameBase();
554 localizedProps = null;
557 findExtensionsAndVariants(manifest);
558 String codeNameBase2 = getCodeNameBase();
559 if (! codeNameBase1.equals(codeNameBase2)) {
560 throw new InvalidException("Code name base changed during reload: " + codeNameBase1 + " -> " + codeNameBase2); // NOI18N
564 // Access from ModuleManager:
565 /** Turn on the classloader. Passed a list of parent modules to use.
566 * The parents should already have had their classloaders initialized.
568 protected void classLoaderUp(Set<Module> parents) throws IOException {
569 Util.err.fine("classLoaderUp on " + this + " with parents " + parents);
570 // Find classloaders for dependent modules and parent to them.
571 List<ClassLoader> loaders = new ArrayList<ClassLoader>(parents.size() + 1);
572 // This should really be the base loader created by org.nb.Main for loading openide etc.:
573 loaders.add(Module.class.getClassLoader());
574 for (Module parent: parents) {
575 PackageExport[] exports = parent.getPublicPackages();
576 if (exports != null && exports.length == 0) {
577 // Check if there is an impl dep here.
578 boolean implDep = false;
579 for (Dependency dep : getDependenciesArray()) {
580 if (dep.getType() == Dependency.TYPE_MODULE &&
581 dep.getComparison() == Dependency.COMPARE_IMPL &&
582 dep.getName().equals(parent.getCodeName())) {
588 // Nothing exported from here at all, no sense even adding it.
589 // Shortcut; would not harm anything to add it now, but we would
590 // never use it anyway.
595 ClassLoader l = parent.getClassLoader();
596 if (parent.isFixed() && loaders.contains(l)) {
597 Util.err.fine("#24996: skipping duplicate classloader from " + parent);
602 List<File> classp = new ArrayList<File>(3);
603 if (patches != null) classp.addAll(patches);
607 // Using OPEN_DELETE does not work well with test modules under 1.4.
608 // Random code (URL handler?) still expects the JAR to be there and
610 classp.add(physicalJar);
614 // URLClassLoader would not otherwise find these, so:
615 if (localeVariants != null) classp.addAll(localeVariants);
617 if (localeExtensions != null) classp.addAll(localeExtensions);
619 if (plainExtensions != null) classp.addAll(plainExtensions);
622 getManager().refineClassLoader(this, loaders);
625 classloader = new OneModuleClassLoader(classp, loaders.toArray(new ClassLoader[loaders.size()]));
626 } catch (IllegalArgumentException iae) {
627 // Should not happen, but just in case.
628 throw (IOException) new IOException(iae.toString()).initCause(iae);
632 /** Turn off the classloader and release all resources. */
633 protected void classLoaderDown() {
634 if (classloader instanceof ProxyClassLoader) {
635 ((ProxyClassLoader)classloader).destroy();
638 Util.err.fine("classLoaderDown on " + this + ": releaseCount=" + releaseCount + " released=" + released);
641 /** Should be called after turning off the classloader of one or more modules & GC'ing. */
642 protected void cleanup() {
643 if (isEnabled()) throw new IllegalStateException("cleanup on enabled module: " + this); // NOI18N
644 if (classloader != null) throw new IllegalStateException("cleanup on module with classloader: " + this); // NOI18N
646 Util.err.fine("Warning: not all resources associated with module " + jar + " were successfully released.");
649 Util.err.fine("All resources associated with module " + jar + " were successfully released.");
651 // XXX should this rather be done when the classloader is collected?
652 destroyPhysicalJar();
655 /** Notify the module that it is being deleted. */
656 public void destroy() {
657 moduleJARs.remove(jar);
660 /** String representation for debugging. */
661 public String toString() {
662 String s = "StandardModule:" + getCodeNameBase() + " jarFile: " + jar.getAbsolutePath(); // NOI18N
663 if (!isValid()) s += "[invalid]"; // NOI18N
667 /** PermissionCollection with an instance of AllPermission. */
668 private static PermissionCollection modulePermissions;
669 /** @return initialized @see #modulePermission */
670 private static synchronized PermissionCollection getAllPermission() {
671 if (modulePermissions == null) {
672 modulePermissions = new Permissions();
673 modulePermissions.add(new AllPermission());
674 modulePermissions.setReadOnly();
676 return modulePermissions;
679 /** Class loader to load a single module.
680 * Auto-localizing, multi-parented, permission-granting, the works.
682 private class OneModuleClassLoader extends JarClassLoader implements Util.ModuleProvider {
684 /** Create a new loader for a module.
685 * @param classp the List of all module jars of code directories;
686 * includes the module itself, its locale variants,
687 * variants of extensions and Class-Path items from Manifest.
688 * The items are JarFiles for jars and Files for directories
689 * @param parents a set of parent classloaders (from other modules)
691 public OneModuleClassLoader(List<File> classp, ClassLoader[] parents) throws IllegalArgumentException {
692 super(classp, parents, false, StandardModule.this);
696 public Module getModule() {
697 return StandardModule.this;