blob: b661b1ea18709bea6dd36f2f4a2d0ba22c241bff [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.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.CharArrayWriter;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PushbackInputStream;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.DuplicateException;
import org.netbeans.Events;
import org.netbeans.InvalidException;
import org.netbeans.Module;
import org.netbeans.ModuleManager;
import org.netbeans.Stamps;
import org.netbeans.Util;
import org.openide.filesystems.FileAttributeEvent;
import org.openide.filesystems.FileChangeListener;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileRenameEvent;
import org.openide.filesystems.FileSystem;
import org.openide.filesystems.FileSystem.AtomicAction;
import org.openide.filesystems.FileUtil;
import org.openide.modules.Dependency;
import org.openide.modules.InstalledFileLocator;
import org.openide.modules.SpecificationVersion;
import org.openide.util.BaseUtilities;
import org.openide.util.Parameters;
import org.openide.util.RequestProcessor;
import org.openide.util.RequestProcessor.Task;
import org.openide.util.WeakSet;
import org.openide.xml.EntityCatalog;
import org.openide.xml.XMLUtil;
import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
/** Class responsible for maintaining the list of modules in the IDE persistently.
* This class understands the "module status" XML format, and the list of modules
* present in the Modules/ folder. And it can keep track of module histories.
* Methods must be called from within appropriate mutex access.
* @author Jesse Glick
*/
final class ModuleList implements Stamps.Updater {
static final RequestProcessor RP = new RequestProcessor("Module List Updates"); // NOI18N
/** The DTD for a module status. */
public static final String PUBLIC_ID = "-//NetBeans//DTD Module Status 1.0//EN"; // NOI18N
public static final String SYSTEM_ID = "http://www.netbeans.org/dtds/module-status-1_0.dtd"; // NOI18N
/** Whether to validate module XML files.
* Safer; only slows down startup in case quickie parse of XML statuses fails for some reason.
*/
private static final boolean VALIDATE_XML = true;
private static final Logger LOG = Logger.getLogger(ModuleList.class.getName());
/** associated module manager */
private final ModuleManager mgr;
/** Modules/ folder containing XML data */
private final FileObject folder;
/** to fire events with */
private final Events ev;
/** map from code name (base)s to statuses of modules on disk */
private final Map<String,DiskStatus> statuses = new HashMap<String,DiskStatus>(100);
/** whether the initial round has been triggered or not */
private boolean triggered = false;
/** listener for changes in modules, etc.; see comment on class Listener */
private final Listener listener = new Listener();
private FileChangeListener weakListener;
/** atomic actions I have used to change Modules/*.xml */
private final Set<FileSystem.AtomicAction> myAtomicActions = Collections.<FileSystem.AtomicAction>synchronizedSet(new WeakSet<FileSystem.AtomicAction>(100));
/** Create the list manager.
* @param mgr the module manager which will actually control the modules at runtime
* @param folder the Modules/ folder on the system file system to scan/write
* @param ev the event logger
*/
public ModuleList(ModuleManager mgr, FileObject folder, Events ev) {
this.mgr = mgr;
this.folder = folder;
this.ev = ev;
LOG.fine("ModuleList created, storage in " + folder);
}
/** Read an initial list of modules from disk according to their stored settings.
* Just reads the XML files in the Modules/ directory, and adds those to
* the manager's list of modules. Errors are handled internally.
* Note that the modules encountered are not turned on at this point even if
* the XML says they should be; but they are added to the list of modules to
* enable as needed. All discovered modules are returned.
* Write mutex only.
*/
public Set<Module> readInitial() {
ev.log(Events.START_READ);
final Set<Module> read = new HashSet<Module>();
try {
folder.getFileSystem().runAtomicAction(new ReadInitial(read));
} catch (IOException ioe) {
LOG.log(Level.WARNING, null, ioe);
}
return read;
}
final Module createModule(
File jarFile, ModuleHistory hist, boolean reloadable, boolean autoload,
boolean eager, Integer startLevel
) throws IOException {
Module m;
try {
if (startLevel != null) {
m = mgr.createBundle(jarFile, hist, reloadable, autoload, eager, startLevel);
} else {
m = mgr.create(jarFile, hist, reloadable, autoload, eager);
}
} catch (DuplicateException dupe) {
// XXX should this be tolerated somehow? In case the original is
// in fact scheduled for deletion anyway?
throw new IOException(dupe);
}
return m;
}
/**
* Try to find a module JAR by an XML-supplied name.
* @param jar the JAR name (relative to an install dir, or a full path)
* @param name code name base of the module JAR
* @return an actual JAR file
* @throws FileNotFoundException if no such JAR file could be found on disk
* @throws IOException if something else was wrong
*/
private File findJarByName(String jar, String name) throws IOException {
File f = new File(jar);
if (f.isAbsolute()) {
if (!f.isFile()) throw new FileNotFoundException(f.getAbsolutePath());
return f;
} else {
Set<File> jars = InstalledFileLocator.getDefault().locateAll(jar, name, false);
if (jars.isEmpty()) {
throw new FileNotFoundException(jar);
} else if (jars.size() == 1 || Boolean.getBoolean("org.netbeans.core.startup.ModuleList.firstModuleJarWins")) {
return jars.iterator().next();
} else {
// Pick the newest one available.
int major = -1;
SpecificationVersion spec = null;
File newest = null;
for (File candidate : jars) {
int candidateMajor = -1;
SpecificationVersion candidateSpec = null;
JarFile jf = new JarFile(candidate);
try {
java.util.jar.Attributes attr = jf.getManifest().getMainAttributes();
String codename = attr.getValue("OpenIDE-Module");
if (codename != null) {
int slash = codename.lastIndexOf('/');
if (slash != -1) {
candidateMajor = Integer.parseInt(codename.substring(slash + 1));
}
}
String sv = attr.getValue("OpenIDE-Module-Specification-Version");
if (sv != null) {
candidateSpec = new SpecificationVersion(sv);
}
} finally {
jf.close();
}
if (newest == null || candidateMajor > major || (spec != null && candidateSpec != null && candidateSpec.compareTo(spec) > 0)) {
newest = candidate;
major = candidateMajor;
spec = candidateSpec;
}
}
return newest;
}
}
}
/** Actually go ahead and enable modules which were queued up by
* reading methods. Should be done after as many modules
* are collected as possible, in case they have odd mutual
* dependencies. Also begins listening to further changes.
* Pass in a list of boot modules which you would
* like to also try to enable now.
*/
public void trigger(Set<Module> boot) {
ev.log(Events.PERF_START, "ModuleList.trigger"); // NOI18N
if (triggered) throw new IllegalStateException("Duplicate call to trigger()"); // NOI18N
Set<Module> maybeEnable = new HashSet<Module>(boot);
for (DiskStatus status: statuses.values()) {
if (status.pendingInstall) {
// We are going to try to turn it on...
status.pendingInstall = false;
Module m = status.module;
if (m.isEnabled() || m.isAutoload() || m.isEager()) throw new IllegalStateException();
maybeEnable.add(m);
}
}
ev.log(Events.PERF_TICK, "modules to enable prepared"); // NOI18N
if (! maybeEnable.isEmpty()) {
ev.log(Events.START_AUTO_RESTORE, maybeEnable);
installNew(maybeEnable);
ev.log(Events.FINISH_AUTO_RESTORE, maybeEnable);
}
LOG.fine("ModuleList.trigger: enabled new modules, flushing changes...");
triggered = true;
flushInitial();
ev.log(Events.PERF_END, "ModuleList.trigger"); // NOI18N
}
// XXX is this method still needed? rethink...
private void installNew(Set<Module> modules) {
if (modules.isEmpty()) {
return;
}
ev.log(Events.PERF_START, "ModuleList.installNew"); // NOI18N
// First suppress all autoloads.
Iterator<Module> it = modules.iterator();
while (it.hasNext()) {
Module m = it.next();
if (m.isAutoload() || m.isEager()) {
it.remove();
} else if (m.isEnabled()) {
// Can happen in obscure circumstances: old module A
// now exists again but with dependency on new module B,
// and a complete build was not done for A+B, so they have
// no existing Modules/ *.xml. In such a case B will already
// have been turned on when restoring A; harmless to remove
// it from the list here.
LOG.fine("#17295 fix active for " + m.getCodeNameBase());
it.remove();
} else if (!m.isValid()) {
// Again can also happen if the user upgrades from one version
// of a module to another. In this case ModuleList correctly removed
// the old dead module from the manager's list, however it is still
// in the set of modules to restore.
LOG.fine("#17471 fix active for " + m.getCodeNameBase());
it.remove();
}
}
List<Module> toEnable = mgr.simulateEnable(modules);
for (Module m: toEnable) {
if (m.isAutoload() || m.isEager()) {
continue;
}
// Quietly turn on others as well:
if (! modules.contains(m)) {
modules.add(m);
}
}
Set<Module> missing = new HashSet<Module>(modules);
missing.removeAll(toEnable);
if (! missing.isEmpty()) {
// Include also problematic autoloads and so on needed by these modules.
Util.transitiveClosureModuleDependencies(mgr, missing);
it = missing.iterator();
while (it.hasNext()) {
Module m = it.next();
if (m.getProblems().isEmpty()) {
it.remove();
}
}
ev.log(Events.FAILED_INSTALL_NEW, missing);
modules.removeAll(missing);
}
try {
mgr.enable(modules);
} catch (InvalidException ie) {
LOG.log(Level.INFO, null, ie);
Module bad = ie.getModule();
if (bad == null) throw new IllegalStateException();
Set<Module> affectedModules = mgr.getModuleInterdependencies(bad, true, true, true);
ev.log(Events.FAILED_INSTALL_NEW_UNEXPECTED, bad, affectedModules, ie);
modules.removeAll (affectedModules);
// Try again without it. Note that some other dependent modules might
// then be in the missing list for the second round.
installNew(modules);
}
ev.log(Events.PERF_END, "ModuleList.installNew"); // NOI18N
}
/** Read an XML file using an XMLReader and parse into a map of properties.
* One distinguished property 'name' is the code name base
* and is taken from the root element. Others are taken
* from the param elements.
* Properties are of type String, Boolean, Integer, or SpecificationVersion
* according to the property name.
* @param is the input stream
* @param reader the XML reader to use to parse; may be null
* @return a map of named properties to values of various types
*/
private Map<String,Object> readStatus(InputSource is, XMLReader reader) throws IOException, SAXException {
if (reader == null) {
reader = XMLUtil.createXMLReader(VALIDATE_XML);
reader.setEntityResolver(listener);
reader.setErrorHandler(listener);
}
final Map<String,Object> m = new HashMap<String,Object>();
DefaultHandler handler = new DefaultHandler() {
private String modName;
private String paramName;
private StringBuffer data = new StringBuffer();
public @Override void startElement(String uri,
String localname,
String qname,
Attributes attrs) throws SAXException {
if ("module".equals(qname) ) { // NOI18N
modName = attrs.getValue("name"); // NOI18N
if( modName == null )
throw new SAXException("No module name"); // NOI18N
m.put("name", modName.intern()); // NOI18N
}
else if (modName != null && "param".equals(qname)) { // NOI18N
paramName = attrs.getValue("name");
if( paramName == null ) {
throw new SAXException("No param name"); // NOI18N
}
paramName = paramName.intern();
data.setLength(0);
}
}
public @Override void characters(char[] ch, int start, int len) {
if(modName != null && paramName != null)
data.append( ch, start, len );
}
public @Override void endElement (String uri, String localname, String qname)
throws SAXException
{
if ("param".equals(qname)) { // NOI18N
if (modName != null && paramName != null) {
if (data.length() == 0)
throw new SAXException("No text contents in " + paramName + " of " + modName); // NOI18N
try {
m.put(paramName, processStatusParam(paramName, data.toString()));
} catch (NumberFormatException nfe) {
// From either Integer or SpecificationVersion constructors.
throw (SAXException) new SAXException(nfe.toString()).initCause(nfe);
}
data.setLength(0);
paramName = null;
}
}
else if ("module".equals(qname)) { // NOI18N
modName = null;
}
}
};
reader.setContentHandler(handler);
reader.parse(is);
sanityCheckStatus(m);
return m;
}
/** Parse a param value according to a natural type.
* @param k the param name (must be interned!)
* @param v the raw string value from XML
* @return some parsed value suitable for the status map
*/
private Object processStatusParam(String k, String v) throws NumberFormatException {
if (k == "enabled" // NOI18N
|| k == "autoload" // NOI18N
|| k == "eager" // NOI18N
|| k == "reloadable" // NOI18N
) {
return Boolean.valueOf(v);
} else {
if (k == "startlevel") { // NOI18N
return Integer.valueOf(v);
}
// Other properties are of type String.
// Intern the smaller ones which are likely to be repeated somewhere.
if (v.length() < 100) v = v.intern();
return v;
}
}
/** Just checks that all the right stuff is there.
*/
private void sanityCheckStatus(Map<String,Object> m) throws IOException {
String jar = (String) m.get("jar"); // NOI18N
if (jar == null) {
throw new IOException("Must define jar param"); // NOI18N
}
if (Boolean.TRUE.equals(m.get("autoload")) && m.get("enabled") != null) { // NOI18N
throw new IOException("Autoload " + jar + " cannot specify enablement");
}
if (Boolean.TRUE.equals(m.get("eager")) && m.get("enabled") != null) { // NOI18N
throw new IOException("Eager " + jar + " cannot specify enablement");
}
}
// Encoding irrelevant for these getBytes() calls: all are ASCII...
// (unless someone has their system encoding set to UCS-16!)
private static final byte[] MODULE_XML_INTRO = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE module PUBLIC \"-//NetBeans//DTD Module Status 1.0//EN\"\n \"http://www.netbeans.org/dtds/module-status-1_0.dtd\">\n<module name=\"".getBytes(); // NOI18N
// private static final byte[] MODULE_XML_DIV1 = ">\n <param name=\"".getBytes(); // NOI18N
private static final byte[] MODULE_XML_INTRO_END = ">\n".getBytes(); // NOI18N
private static final byte[] MODULE_XML_DIV2 = " <param name=\"".getBytes(); // NOI18N
private static final byte[] MODULE_XML_DIV3 = "/param>\n".getBytes(); // NOI18N
private static final byte[] MODULE_XML_END = "/module>\n".getBytes(); // NOI18N
/** Just like {@link #readStatus(InputSource,XMLReader)} but avoids using an XML parser.
* If it does not manage to parse it this way, it returns null, in which case
* you have to use a real parser.
* @see "#26786"
*/
private Map<String, Object> readStatus(InputStream is, boolean checkEOF) throws IOException {
PushbackInputStream pbis = new PushbackInputStream(is, 1);
Map<String,Object> m = new HashMap<String,Object>(15);
if (!expect(pbis, MODULE_XML_INTRO)) {
LOG.fine("Could not read intro");
return null;
}
String name = readTo(pbis, '"');
if (name == null) {
LOG.fine("Could not read code name base");
return null;
}
m.put("name", name.intern()); // NOI18N
if (!expect(pbis, MODULE_XML_INTRO_END)) {
LOG.fine("Could not read stuff after cnb");
return null;
}
// Now we have <param>s some number of times, finally </module>.
PARSE:
while (true) {
int c = pbis.read();
switch (c) {
case ' ':
// <param>
if (!expect(pbis, MODULE_XML_DIV2)) {
LOG.fine("Could not read up to param");
return null;
}
String k = readTo(pbis, '"');
if (k == null) {
LOG.fine("Could not read param");
return null;
}
k = k.intern();
if (pbis.read() != '>') {
LOG.fine("No > at end of <param> " + k);
return null;
}
String v = readTo(pbis, '<');
if (v == null) {
LOG.fine("Could not read value of " + k);
return null;
}
if (!expect(pbis, MODULE_XML_DIV3)) {
LOG.fine("Could not read end of param " + k);
return null;
}
try {
m.put(k, processStatusParam(k, v));
} catch (NumberFormatException nfe) {
LOG.fine("Number misparse: " + nfe);
return null;
}
break;
case '<':
// </module>
if (!expect(pbis, MODULE_XML_END)) {
LOG.fine("Strange ending");
return null;
}
if (!checkEOF) {
break PARSE;
}
if (pbis.read() != -1) {
LOG.fine("Trailing garbage");
return null;
}
// Success!
break PARSE;
default:
LOG.fine("Strange stuff after <param>s: " + c);
return null;
}
}
sanityCheckStatus(m);
return m;
}
/** Read some stuff from a stream and skip over it.
* Newline conventions are normalized to Unix \n.
* @return true upon success, false if stream contained something else
*/
private boolean expect(PushbackInputStream is, byte[] stuff) throws IOException {
int len = stuff.length;
boolean inNewline = false;
for (int i = 0; i < len; ) {
int c = is.read();
if (c == 10 || c == 13) {
// Normalize: s/[\r\n]+/\n/g
if (inNewline) {
continue;
} else {
inNewline = true;
c = 10;
}
} else {
inNewline = false;
}
if (c != stuff[i++]) {
return false;
}
}
if (stuff[len - 1] == 10) {
// Expecting something ending in a \n - so we have to
// read any further \r or \n and discard.
int c = is.read();
if (c != -1 && c != 10 && c != 13) {
// Got some non-newline character, push it back!
is.unread(c);
}
}
return true;
}
/** Read a maximal string until delim is encountered (which will be removed from stream).
* This impl reads only ASCII, for speed.
* Newline conventions are normalized to Unix \n.
* @return the read string, or null if the delim is not encountered before EOF.
*/
private String readTo(InputStream is, char delim) throws IOException {
if (delim == 10) {
// Not implemented - stream might have "foo\r\n" and we would
// return "foo" and leave "\n" in the stream.
throw new IOException("Not implemented"); // NOI18N
}
CharArrayWriter caw = new CharArrayWriter(100);
boolean inNewline = false;
while (true) {
int c = is.read();
if (c == -1) return null;
if (c > 126) return null;
if (c == 10 || c == 13) {
// Normalize: s/[\r\n]+/\n/g
if (inNewline) {
continue;
} else {
inNewline = true;
c = 10;
}
} else if (c < 32 && c != 9) {
// Random control character!
return null;
} else {
inNewline = false;
}
if (c == delim) {
return caw.toString();
} else {
caw.write(c);
}
}
}
final Map<String,Map<String,Object>> readCache() {
InputStream is = Stamps.getModulesJARs().asStream("all-modules.dat"); // NOI18N
if (is == null) {
// schedule write for later
writeCache();
return null;
}
LOG.log(Level.FINEST, "Reading cache all-modules.dat");
try {
ObjectInputStream ois = new ObjectInputStream(is);
Map<String,Map<String,Object>> ret = new HashMap<String, Map<String, Object>>(1333);
while (is.available() > 0) {
Map<String, Object> prop = readStatus(ois, false);
if (prop == null) {
LOG.log(Level.CONFIG, "Cache is invalid all-modules.dat");
return null;
}
Set<?> deps;
try {
deps = (Set<?>) ois.readObject();
} catch (ClassNotFoundException ex) {
throw new IOException(ex);
}
prop.put("deps", deps);
String cnb = (String)prop.get("name"); // NOI18N
ret.put(cnb, prop);
}
is.close();
return ret;
} catch (IOException ex) {
LOG.log(Level.INFO, "Cannot read cache", ex);
writeCache();
return null;
}
}
final void writeCache() {
Stamps.getModulesJARs().scheduleSave(this, "all-modules.dat", false);
}
@Override
public void cacheReady() {
}
@Override
public void flushCaches(DataOutputStream os) throws IOException {
ObjectOutputStream oss = new ObjectOutputStream(os);
for (Module m : mgr.getModules()) {
if (m.isFixed()) {
continue;
}
Map<String, Object> prop = computeProperties(m);
writeStatus(prop, oss);
oss.writeObject(m.getDependencies());
}
}
/** Write a module's status to disk in the form of an XML file.
* The map of parameters must contain one named 'name' with the code
* name base of the module.
*/
private void writeStatus(Map<String, Object> m, OutputStream os) throws IOException {
String codeName = (String)m.get("name"); // NOI18N
if (codeName == null)
throw new IllegalArgumentException("no code name present"); // NOI18N
Writer w = new OutputStreamWriter(os, StandardCharsets.UTF_8);
w.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); // NOI18N
w.write("<!DOCTYPE module PUBLIC \""); // NOI18N
w.write(PUBLIC_ID);
w.write("\"\n \""); // NOI18N
w.write(SYSTEM_ID);
w.write("\">\n"); // NOI18N
w.write("<module name=\""); // NOI18N
w.write(XMLUtil.toAttributeValue(codeName)); // NOI18N
w.write("\">\n"); // NOI18N
// Use TreeMap to sort the keys by name; since the module status files might
// be version-controlled we want to avoid gratuitous format changes.
for (Map.Entry<String, Object> entry: new TreeMap<String, Object>(m).entrySet()) {
String name = entry.getKey();
if (
name.equals("name") || // NOI18N
name.equals("deps") // NOI18N
) {
// Skip this one, it is a pseudo-param.
continue;
}
Object val = entry.getValue();
w.write(" <param name=\""); // NOI18N
w.write(XMLUtil.toAttributeValue(name)); // NOI18N
w.write("\">"); // NOI18N
w.write(XMLUtil.toElementContent(val.toString()));
w.write("</param>\n"); // NOI18N
}
w.write("</module>\n"); // NOI18N
w.flush();
}
/** Write information about a module out to disk.
* If the old status is given as null, this is a newly
* added module; create an appropriate status and return it.
* Else update the existing status and return it (it is
* assumed properties are already updated).
* Should write the XML and create/rewrite/delete the serialized
* installer file as needed.
*/
private DiskStatus writeOut(Module m, DiskStatus old) throws IOException {
final DiskStatus nue;
if (old == null) {
nue = new DiskStatus();
nue.module = m;
nue.setDiskProps(computeProperties(m));
} else {
nue = old;
}
FileSystem.AtomicAction aa = new FileSystem.AtomicAction() {
public void run() throws IOException {
if (nue.file == null) {
nue.file = FileUtil.createData(folder, ((String)nue.diskProps.get("name")).replace('.', '-') + ".xml"); // NOI18N
} else {
// Just verify that no one else touched it since we last did.
if (/*nue.lastApprovedChange != nue.file.lastModified().getTime()*/nue.dirty) {
// Oops, something is wrong. #156764 - log at lower level.
LOG.log(Level.INFO, null, new IOException("Will not clobber external changes in " + nue.file));
return;
}
}
LOG.fine("ModuleList: (re)writing " + nue.file);
FileLock lock = nue.file.lock();
try {
OutputStream os = nue.file.getOutputStream(lock);
try {
writeStatus(nue.diskProps, os);
} finally {
os.close();
}
} finally {
lock.releaseLock();
}
//nue.lastApprovedChange = nue.file.lastModified().getTime();
}
};
myAtomicActions.add(aa);
folder.getFileSystem().runAtomicAction(aa);
return nue;
}
/** Delete a module from disk.
*/
private void deleteFromDisk(final Module m, final DiskStatus status) throws IOException {
final String nameDashes = m.getCodeNameBase().replace('.', '-'); // NOI18N
//final long expectedTime = status.lastApprovedChange;
FileSystem.AtomicAction aa = new FileSystem.AtomicAction() {
public void run() throws IOException {
FileObject xml = folder.getFileObject(nameDashes, "xml"); // NOI18N
if (xml == null) {
// Could be that the XML was already deleted externally, etc.
LOG.fine("ModuleList: " + m + "'s XML already gone from disk");
return;
}
//if (xml == null) throw new IOException("No such XML file: " + nameDashes + ".xml"); // NOI18N
if (status.dirty) {
// Someone wrote to the file since we did. Don't delete it blindly!
// XXX should this throw an exception, or just warn??
throw new IOException("Unapproved external change to " + xml); // NOI18N
}
LOG.fine("ModuleList: deleting " + xml);
/*
if (xml.lastModified().getTime() != expectedTime) {
// Someone wrote to the file since we did. Don't delete it blindly!
throw new IOException("Unapproved external change to " + xml); // NOI18N
}
*/
xml.delete();
FileObject ser = folder.getFileObject(nameDashes, "ser"); // NOI18N
if (ser != null) {
LOG.fine("(and also " + ser + ")");
ser.delete();
}
}
};
myAtomicActions.add(aa);
folder.getFileSystem().runAtomicAction(aa);
}
/** Flush the initial state of the module installer after startup to disk.
* This means:
* 1. Find all modules in the manager.
* 2. Anything for which we have no status, write out its XML now
* and create a status object for it.
* 3. Anything for which we have a status, compare the status we
* have to its current state (don't forget the installer
* serialization state--if this is nonnull, that counts as an
* automatic change because it means the module was loaded and
* needed to store something).
* 4. For any changes found in 3., write out new XML (and if
* there is any installer state, a new installer ser).
* 5. Attach listeners to the manager and all modules to catch further
* changes in the system so they may be flushed.
* We could in principle start listening right after readInitial()
* but it should be more efficient to wait and see what has really
* changed. Also, some XML may say that a module is enabled, and in
* fact trigger() was not able to turn it on. In that case, this will
* show up as a change in step 3. and we will rewrite it as disabled.
* Called within write mutex by trigger().
*/
private void flushInitial() {
LOG.fine("Flushing initial module list...");
// Find all modules for which we have status already. Treat
// them as possibly changed, and attach listeners.
for (Module m : mgr.getModules()) {
DiskStatus status = statuses.get(m.getCodeNameBase());
if (status != null) {
moduleChanged(m, status);
m.addPropertyChangeListener(listener);
}
}
// Now find all new and deleted modules.
moduleListChanged();
// And listener for new or deleted modules.
mgr.addPropertyChangeListener(listener);
}
/** Does the real work when the list of modules changes.
* Finds newly added modules, creates XML and status for
* them and begins listening for changes; finds deleted
* modules, removes their listener, XML, and status.
* May be called within read or write mutex; since it
* could be in the read mutex, synchronize (on statuses).
*/
final void moduleListChanged() {
synchronized (statuses) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("ModuleList: moduleListChanged; statuses=" + statuses);
}
// Newly added modules first.
for (Module m : mgr.getModules()) {
if (m.isFixed() || m.getJarFile() == null) {
// No way, we don't manage these.
continue;
}
final String name = m.getCodeNameBase();
if (statuses.get(name) == null) {
// Yup, it's new. Write it out.
LOG.fine("moduleListChanged: added: " + m);
try {
statuses.put(name, writeOut(m, null));
m.addPropertyChangeListener(listener);
} catch (IOException ioe) {
LOG.log(Level.WARNING, null, ioe);
// XXX Now what? Keep it in our list or what??
}
}
}
// Now deleted & recreated modules.
Iterator<DiskStatus> it = statuses.values().iterator();
while (it.hasNext()) {
DiskStatus status = it.next();
if (! status.module.isValid()) {
status.module.removePropertyChangeListener(listener);
Module nue = mgr.get(status.module.getCodeNameBase());
if (nue != null) {
// Deleted, but a new module with the same code name base
// was created (#5922 e.g.). So change the module reference
// in the status and write out any changes to disk.
LOG.fine("moduleListChanged: recreated: " + nue);
nue.addPropertyChangeListener(listener);
status.module = nue;
moduleChanged(nue, status);
} else {
// Newly deleted.
LOG.fine("moduleListChanged: deleted: " + status.module);
it.remove();
try {
deleteFromDisk(status.module, status);
} catch (IOException ioe) {
LOG.log(Level.WARNING, null, ioe);
}
}
}
}
}
}
/** Does the real work when one module changes.
* Compares old and new state and writes XML
* (and perhaps serialized installer state) as needed.
* May be called within read or write mutex; since it
* could be in the read mutex, synchronize (on status).
*/
private void moduleChanged(Module m, DiskStatus status) {
synchronized (status) {
LOG.log(Level.FINE, "moduleChanged: {0}", m);
Map<String,Object> newProps = computeProperties(m);
int cnt = 0;
for (Map.Entry<String, Object> entry : status.diskProps.entrySet()) {
if (entry.getKey().equals("deps")) { // NOI18N
continue;
}
Object snd = newProps.get(entry.getKey());
if (!entry.getValue().equals(snd)) {
cnt = -1;
break;
}
cnt++;
}
if (cnt != newProps.size()) {
if (LOG.isLoggable(Level.FINE)) {
Set<Map.Entry<String,Object>> changes = new HashSet<Map.Entry<String,Object>>(newProps.entrySet());
changes.removeAll(status.diskProps.entrySet());
LOG.fine("ModuleList: changes are " + changes);
}
// We need to write changes.
status.setDiskProps(newProps);
try {
writeOut(m, status);
} catch (IOException ioe) {
LOG.log(Level.WARNING, null, ioe);
// XXX now what? continue to manage it anyway?
}
writeCache();
}
}
}
/** Compute what properties we would want to store in XML
* for this module. I.e. 'name', 'reloadable', etc.
*/
private Map<String,Object> computeProperties(Module m) {
if (m.isFixed() || ! m.isValid()) throw new IllegalArgumentException("fixed or invalid: " + m); // NOI18N
Map<String,Object> p = new HashMap<String,Object>();
p.put("name", m.getCodeNameBase()); // NOI18N
if (!m.isAutoload() && !m.isEager()) {
p.put("enabled", m.isEnabled()); // NOI18N
}
p.put("autoload", m.isAutoload()); // NOI18N
p.put("eager", m.isEager()); // NOI18N
p.put("reloadable", m.isReloadable()); // NOI18N
if (m.getStartLevel() > 0) {
p.put("startlevel", m.getStartLevel()); // NOI18N
}
if (m.getHistory() instanceof ModuleHistory) {
ModuleHistory hist = (ModuleHistory) m.getHistory();
p.put("jar", hist.getJar()); // NOI18N
}
return p;
}
final void init() {
weakListener = FileUtil.weakFileChangeListener(listener, folder);
folder.getChildren();
folder.addFileChangeListener(weakListener);
}
final void shutDown() {
folder.removeFileChangeListener(weakListener);
}
/** Listener for changes in set of modules and various properties of individual modules.
* Also serves as a strict error handler for XML parsing.
* Also listens to changes in the Modules/ folder and processes them in req proc.
*/
private final class Listener implements PropertyChangeListener, ErrorHandler, EntityResolver, FileChangeListener, Runnable {
private final RequestProcessor.Task task;
Listener() {
task = RP.create(this);
}
// Property change coming from ModuleManager or some known Module.
private boolean listening = true;
public void propertyChange(PropertyChangeEvent evt) {
if (! triggered) throw new IllegalStateException("Property change before trigger()"); // NOI18N
// REMEMBER this is inside *read* mutex, it is forbidden to even attempt
// to get write access synchronously here!
String prop = evt.getPropertyName();
Object src = evt.getSource();
if (!listening) {
// #27106: do not react to our own changes while we are making them
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("ModuleList: ignoring own change " + prop + " from " + src);
}
return;
}
if (ModuleManager.PROP_CLASS_LOADER.equals(prop) ||
ModuleManager.PROP_ENABLED_MODULES.equals(prop) ||
Module.PROP_CLASS_LOADER.equals(prop) ||
Module.PROP_PROBLEMS.equals(prop) ||
Module.PROP_VALID.equals(prop)) {
// Properties we are not directly interested in, ignore.
// Note that rather than paying attention to PROP_VALID
// we simply deal with deletions when PROP_MODULES is fired.
return;
} else if (ModuleManager.PROP_MODULES.equals(prop)) {
moduleListChanged();
} else if (src instanceof Module) {
// enabled, manifest, reloadable, possibly other stuff in the future
Module m = (Module)src;
if (! m.isValid()) {
// Skip it. We will get PROP_MODULES sometime anyway.
return;
}
DiskStatus status = statuses.get(m.getCodeNameBase());
if (status == null) {
throw new IllegalStateException("Unknown module " + m + "; statuses=" + statuses); // NOI18N
}
if (status.pendingInstall && Module.PROP_ENABLED.equals(prop)) {
throw new IllegalStateException("Got PROP_ENABLED on " + m + " before trigger()"); // NOI18N
}
moduleChanged(m, status);
} else {
LOG.fine("Unexpected property change: " + evt + " prop=" + prop + " src=" + src);
}
}
// SAX stuff.
public void warning(SAXParseException e) throws SAXException {
LOG.log(Level.WARNING, null, e);
}
public void error(SAXParseException e) throws SAXException {
throw e;
}
public void fatalError(SAXParseException e) throws SAXException {
throw e;
}
public InputSource resolveEntity(String pubid, String sysid) throws SAXException, IOException {
if (pubid.equals(PUBLIC_ID)) {
if (VALIDATE_XML) {
// We certainly know where to get this from.
return new InputSource(ModuleList.class.getResource("module-status-1_0.dtd").toExternalForm()); // NOI18N
} else {
// Not validating, don't load any DTD! Significantly faster.
return new InputSource(new ByteArrayInputStream(new byte[0]));
}
} else {
// Otherwise try the standard places.
return EntityCatalog.getDefault().resolveEntity(pubid, sysid);
}
}
// Changes in Modules/ folder.
public void fileDeleted(FileEvent ev) {
if (isOurs(ev)) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("ModuleList: got expected deletion " + ev);
}
return;
}
FileObject fo = ev.getFile();
fileDeleted0(fo.getName(), fo.getExt()/*, ev.getTime()*/);
}
public void fileDataCreated(FileEvent ev) {
if (isOurs(ev)) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("ModuleList: got expected creation " + ev);
}
return;
}
FileObject fo = ev.getFile();
fileCreated0(fo.getName(), fo.getExt()/*, ev.getTime()*/);
}
public void fileRenamed(FileRenameEvent ev) {
if (isOurs(ev)) {
throw new IllegalStateException("I don't rename anything! " + ev); // NOI18N
}
FileObject fo = ev.getFile();
fileDeleted0(ev.getName(), ev.getExt()/*, ev.getTime()*/);
fileCreated0(fo.getName(), fo.getExt()/*, ev.getTime()*/);
}
private void fileCreated0(String name, String ext/*, long time*/) {
if ("xml".equals(ext)) { // NOI18N
String codenamebase = name.replace('-', '.');
DiskStatus status = statuses.get(codenamebase);
LOG.fine("ModuleList: outside file creation event for " + codenamebase);
if (status != null) {
// XXX should this really happen??
status.dirty = true;
}
runme();
} else if ("ser".equals(ext)) { // NOI18N
// XXX handle newly added installers?? or not
} // else ignore
}
private void fileDeleted0(String name, String ext/*, long time*/) {
if ("xml".equals(ext)) { // NOI18N
// Removed module.
String codenamebase = name.replace('-', '.');
DiskStatus status = statuses.get(codenamebase);
LOG.fine("ModuleList: outside file deletion event for " + codenamebase);
if (status != null) {
// XXX should this ever happen?
status.dirty = true;
}
runme();
} else if ("ser".equals(ext)) { // NOI18N
// XXX handle newly deleted installers?? or not
} // else ignore
}
public void fileChanged(FileEvent ev) {
if (isOurs(ev)) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("ModuleList: got expected modification " + ev);
}
return;
}
FileObject fo = ev.getFile();
String name = fo.getName();
String ext = fo.getExt();
if ("xml".equals(ext)) { // NOI18N
// Changed module.
String codenamebase = name.replace('-', '.');
DiskStatus status = statuses.get(codenamebase);
LOG.fine("ModuleList: outside file modification event for " + codenamebase + ": " + ev);
if (status != null) {
status.dirty = true;
} else {
// XXX should this ever happen?
}
runme();
} else if ("ser".equals(ext)) { // NOI18N
// XXX handle changes of installers?? or not
} // else ignore
}
public void fileFolderCreated(FileEvent ev) {
// ignore
}
public void fileAttributeChanged(FileAttributeEvent ev) {
// ignore
}
/** Check if a given file event in the Modules/ folder was a result
* of our own manipulations, as opposed to some other code (or polled
* refresh) manipulating one of these XML files. See #15573.
*/
private boolean isOurs(FileEvent ev) {
for (FileSystem.AtomicAction action : myAtomicActions) {
if (ev.firedFrom(action)) {
return true;
}
}
return false;
}
// Dealing with changes in Modules/ folder and processing them.
private void runme() {
task.schedule(100);
}
@Override
public void run() {
LOG.fine("ModuleList: will process outstanding external XML changes");
mgr.mutexPrivileged().enterWriteAccess();
try {
folder.getFileSystem().runAtomicAction(new FileSystem.AtomicAction() {
public void run() throws IOException {
// 1. For any dirty XML for which status exists but reloadable differs from XML: change.
// 2. For any XML for which we have no status: create & create status, as disabled.
// 3. For all dirty XML which says enabled but status says disabled: batch-enable as possible.
// (Where not possible, mark disabled in XML??)
// 4. For all dirty XML which says disabled but status says enabled: batch-disable plus others.
// 5. For all status for which no XML exists: batch-disable plus others, then delete.
// 6. For any dirty XML for which jar/autoload/eager/release/specversion differs from
// actual state of module: warn but do nothing.
// 7. For now, ignore any changes in *.ser.
// 8. For any dirty XML for which status now exists: replace diskProps with contents of XML.
// 9. Mark all statuses clean.
// Code name to module XMLs found on disk:
Map<String,FileObject> xmlfiles = prepareXMLFiles();
// Code name to properties for dirty XML or XML sans status only.
Map<String,Map<String,Object>> dirtyprops = prepareDirtyProps(xmlfiles);
// #27106: do not listen to changes we ourselves produce.
// It only matters if statuses has not been updated before
// the changes are fired.
listening = false;
try {
stepCheckReloadable(dirtyprops);
stepCreate(xmlfiles, dirtyprops);
stepEnable(dirtyprops);
stepDisable(dirtyprops);
stepDelete(xmlfiles);
stepCheckMisc(dirtyprops);
stepCheckSer(xmlfiles, dirtyprops);
} finally {
listening = true;
stepUpdateProps(dirtyprops);
stepMarkClean();
}
}
});
LOG.fine("ModuleList: finished processing outstanding external XML changes");
} catch (IOException ioe) {
LOG.log(Level.WARNING, null, ioe);
} finally {
mgr.mutexPrivileged().exitWriteAccess();
}
}
// All the steps called from the run() method to handle disk changes:
private Map<String,FileObject> prepareXMLFiles() {
LOG.fine("ModuleList: prepareXMLFiles");
Map<String,FileObject> xmlfiles = new HashMap<String,FileObject>(100);
FileObject[] kids = folder.getChildren();
for (int i = 0; i < kids.length; i++) {
if (kids[i].hasExt("xml")) { // NOI18N
xmlfiles.put(kids[i].getName().replace('-', '.'), kids[i]);
}
}
return xmlfiles;
}
private Map<String,Map<String,Object>> prepareDirtyProps(Map<String,FileObject> xmlfiles) throws IOException {
LOG.fine("ModuleList: prepareDirtyProps");
Map<String,Map<String,Object>> dirtyprops = new HashMap<String,Map<String,Object>>(100);
for (Map.Entry<String,FileObject> entry : xmlfiles.entrySet()) {
String cnb = entry.getKey();
DiskStatus status = statuses.get(cnb);
if (status == null || status.dirty) {
FileObject xmlfile = entry.getValue();
if (xmlfile == null || ! xmlfile.canRead ()) {
continue;
}
Random eth = null;
for (int repeats = 0; ; repeats++) {
InputStream is = xmlfile.getInputStream();
try {
InputSource src = new InputSource(is);
src.setSystemId(xmlfile.toURL().toString());
try {
dirtyprops.put(cnb, readStatus(src, null));
} catch (SAXException saxe) {
final String msg = "Parse error:\n---%<--- " + xmlfile.getPath() + "\n" +
xmlfile.asText("UTF-8") + "\n---%<---\ngot: " + saxe;
if (repeats < 10) {
int wait;
if (eth == null) {
eth = new Random();
}
wait = eth.nextInt(90) + 10;
LOG.warning(msg);
LOG.log(Level.INFO, "Retry: {0} after waiting {1}", new Object[] { repeats, wait });
try {
Thread.sleep(wait);
} catch (InterruptedException ignore) {
// not that important
}
continue;
}
throw new IOException(msg, saxe);
}
} finally {
is.close();
}
break;
}
}
}
return dirtyprops;
}
private void stepCheckReloadable(Map<String,Map<String,Object>> dirtyprops) {
LOG.fine("ModuleList: stepCheckReloadable");
for (Map.Entry<String,Map<String,Object>> entry : dirtyprops.entrySet()) {
String cnb = entry.getKey();
DiskStatus status = statuses.get(cnb);
if (status != null) {
Map<String,Object> props = entry.getValue();
Boolean diskReloadableB = (Boolean)props.get("reloadable"); // NOI18N
boolean diskReloadable = (diskReloadableB != null ? diskReloadableB.booleanValue() : false);
boolean memReloadable = status.module.isReloadable();
if (memReloadable != diskReloadable) {
LOG.fine("Disk change in reloadable for " + cnb + " from " + memReloadable + " to " + diskReloadable);
status.module.setReloadable(diskReloadable);
}
}
}
}
private void stepCreate(Map<String,FileObject> xmlfiles, Map<String,Map<String,Object>> dirtyprops) throws IOException {
LOG.fine("ModuleList: stepCreate");
for (Map.Entry<String,FileObject> entry : xmlfiles.entrySet()) {
String cnb = entry.getKey();
if (! statuses.containsKey(cnb)) {
FileObject xmlfile = entry.getValue();
Map<String, Object> props = dirtyprops.get(cnb);
if (! cnb.equals(props.get("name"))) throw new IOException("Code name mismatch"); // NOI18N
String jar = (String)props.get("jar"); // NOI18N
File jarFile;
try {
jarFile = findJarByName(jar, cnb);
} catch (FileNotFoundException fnfe) {
final File file = new File(fnfe.getMessage());
ev.log(Events.MISSING_JAR_FILE, file, true);
final File p = file.getParentFile();
File[] arr = p.listFiles();
LOG.log(Level.FINE, "Content of {0} is:", p); // NOI18N
int cnt = 0;
if (arr != null) {
for (File f : arr) {
LOG.log(Level.FINE, "{0}. = {1}", new Object[] { ++cnt, f }); // NOI18N
}
LOG.log(Level.FINE, "There was {0} files", cnt); // NOI18N
} else {
LOG.fine("Directory does not exist"); // NOI18N
}
dirtyprops.remove(cnb); // #159001
continue;
}
Boolean reloadableB = (Boolean)props.get("reloadable"); // NOI18N
boolean reloadable = (reloadableB != null ? reloadableB.booleanValue() : false);
Boolean autoloadB = (Boolean)props.get("autoload"); // NOI18N
boolean autoload = (autoloadB != null ? autoloadB.booleanValue() : false);
Boolean eagerB = (Boolean)props.get("eager"); // NOI18N
boolean eager = (eagerB != null ? eagerB.booleanValue() : false);
Integer startLevel = (Integer)props.get("startlevel"); // NOI18N
ModuleHistory hist = new ModuleHistory(jar, "created from " + xmlfile);
Module m = createModule(jarFile, hist, reloadable, autoload, eager, startLevel);
m.addPropertyChangeListener(this);
// Mark the status as disabled for the moment, so in step 3 it will be turned on
// if in dirtyprops it was marked enabled.
Map<String, Object> statusProps;
if (props.get("enabled") != null && ((Boolean)props.get("enabled")).booleanValue()) { // NOI18N
statusProps = new HashMap<String, Object>(props);
statusProps.put("enabled", Boolean.FALSE); // NOI18N
} else {
statusProps = props;
}
DiskStatus status = new DiskStatus();
status.module = m;
status.file = xmlfile;
status.setDiskProps(statusProps);
statuses.put(cnb, status);
}
}
}
private void stepEnable(Map<String,Map<String,Object>> dirtyprops) throws IOException {
LOG.fine("ModuleList: stepEnable");
if (LOG.isLoggable(Level.FINEST)) {
for (Entry<String, Map<String, Object>> e : dirtyprops.entrySet()) {
LOG.log(Level.FINEST, "{0} = {1}", new Object[]{e.getKey(), e.getValue()}); // NOI18N
}
}
Set<Module> toenable = new HashSet<Module>();
for (Map.Entry<String,Map<String,Object>> entry : dirtyprops.entrySet()) {
String cnb = entry.getKey();
Map<String, Object> props = entry.getValue();
if (props.get("enabled") != null && ((Boolean)props.get("enabled")).booleanValue()) { // NOI18N
DiskStatus status = statuses.get(cnb);
assert status != null : cnb; // #159001
if (status.diskProps.get("enabled") == null || ! ((Boolean)status.diskProps.get("enabled")).booleanValue()) { // NOI18N
if (status.module.isEnabled()) throw new IllegalStateException("Already enabled: " + status.module); // NOI18N
toenable.add(status.module);
}
}
}
if (LOG.isLoggable(Level.FINEST)) {
for (Module m : toenable) {
LOG.log(Level.FINEST, "About to enable {0}", m); // NOI18N
}
}
installNew(toenable);
}
private void stepDisable(Map<String,Map<String,Object>> dirtyprops) throws IOException {
LOG.fine("ModuleList: stepDisable");
Set<Module> todisable = new HashSet<Module>();
for (Map.Entry<String,Map<String,Object>> entry: dirtyprops.entrySet()) {
String cnb = entry.getKey();
Map<String, Object> props = entry.getValue();
if (props.get("enabled") == null || ! ((Boolean)props.get("enabled")).booleanValue()) { // NOI18N
DiskStatus status = statuses.get(cnb);
assert status != null : cnb; // #159001
if (Boolean.TRUE.equals(status.diskProps.get("enabled"))) { // NOI18N
if (! status.module.isEnabled()) throw new IllegalStateException("Already disabled: " + status.module); // NOI18N
todisable.add(status.module);
}
}
}
if (todisable.isEmpty()) {
return;
}
List<Module> reallydisable = mgr.simulateDisable(todisable);
for (Module m: reallydisable) {
if (!m.isAutoload() && !m.isEager() && !todisable.contains(m)) {
todisable.add(m);
}
}
mgr.disable(todisable);
}
private void stepDelete(Map<String,FileObject> xmlfiles) throws IOException {
LOG.fine("ModuleList: stepDelete");
Set<Module> todelete = new HashSet<Module>();
Iterator<Map.Entry<String,DiskStatus>> it = statuses.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String,DiskStatus> entry = it.next();
String cnb = entry.getKey();
DiskStatus status = entry.getValue();
if (! xmlfiles.containsKey(cnb)) {
Module m = status.module;
todelete.add(m);
it.remove();
}
}
if (todelete.isEmpty()) {
return;
}
Set<Module> todisable = new HashSet<Module>();
for (Module m: todelete) {
if (m.isEnabled() && !m.isAutoload() && !m.isEager()) {
todisable.add(m);
}
}
List<Module> reallydisable = mgr.simulateDisable(todisable);
for (Module m: reallydisable) {
if (!m.isAutoload() && !m.isEager() && !todisable.contains(m)) {
todisable.add(m);
}
}
mgr.disable(todisable);
// In case someone tried to delete an enabled autoload/eager module:
Iterator<Module> delIt = todelete.iterator();
while (delIt.hasNext()) {
Module m = delIt.next();
if (m.isEnabled()) {
if (!m.isAutoload() && !m.isEager()) throw new IllegalStateException("Module " + m + " scheduled for deletion could not be disabled yet was not an autoload nor eager"); // NOI18N
// XXX is it better to find all regular module using it and turn all of those off?
ev.log(Events.CANT_DELETE_ENABLED_AUTOLOAD, m);
delIt.remove();
} else {
mgr.delete(m);
}
}
}
private void stepCheckMisc(Map<String,Map<String,Object>> dirtyprops) {
LOG.fine("ModuleList: stepCheckMisc");
String[] toCheck = {"jar", "autoload", "eager"}; // NOI18N
for (Map.Entry<String,Map<String,Object>> entry : dirtyprops.entrySet()) {
String cnb = entry.getKey();
Map<String,Object> props = entry.getValue();
DiskStatus status = statuses.get(cnb);
assert status != null : cnb; // #159001
Map<String,Object> diskProps = status.diskProps;
for (int i = 0; i < toCheck.length; i++) {
String prop = toCheck[i];
Object onDisk = props.get(prop);
Object inMem = diskProps.get(prop);
if (! BaseUtilities.compareObjects(onDisk, inMem)) {
ev.log(Events.MISC_PROP_MISMATCH, status.module, prop, onDisk, inMem);
}
}
}
}
private void stepCheckSer(Map<String,FileObject> xmlfiles, Map<String,Map<String,Object>> dirtyprops) {
// There is NO step 7!
}
private void stepUpdateProps(Map<String,Map<String,Object>> dirtyprops) {
LOG.fine("ModuleList: stepUpdateProps");
for (Map.Entry<String,Map<String,Object>> entry: dirtyprops.entrySet()) {
String cnb = entry.getKey();
DiskStatus status = statuses.get(cnb);
if (status != null) {
Map<String,Object> props = entry.getValue();
status.setDiskProps(props);
}
}
}
private void stepMarkClean() {
LOG.fine("ModuleList: stepMarkClean");
for (DiskStatus status : statuses.values()) {
status.dirty = false;
}
}
}
/** Representation of the status of a module on disk and so on. */
private static final class DiskStatus {
/** Initialize as a struct, i.e. member by member: */
public DiskStatus() {}
/** actual module object */
public Module module;
/** XML file holding its status */
public FileObject file;
/** timestamp of last modification to XML file that this class did */
//public long lastApprovedChange;
/** if true, this module was scanned and should be enabled but we are waiting for trigger */
public boolean pendingInstall = false;
/** properties of the module on disk */
public Map<String,Object /*String|Integer|Boolean|SpecificationVersion*/> diskProps;
void setDiskProps(Map<String,Object> diskProps) {
Parameters.notNull("diskProps", diskProps);
this.diskProps = diskProps;
}
/** if true, the XML was changed on disk by someone else */
public boolean dirty = false;
/** for debugging: */
public @Override String toString() {
return "DiskStatus[module=" + module + // NOI18N
",valid=" + module.isValid() + // NOI18N
",file=" + file + /*",lastApprovedChange=" + new Date(lastApprovedChange) +*/ // NOI18N
",dirty=" + dirty + // NOI18N
",pendingInstall=" + pendingInstall + // NOI18N
",diskProps=" + diskProps + "]"; // NOI18N
}
}
private class ReadInitial implements AtomicAction, Runnable {
private final Set<Module> read;
private volatile Task task;
public ReadInitial(Set<Module> read) {
this.read = read;
}
@Override
public void run() {
if (task != null) {
init();
return;
}
task = RP.create(this);
task.schedule(0);
Map<String, Map<String, Object>> cache = readCache();
String[] names;
if (cache != null) {
names = cache.keySet().toArray(new String[cache.size()]);
} else {
FileObject[] children = folder.getChildren();
List<String> arr = new ArrayList<String>(children.length);
for (FileObject f : children) {
if (f.hasExt("ser")) { // NOI18N
// Fine, skip over.
} else if (f.hasExt("xml")) {
// NOI18N
// Assume this is one of ours. Note fixed naming scheme.
String nameDashes = f.getName(); // NOI18N
char[] badChars = {'.', '/', '>', '='};
for (int j = 0; j < 4; j++) {
if (nameDashes.indexOf(badChars[j]) != -1) {
throw new IllegalArgumentException("Bad name: " + nameDashes); // NOI18N
}
}
String name = nameDashes.replace('-', '.').intern(); // NOI18N
arr.add(name);
} else {
LOG.fine("Strange file encountered in modules folder: " + f);
}
}
names = arr.toArray(new String[arr.size()]);
}
ev.log(Events.MODULES_FILE_SCANNED, names.length);
XMLReader reader = null;
for (int i = 0; i < names.length; i++) {
String name = names[i];
FileObject f = null;
try {
// OK, read it from disk.
Map<String, Object> props = cache == null ? null : cache.get(name);
if (props == null) {
// Now name is the code name base of the module we expect to find.
// Check its format (throws IllegalArgumentException if bad):
Dependency.create(Dependency.TYPE_MODULE, name);
LOG.log(Level.FINEST, "no cache for {0}", name);
f = folder.getFileObject(name.replace('.', '-') + ".xml");
InputStream is = f.getInputStream();
try {
props = readStatus(new BufferedInputStream(is), true);
if (props == null) {
LOG.warning("Note - failed to parse " + f + " the quick way, falling back on XMLReader");
is.close();
is = f.getInputStream();
InputSource src = new InputSource(is);
// Make sure any includes etc. are handled properly:
src.setSystemId(f.toURL().toExternalForm());
if (reader == null) {
try {
reader = XMLUtil.createXMLReader();
} catch (SAXException e) {
throw (IllegalStateException) new IllegalStateException(e.toString()).initCause(e);
}
reader.setEntityResolver(listener);
reader.setErrorHandler(listener);
}
props = readStatus(src, reader);
}
} finally {
is.close();
}
}
if (!name.equals(props.get("name"))) {
throw new IOException("Code name mismatch: " + name + " vs. " + props.get("name")); // NOI18N
}
Boolean enabledB = (Boolean) props.get("enabled"); // NOI18N
String jar = (String) props.get("jar"); // NOI18N
File jarFile;
try {
jarFile = findJarByName(jar, name);
} catch (FileNotFoundException fnfe) {
//LOG.fine("Cannot find: " + fnfe.getMessage());
ev.log(Events.MISSING_JAR_FILE, new File(fnfe.getMessage()), enabledB);
if (f != null && !Boolean.FALSE.equals(enabledB)) {
try {
f.delete();
} catch (IOException ioe) {
LOG.log(Level.WARNING, null, ioe);
}
}
continue;
}
ModuleHistory history = new ModuleHistory(jar, "loaded from " + f); // NOI18N
Boolean reloadableB = (Boolean) props.get("reloadable"); // NOI18N
boolean reloadable = reloadableB != null ? reloadableB.booleanValue() : false;
boolean enabled = enabledB != null ? enabledB.booleanValue() : false;
Boolean autoloadB = (Boolean) props.get("autoload"); // NOI18N
boolean autoload = autoloadB != null ? autoloadB.booleanValue() : false;
Boolean eagerB = (Boolean) props.get("eager"); // NOI18N
boolean eager = eagerB != null ? eagerB.booleanValue() : false;
NbInstaller.register(name, props.get("deps")); // NOI18N
Integer startLevel = (Integer)props.get("startlevel"); // NOI18N
Module m = createModule(jarFile, history, reloadable, autoload, eager, startLevel);
NbInstaller.register(null, null);
read.add(m);
DiskStatus status = new DiskStatus();
status.module = m;
status.file = f;
//status.lastApprovedChange = children[i].lastModified().getTime();
status.pendingInstall = enabled;
// Will only really be flushed if mgr props != disk props, i.e
// if version changed or could not be enabled.
//status.pendingFlush = true;
status.setDiskProps(props);
statuses.put(name, status);
} catch (Exception e) {
LOG.log(Level.WARNING, "Error encountered while reading " + name, e);
}
ev.log(Events.MODULES_FILE_PROCESSED, name);
}
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("read initial XML files: statuses=" + statuses);
}
ev.log(Events.FINISH_READ, read);
// Handle changes in the Modules/ folder on disk by parsing & applying them.
task.waitFinished();
}
}
}