blob: 52ab00b697126e847494cc74f969b2072c42f477 [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;
// THIS CLASS OUGHT NOT USE NbBundle NOR org.openide CLASSES
// OUTSIDE OF openide-util.jar! UI AND FILESYSTEM/DATASYSTEM
// INTERACTIONS SHOULD GO ELSEWHERE.
// (NbBundle.getLocalizedValue is OK here.)
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.security.AllPermission;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import org.netbeans.LocaleVariants.FileWithSuffix;
import org.openide.modules.Dependency;
import org.openide.modules.InstalledFileLocator;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.BaseUtilities;
/** Object representing one module, possibly installed.
* Responsible for opening of module JAR file; reading
* manifest; parsing basic information such as dependencies;
* and creating a classloader for use by the installer.
* Methods not defined in ModuleInfo must be called from within
* the module manager's read mutex as a rule.
* @author Jesse Glick, Allan Gregersen
*/
class StandardModule extends Module {
/** JAR file holding the module */
private final File jar;
/** if reloadable, temporary JAR file actually loaded from */
private File physicalJar;
private Manifest manifest;
/** Simple registry of JAR files used as modules.
* Used only for debugging purposes, so that we can be sure
* that no one is using Class-Path to refer to other modules.
*/
private static final Set<File> moduleJARs = new HashSet<File>();
/** Patches added at the front of the classloader (or null).
* Files are assumed to be JARs; directories are themselves.
*/
private Set<File> patches;
/** localized properties, only non-null if requested from disabled module */
private Properties localizedProps;
/** Use ModuleManager.create as a factory. */
public StandardModule(ModuleManager mgr, Events ev, File jar, Object history, boolean reloadable, boolean autoload, boolean eager) throws IOException {
super(mgr, ev, history, JaveleonModule.isJaveleonPresent || reloadable, autoload, eager);
this.jar = jar;
moduleJARs.add(jar);
}
@Override
ModuleData createData(ObjectInput in, Manifest mf) throws IOException {
if (in != null) {
return new StandardModuleData(in);
} else {
return new StandardModuleData(mf, this);
}
}
public @Override Manifest getManifest() {
if (manifest == null) {
try {
loadManifest();
} catch (IOException x) {
Util.err.log(Level.WARNING, "While loading manifest for " + getJarFile(), x);
manifest = new Manifest();
}
}
return manifest;
}
public @Override void releaseManifest() {
manifest = null;
}
/** Get a localized attribute.
* First, if OpenIDE-Module-Localizing-Bundle was given, the specified
* bundle file (in all locale JARs as well as base JAR) is searched for
* a key of the specified name.
* Otherwise, the manifest's main attributes are searched for an attribute
* with the specified name, possibly with a locale suffix.
* If the attribute name contains a slash, and there is a manifest section
* named according to the part before the last slash, then this section's attributes
* are searched instead of the main attributes, and for the attribute listed
* after the slash. Currently this would only be useful for localized filesystem
* names. E.g. you may request the attribute org/foo/MyFileSystem.class/Display-Name.
* In the future certain attributes known to be dangerous could be
* explicitly suppressed from this list; should only be used for
* documented localizable attributes such as OpenIDE-Module-Name etc.
*/
public Object getLocalizedAttribute(String attr) {
String locb = getManifest().getMainAttributes().getValue("OpenIDE-Module-Localizing-Bundle"); // NOI18N
boolean usingLoader = false;
if (locb != null) {
if (classloader != null) {
if (locb.endsWith(".properties")) { // NOI18N
usingLoader = true;
String basename = locb.substring(0, locb.length() - 11).replace('/', '.');
try {
ResourceBundle bundle = NbBundle.getBundle(basename, Locale.getDefault(), classloader);
try {
return bundle.getString(attr);
} catch (MissingResourceException mre) {
// Fine, ignore.
}
} catch (MissingResourceException mre) {
String resource = basename.replace('.', '/') + ".properties";
Exceptions.attachMessage(mre, "#149833: failed to find " + basename +
" in locale " + Locale.getDefault() + " in " + classloader + " for " + jar +
"; resource lookup of " + resource + " -> " + classloader.getResource(resource));
Exceptions.printStackTrace(mre);
}
} else {
Util.err.warning("cannot efficiently load non-*.properties OpenIDE-Module-Localizing-Bundle: " + locb);
}
}
if (!usingLoader) {
if (localizedProps == null) {
Util.err.log(Level.FINE, "Trying to get localized attr {0} from disabled module {1}", new Object[] {attr, getCodeNameBase()});
try {
// check if the jar file still exists (see issue 82480)
if (jar != null && jar.isFile ()) {
JarFile jarFile = new JarFile(jar, false);
try {
loadLocalizedProps(jarFile, getManifest());
} finally {
jarFile.close();
}
} else {
Util.err.log(Level.FINE, "Cannot get localized attr {0} from module {1} (missing or deleted JAR file: {2})", new Object[] {attr, getCodeNameBase(), jar});
}
} catch (IOException ioe) {
Util.err.log(Level.WARNING, jar.getAbsolutePath(), ioe);
}
}
if (localizedProps != null) {
String val = localizedProps.getProperty(attr);
if (val != null) {
return val;
}
}
}
}
// Try in the manifest now.
int idx = attr.lastIndexOf('/'); // NOI18N
if (idx == -1) {
// Simple main attribute.
return NbBundle.getLocalizedValue(getManifest().getMainAttributes(), new Attributes.Name(attr));
} else {
// Attribute of a manifest section.
String section = attr.substring(0, idx);
String realAttr = attr.substring(idx + 1);
Attributes attrs = getManifest().getAttributes(section);
if (attrs != null) {
return NbBundle.getLocalizedValue(attrs, new Attributes.Name(realAttr));
} else {
return null;
}
}
}
public boolean isFixed() {
return false;
}
/** Get the JAR this module is packaged in.
* May be null for modules installed specially, e.g.
* automatically from the classpath.
* @see #isFixed
*/
public @Override File getJarFile() {
return jar;
}
/** Create a temporary test JAR if necessary.
* This is primarily necessary to work around a Java bug,
* #4405789, which is marked as fixed so might be obsolete.
*/
private void ensurePhysicalJar() throws IOException {
if (reloadable && physicalJar == null) {
physicalJar = Util.makeTempJar(jar);
}
}
private void destroyPhysicalJar() {
if (physicalJar != null) {
if (physicalJar.isFile()) {
if (! physicalJar.delete()) {
Util.err.warning("temporary JAR " + physicalJar + " not currently deletable.");
} else {
Util.err.fine("deleted: " + physicalJar);
}
}
physicalJar = null;
} else {
Util.err.fine("no physicalJar to delete for " + this);
}
}
/** Open the JAR, load its manifest, and do related things. */
private void loadManifest() throws IOException {
if (Util.err.isLoggable(Level.FINE)) {
Util.err.fine("loading manifest of " + jar);
}
File jarBeingOpened = null; // for annotation purposes
try {
if (reloadable) {
// Never try to cache reloadable JARs.
jarBeingOpened = physicalJar; // might be null
ensurePhysicalJar();
jarBeingOpened = physicalJar; // might have changed
JarFile jarFile = new JarFile(physicalJar, false);
try {
Manifest m = jarFile.getManifest();
if (m == null) throw new IOException("No manifest found in " + physicalJar); // NOI18N
manifest = m;
} finally {
jarFile.close();
}
} else {
jarBeingOpened = jar;
manifest = getManager().loadManifest(jar);
}
} catch (IOException e) {
if (jarBeingOpened != null) {
Exceptions.attachMessage(e,
"While loading manifest from: " +
jarBeingOpened); // NOI18N
}
throw e;
}
}
private Set<File> findPatches() {
if (patches == null) {
// #9273: load any modules/patches/this-code-name/*.jar files first:
File patchdir = new File(new File(jar.getParentFile(), "patches"), // NOI18N
getCodeNameBase().replace('.', '-')); // NOI18N
if (patchdir.isDirectory()) {
File[] jars = patchdir.listFiles(Util.jarFilter());
if (jars != null) {
for (File patchJar : jars) {
if (patches == null) {
patches = new HashSet<File>(5);
}
patches.add(patchJar);
}
} else {
Util.err.warning("Could not search for patches in " + patchdir);
}
}
// The following system property is used
// by XTest, Maven Compile On Save & co.
// to influence installed modules without changing the build.
// Format is -Dnetbeans.patches.org.nb.mods.foo=/path/to.file.jar:/path/to/dir
String patchesClassPath = System.getProperty("netbeans.patches." + getCodeNameBase()); // NOI18N
if (patchesClassPath != null) {
StringTokenizer tokenizer = new StringTokenizer(patchesClassPath, File.pathSeparator);
while (tokenizer.hasMoreTokens()) {
String element = tokenizer.nextToken();
File fileElement = new File(element);
if (fileElement.exists()) {
if (patches == null) {
patches = new HashSet<File>(15);
}
patches.add(fileElement);
}
}
}
if (Util.err.isLoggable(Level.FINE)) {
Util.err.log(Level.FINE, "patches of {0}: {1}", new Object[]{jar, patches});
}
if (patches != null) {
for (File patch : patches) {
events.log(Events.PATCH, patch);
}
}
if (patches == null) {
patches = Collections.emptySet();
}
}
return patches;
}
/** Check if there is any need to load localized properties.
* If so, try to load them. Throw an exception if they cannot
* be loaded for some reason. Uses an open JAR file for the
* base module at least, though may also open locale variants
* as needed.
* Note: due to #19698, this cache is not usually used; only if you
* specifically go to look at the display properties of a disabled module.
* @see <a href="http://www.netbeans.org/issues/show_bug.cgi?id=12549">#12549</a>
*/
private void loadLocalizedProps(JarFile jarFile, Manifest m) throws IOException {
String locbundle = m.getMainAttributes().getValue("OpenIDE-Module-Localizing-Bundle"); // NOI18N
if (locbundle != null) {
// Something requested, read it in.
// locbundle is a resource path.
{
ZipEntry bundleFile = jarFile.getEntry(locbundle);
// May not be present in base JAR: might only be in e.g. default locale variant.
if (bundleFile != null) {
localizedProps = new Properties();
InputStream is = jarFile.getInputStream(bundleFile);
try {
localizedProps.load(is);
} finally {
is.close();
}
}
}
{
// Check also for localized variant JARs and load in anything from them as needed.
// Note we need to go in the reverse of the usual search order, so as to
// overwrite less specific bundles with more specific.
int idx = locbundle.lastIndexOf('.'); // NOI18N
String name, ext;
if (idx == -1) {
name = locbundle;
ext = ""; // NOI18N
} else {
name = locbundle.substring(0, idx);
ext = locbundle.substring(idx);
}
List<FileWithSuffix> pairs = LocaleVariants.findLocaleVariantsWithSuffixesOf(jar, getCodeNameBase());
Collections.reverse(pairs);
for (FileWithSuffix pair : pairs) {
File localeJar = pair.file;
String suffix = pair.suffix;
String rsrc = name + suffix + ext;
JarFile localeJarFile = new JarFile(localeJar, false);
try {
ZipEntry bundleFile = localeJarFile.getEntry(rsrc);
// Need not exist in all locale variants.
if (bundleFile != null) {
if (localizedProps == null) {
localizedProps = new Properties();
} // else append and overwrite base-locale values
InputStream is = localeJarFile.getInputStream(bundleFile);
try {
localizedProps.load(is);
} finally {
is.close();
}
}
} finally {
localeJarFile.close();
}
}
}
if (localizedProps == null) {
// We should have loaded from at least some bundle in there...
throw new IOException("Could not find localizing bundle: " + locbundle); // NOI18N
}
/* Don't log; too large and annoying:
if (Util.err.isLoggable(ErrorManager.INFORMATIONAL)) {
Util.err.fine("localizedProps=" + localizedProps);
}
*/
}
}
/** Get all JARs loaded by this module.
* Includes the module itself, any locale variants of the module,
* any extensions specified with Class-Path, any locale variants
* of those extensions.
* The list will be in classpath order (patches first).
* Currently the temp JAR is provided in the case of test modules, to prevent
* sporadic ZIP file exceptions when background threads (like Java parsing) tries
* to open libraries found in the library path.
* JARs already present in the classpath are <em>not</em> listed.
* @return a <code>List&lt;File&gt;</code> of JARs
*/
@Override
public List<File> getAllJars() {
List<File> l = new ArrayList<File>();
Set<File> ptchs = findPatches();
if (ptchs != null) l.addAll(ptchs);
if (physicalJar != null) {
l.add(physicalJar);
} else if (jar != null) {
l.add(jar);
}
((StandardModuleData)data()).addCp(l);
return l;
}
/** Set whether this module is supposed to be reloadable.
* Has no immediate effect, only impacts what happens the
* next time it is enabled (after having been disabled if
* necessary).
* Must be called from within a write mutex.
* @param r whether the module should be considered reloadable
*/
public void setReloadable(boolean r) {
getManager().assertWritable();
if (reloadable != r) {
reloadable = r;
getManager().fireReloadable(this);
}
}
/** Reload this module. Access from ModuleManager.
* If an exception is thrown, the module is considered
* to be in an invalid state.
*/
public void reload() throws IOException {
// Probably unnecessary but just in case:
destroyPhysicalJar();
String codeNameBase1 = getCodeNameBase();
localizedProps = null;
loadManifest();
parseManifest();
// JST: reload not solved yet
// JST findExtensionsAndVariants(manifest);
String codeNameBase2 = getCodeNameBase();
if (! codeNameBase1.equals(codeNameBase2)) {
throw new InvalidException("Code name base changed during reload: " + codeNameBase1 + " -> " + codeNameBase2); // NOI18N
}
}
// Access from ModuleManager:
/** Turn on the classloader. Passed a list of parent modules to use.
* The parents should already have had their classloaders initialized.
*/
protected void classLoaderUp(Set<Module> parents) throws IOException {
if (Util.err.isLoggable(Level.FINE)) {
Util.err.fine("classLoaderUp on " + this + " with parents " + parents);
}
// Find classloaders for dependent modules and parent to them.
List<ClassLoader> loaders = new ArrayList<ClassLoader>(parents.size() + 1);
// This should really be the base loader created by org.nb.Main for loading openide etc.:
loaders.add(Module.class.getClassLoader());
for (Module parent: parents) {
PackageExport[] exports = parent.getPublicPackages();
if (exports != null && exports.length == 0) {
// Check if there is an impl dep here.
boolean implDep = false;
for (Dependency dep : getDependenciesArray()) {
if (dep.getType() == Dependency.TYPE_MODULE &&
dep.getComparison() == Dependency.COMPARE_IMPL &&
dep.getName().equals(parent.getCodeName())) {
implDep = true;
break;
}
}
if (!implDep) {
// Nothing exported from here at all, no sense even adding it.
// Shortcut; would not harm anything to add it now, but we would
// never use it anyway.
// Cf. #27853.
continue;
}
}
ClassLoader l = getParentLoader(parent);
if (parent.isFixed() && loaders.contains(l)) {
Util.err.log(Level.FINE, "#24996: skipping duplicate classloader from {0}", parent);
continue;
}
loaders.add(l);
}
List<File> classp = new ArrayList<File>(3);
Set<File> ptchs = findPatches();
if (ptchs != null) classp.addAll(ptchs);
if (reloadable) {
ensurePhysicalJar();
// Using OPEN_DELETE does not work well with test modules under 1.4.
// Random code (URL handler?) still expects the JAR to be there and
// it is not.
classp.add(physicalJar);
} else {
classp.add(jar);
}
((StandardModuleData)data()).addCp(classp);
// possibly inject some patches
getManager().refineModulePath(this, classp);
// #27853
ClassLoader cld = getManager().refineClassLoader(this, loaders);
// the classloader may be shared, if this module is a fragment
if (cld != null) {
classloader = cld;
} else {
try {
classloader = createNewClassLoader(classp, loaders);
} catch (IllegalArgumentException iae) {
// Should not happen, but just in case.
throw (IOException) new IOException(iae.toString()).initCause(iae);
}
}
}
/** Setup a new module with the given class path and the set of parent
* class loaders.
*/
protected ClassLoader createNewClassLoader(List<File> classp, List<ClassLoader> parents) {
return new OneModuleClassLoader(classp, parents.toArray(new ClassLoader[parents.size()]));
}
/** Get the class loader of a particular parent module. */
protected ClassLoader getParentLoader(Module parent) {
return parent.getClassLoader();
}
/** Turn off the classloader and release all resources. */
protected void classLoaderDown() {
if (classloader instanceof ProxyClassLoader) {
((ProxyClassLoader)classloader).destroy();
}
classloader = null;
}
/** Should be called after turning off the classloader of one or more modules & GC'ing. */
protected void cleanup() {
if (isEnabled()) throw new IllegalStateException("cleanup on enabled module: " + this); // NOI18N
if (classloader != null) throw new IllegalStateException("cleanup on module with classloader: " + this); // NOI18N
// XXX should this rather be done when the classloader is collected?
destroyPhysicalJar();
}
/** Notify the module that it is being deleted. */
public void destroy() {
moduleJARs.remove(jar);
}
/** String representation for debugging. */
public @Override String toString() {
String s = "StandardModule:" + getCodeNameBase() + " jarFile: " + jar.getAbsolutePath(); // NOI18N
if (!isValid()) s += "[invalid]"; // NOI18N
return s;
}
/** PermissionCollection with an instance of AllPermission. */
private static PermissionCollection modulePermissions;
/** @return initialized @see #modulePermission */
private static synchronized PermissionCollection getAllPermission() {
if (modulePermissions == null) {
modulePermissions = new Permissions();
modulePermissions.add(new AllPermission());
modulePermissions.setReadOnly();
}
return modulePermissions;
}
static boolean isModuleJar(File f) {
return moduleJARs.contains(f);
}
private static final Logger CL_LOG = Logger.getLogger(OneModuleClassLoader.class.getName());
/** Class loader to load a single module.
* Auto-localizing, multi-parented, permission-granting, the works.
*/
class OneModuleClassLoader extends JarClassLoader implements Util.ModuleProvider {
/** Create a new loader for a module.
* @param classp the List of all module jars of code directories;
* includes the module itself, its locale variants,
* variants of extensions and Class-Path items from Manifest.
* The items are JarFiles for jars and Files for directories
* @param parents a set of parent classloaders (from other modules)
*/
public OneModuleClassLoader(List<File> classp, ClassLoader[] parents) throws IllegalArgumentException {
super(classp, parents, false, StandardModule.this);
JaveleonModule.registerClassLoader(this, getCodeNameBase());
}
public Module getModule() {
return StandardModule.this;
}
/** Inherited.
* @param cs is ignored
* @return PermissionCollection with an AllPermission instance
*/
protected @Override PermissionCollection getPermissions(CodeSource cs) {
return getAllPermission();
}
/**
* Look up a native library as described in modules documentation.
* @see http://bits.netbeans.org/dev/javadoc/org-openide-modules/org/openide/modules/doc-files/api.html#jni
*/
protected @Override String findLibrary(String libname) {
InstalledFileLocator ifl = InstalledFileLocator.getDefault();
String arch = System.getProperty("os.arch"); // NOI18N
String system = System.getProperty("os.name").toLowerCase(Locale.ENGLISH); // NOI18N
String mapped = System.mapLibraryName(libname);
File lib;
lib = ifl.locate("modules/lib/" + mapped, getCodeNameBase(), false); // NOI18N
if (lib != null) {
CL_LOG.log(Level.FINE, "found {0}", lib);
return lib.getAbsolutePath();
}
lib = ifl.locate("modules/lib/" + arch + "/" + mapped, getCodeNameBase(), false); // NOI18N
if (lib != null) {
CL_LOG.log(Level.FINE, "found {0}", lib);
return lib.getAbsolutePath();
}
lib = ifl.locate("modules/lib/" + arch + "/" + system + "/" + mapped, getCodeNameBase(), false); // NOI18N
if (lib != null) {
CL_LOG.log(Level.FINE, "found {0}", lib);
return lib.getAbsolutePath();
}
if( BaseUtilities.isMac() ) {
String jniMapped = mapped.replaceFirst("\\.dylib$",".jnilib");
lib = ifl.locate("modules/lib/" + jniMapped, getCodeNameBase(), false); // NOI18N
if (lib != null) {
CL_LOG.log(Level.FINE, "found {0}", lib);
return lib.getAbsolutePath();
}
lib = ifl.locate("modules/lib/" + arch + "/" + jniMapped, getCodeNameBase(), false); // NOI18N
if (lib != null) {
CL_LOG.log(Level.FINE, "found {0}", lib);
return lib.getAbsolutePath();
}
lib = ifl.locate("modules/lib/" + arch + "/" + system + "/" + jniMapped, getCodeNameBase(), false); // NOI18N
if (lib != null) {
CL_LOG.log(Level.FINE, "found {0}", lib);
return lib.getAbsolutePath();
}
CL_LOG.log(Level.FINE, "found nothing like modules/lib/{0}/{1}/{2}", new Object[] {arch, system, jniMapped});
}
CL_LOG.log(Level.FINE, "found nothing like modules/lib/{0}/{1}/{2}", new Object[] {arch, system, mapped});
return null;
}
protected @Override boolean shouldDelegateResource(String pkg, ClassLoader parent) {
if (!super.shouldDelegateResource(pkg, parent)) {
return false;
}
Module other;
if (parent instanceof Util.ModuleProvider) {
other = ((Util.ModuleProvider)parent).getModule();
} else {
other = null;
}
return getManager().shouldDelegateResource(StandardModule.this, other, pkg, parent);
}
public @Override String toString() {
return "ModuleCL@" + Integer.toHexString(System.identityHashCode(this)) + "[" + getCodeNameBase() + "]"; // NOI18N
}
}
}