blob: babb7ff353856179a467c8f984e3ab4e9f004e10 [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.modules.autoupdate.services;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.modules.autoupdate.updateprovider.InstalledModuleProvider;
import org.netbeans.updater.ModuleDeactivator;
import org.netbeans.updater.UpdateTracking;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.modules.InstalledFileLocator;
import org.openide.modules.ModuleInfo;
import org.openide.xml.XMLUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/** Control if the module's file can be deleted and can delete them from disk.
* <p> Deletes all files what are installed together with given module, info about
* these files read from <code>update_tracking</code> file related to the module.
* If this <code>update_tracking</code> doesn't exist the files cannot be deleted.
* The Deleter waits until the module is enabled before start delete its files.
*
* @author Jiri Rechtacek
*/
public final class ModuleDeleterImpl {
private static final ModuleDeleterImpl INSTANCE = new ModuleDeleterImpl();
private static final String ELEMENT_MODULE = "module"; // NOI18N
private static final String ELEMENT_VERSION = "module_version"; // NOI18N
private static final String ATTR_LAST = "last"; // NOI18N
private static final String ATTR_FILE_NAME = "name"; // NOI18N
private static final Logger err = Logger.getLogger (ModuleDeleterImpl.class.getName ()); // NOI18N
private Set<File> storageFilesForDelete = null;
public static ModuleDeleterImpl getInstance() {
return INSTANCE;
}
public boolean canDelete (ModuleInfo moduleInfo) {
if (moduleInfo == null) { // XXX: how come that moduleInfo is null?
return false;
}
if (Utilities.isEssentialModule (moduleInfo)) {
err.log(Level.FINE,
"Cannot delete module because module " +
moduleInfo.getCodeName() + " isEssentialModule.");
return false;
} else {
return foundUpdateTracking (moduleInfo);
}
}
public Collection<File> markForDisable (Collection<ModuleInfo> modules, ProgressHandle handle) {
if (modules == null) {
throw new IllegalArgumentException ("ModuleInfo argument cannot be null.");
}
if (handle != null) {
handle.switchToDeterminate (modules.size() + 1);
}
Collection<File> configs = new HashSet<File> ();
int i = 0;
for (ModuleInfo moduleInfo : modules) {
File config = locateConfigFile (moduleInfo);
assert config != null : "Located config file for " + moduleInfo.getCodeName ();
assert config.exists () : config + " config file must exists for " + moduleInfo.getCodeName ();
err.log(Level.FINE, "Locate config file of " + moduleInfo.getCodeNameBase () + ": " + config);
if(config!=null) {
configs.add (config);
}
if (handle != null) {
handle.progress (++i);
}
}
return configs;
}
public Collection<File> markForDelete (Collection<ModuleInfo> modules, ProgressHandle handle) throws IOException {
storageFilesForDelete = null;
if (modules == null) {
throw new IllegalArgumentException ("ModuleInfo argument cannot be null.");
}
if (handle != null) {
handle.switchToDeterminate (modules.size () * 2 + 1);
}
Collection<File> configFiles = new HashSet<File> ();
int i = 0;
for (ModuleInfo moduleInfo : modules) {
Collection<File> configs = locateAllConfigFiles (moduleInfo);
assert configs != null : "Located config files for " + moduleInfo.getCodeName ();
assert ! configs.isEmpty () : configs + " config files must exists for " + moduleInfo.getCodeName ();
configFiles.addAll (configs);
err.log(Level.FINE, "Locate config files of " + moduleInfo.getCodeNameBase () + ": " + configs);
if (handle != null) {
handle.progress (++i);
}
}
getStorageFilesForDelete ().addAll (configFiles);
for (ModuleInfo moduleInfo : modules) {
removeModuleFiles(moduleInfo, true);
if (handle != null) {
handle.progress (++i);
}
}
return getStorageFilesForDelete ();
}
@SuppressWarnings("SleepWhileInLoop")
public void delete (final ModuleInfo[] modules, ProgressHandle handle) throws IOException {
storageFilesForDelete = null;
if (modules == null) {
throw new IllegalArgumentException ("ModuleInfo argument cannot be null.");
}
if (handle != null) {
handle.switchToDeterminate (modules.length + 1);
}
int i = 0;
for (ModuleInfo moduleInfo : modules) {
err.log(Level.FINE,"Locate and remove config file of " + moduleInfo.getCodeNameBase ());
removeControlModuleFile(moduleInfo, false);
}
if (handle != null) {
handle.progress (++i);
}
refreshModuleList ();
int rerunWaitCount = 0;
for (ModuleInfo moduleInfo : modules) {
err.log(Level.FINE, "Locate and remove config file of " + moduleInfo.getCodeNameBase ());
if (handle != null) {
handle.progress (moduleInfo.getDisplayName (), ++i);
}
for (; rerunWaitCount < 100 && !isModuleUninstalled(moduleInfo); rerunWaitCount++) {
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
err.log (Level.INFO, "Overflow checks of uninstalled module " + moduleInfo.getCodeName ());
Thread.currentThread().interrupt();
}
}
removeModuleFiles(moduleInfo, false);
}
}
private boolean isModuleUninstalled(ModuleInfo moduleInfo) {
return (InstalledModuleProvider.getInstalledModules ().get (moduleInfo.getCodeNameBase()) == null);
}
private File locateConfigFile (ModuleInfo m) {
String configFile = ModuleDeactivator.CONFIG + '/' + ModuleDeactivator.MODULES + '/' + m.getCodeNameBase ().replace ('.', '-') + ".xml"; // NOI18N
return InstalledFileLocator.getDefault ().locate (configFile, m.getCodeNameBase (), false);
}
private Collection<File> locateAllConfigFiles (ModuleInfo m) {
Collection<File> configFiles = new HashSet<File> ();
String configFileName = m.getCodeNameBase ().replace ('.', '-') + ".xml"; // NOI18N
for (File cluster : UpdateTracking.clusters (true)) {
File configFile = new File (new File (new File (cluster, ModuleDeactivator.CONFIG), ModuleDeactivator.MODULES), configFileName);
if (configFile.exists ()) {
configFiles.add (configFile);
}
}
return configFiles;
}
private void removeControlModuleFile (ModuleInfo m, boolean markForDelete) throws IOException {
File configFile;
while ((configFile = locateConfigFile (m)) != null && ! getStorageFilesForDelete ().contains (configFile)) {
if (configFile != null && configFile.exists ()) {
//FileUtil.toFileObject (configFile).delete ();
if (markForDelete) {
err.log(Level.FINE, "Control file " + configFile + " is marked for delete.");
getStorageFilesForDelete ().add (configFile);
} else {
err.log(Level.FINE, "Try delete the config File " + configFile);
configFile.delete ();
err.log(Level.FINE, "Control file " + configFile + " is deleted.");
}
} else {
err.log(Level.FINE,
"Warning: Config File " + configFile + " doesn\'t exist!");
}
}
}
private boolean foundUpdateTracking (ModuleInfo moduleInfo) {
File updateTracking = Utilities.locateUpdateTracking (moduleInfo);
if (updateTracking != null && updateTracking.exists ()) {
//err.log ("Find UPDATE_TRACKING: " + updateTracking + " found.");
// check the write permission
if (! Utilities.canWrite (updateTracking)) {
err.log(Level.FINE,
"Cannot delete module " + moduleInfo.getCodeName() +
" because is forbidden to write in directory " +
updateTracking.getParentFile ().getParent ());
return false;
} else {
return true;
}
} else {
err.log(Level.FINE,
"Cannot delete module " + moduleInfo.getCodeName() +
" because no update_tracking file found.");
return false;
}
}
private void removeModuleFiles (ModuleInfo m, boolean markForDelete) throws IOException {
err.log (Level.FINE, "Entry removing files of module " + m);
File updateTracking;
while ((updateTracking = Utilities.locateUpdateTracking (m)) != null && ! getStorageFilesForDelete ().contains (updateTracking)) {
removeModuleFilesInCluster (m, updateTracking, markForDelete);
}
err.log (Level.FINE, "Exit removing files of module " + m);
}
private void removeModuleFilesInCluster (ModuleInfo moduleInfo, File updateTracking, boolean markForDelete) throws IOException {
err.log(Level.FINE, "Read update_tracking " + updateTracking + " file.");
Set<String> moduleFiles = readModuleFiles (getModuleConfiguration (updateTracking));
String configFile = ModuleDeactivator.CONFIG + '/' + ModuleDeactivator.MODULES + '/' + moduleInfo.getCodeNameBase ().replace ('.', '-') + ".xml"; // NOI18N
if (moduleFiles.contains (configFile)) {
File file = InstalledFileLocator.getDefault ().locate (configFile, moduleInfo.getCodeNameBase (), false);
if(file!=null && file.exists() && !getStorageFilesForDelete ().contains (file)) {
err.log(Level.WARNING, "Config file " + configFile +
" must be already removed or marked for remove but still exist as file " + file +
" and not found in StorageFilesForDelete : " + getStorageFilesForDelete());
}
}
for (String fileName : moduleFiles) {
if (fileName.equals (configFile)) {
continue;
}
Set <File> files = InstalledFileLocator.getDefault ().locateAll (fileName, moduleInfo.getCodeNameBase (), false);
File file = null;
if (files.size() > 0) {
file = files.iterator().next();
if (files.size() > 1) {
boolean found = false;
for (File f : files) {
if (f.getPath().startsWith(updateTracking.getParentFile().getParentFile().getPath())) {
file = f;
found = true;
break;
}
}
if (!found) {
err.log(Level.WARNING,
"InstalledFileLocator doesn't choose the right file with file name " + fileName
+ " for module " + moduleInfo.getCodeNameBase()
+ " since a few files were returned : " + Arrays.toString(files.toArray()) +
", and since none are in the same cluster as update tracking file " + updateTracking +
" will use " + file);
}
}
}
if (file == null) {
err.log (Level.WARNING, "InstalledFileLocator doesn't locate file " + fileName + " for module " + moduleInfo.getCodeNameBase ());
continue;
}
if (file.equals (updateTracking)) {
continue;
}
assert file.exists () : "File " + file + " exists.";
if (file.exists ()) {
if (markForDelete) {
err.log(Level.FINE, "File " + file + " is marked for delete.");
getStorageFilesForDelete ().add (file);
} else {
try {
FileObject fo = FileUtil.toFileObject (file);
//assert fo != null || !file.exists() : file.getAbsolutePath();
if (fo != null) {
fo.lock().releaseLock();
}
File f = file;
while (f.delete()) {
f = f.getParentFile(); // remove empty dirs too
}
} catch (IOException ioe) {
assert false : "Waring: IOException " + ioe.getMessage () + " was caught. Propably file lock on the file.";
err.log(Level.FINE,
"Waring: IOException " + ioe.getMessage() +
" was caught. Propably file lock on the file.");
err.log(Level.FINE,
"Try call File.deleteOnExit() on " + file);
file.deleteOnExit ();
}
err.log(Level.FINE, "File " + file + " is deleted.");
}
}
}
FileObject trackingFo = FileUtil.toFileObject (updateTracking);
FileLock lock = null;
try {
lock = (trackingFo != null) ? trackingFo.lock() : null;
if (markForDelete) {
err.log(Level.FINE, "Tracking file " + updateTracking + " is marked for delete.");
getStorageFilesForDelete ().add (updateTracking);
} else {
updateTracking.delete ();
err.log(Level.FINE, "Tracking file " + updateTracking + " is deleted.");
}
} finally {
if (lock != null) {
lock.releaseLock();
}
}
err.log(Level.FINE, "File " + updateTracking + " is deleted.");
}
private Node getModuleConfiguration (File moduleUpdateTracking) {
Document document = null;
InputStream is;
try {
is = new BufferedInputStream (new FileInputStream (moduleUpdateTracking));
InputSource xmlInputSource = new InputSource (is);
document = XMLUtil.parse (xmlInputSource, false, false, null, org.openide.xml.EntityCatalog.getDefault ());
if (is != null) {
is.close ();
}
} catch (SAXException saxe) {
err.log(Level.WARNING, "SAXException when reading " + moduleUpdateTracking, saxe);
//for issue #158186 investigation purpose need to add additional logging to see what is corrupted and how
FileReader reader=null;
try {
reader=new FileReader(moduleUpdateTracking);
char[] text=new char[1024];
String fileContent="";
while(reader.read(text)>0)
{
fileContent+=String.copyValueOf(text);
}
err.log(Level.WARNING, "SAXException in file:\n------FILE START------\n " + fileContent+"\n------FILE END-----\n");
}
catch(Exception ex)
{
//don't need to fail in logging
}
finally
{
if(reader!=null)
{
try {
reader.close();
} catch (IOException ex) {
//don't need any info from logging fail
}
}
}
return null;
} catch (IOException ioe) {
err.log(Level.WARNING, "IOException when reading " + moduleUpdateTracking, ioe);
}
assert document.getDocumentElement () != null : "File " + moduleUpdateTracking + " must contain <module> element.";
return getModuleElement (document.getDocumentElement ());
}
private Node getModuleElement (Element element) {
Node lastElement = null;
assert ELEMENT_MODULE.equals (element.getTagName ()) : "The root element is: " + ELEMENT_MODULE + " but was: " + element.getTagName ();
NodeList listModuleVersions = element.getElementsByTagName (ELEMENT_VERSION);
for (int i = 0; i < listModuleVersions.getLength (); i++) {
lastElement = getModuleLastVersion (listModuleVersions.item (i));
if (lastElement != null) {
break;
}
}
return lastElement;
}
private Node getModuleLastVersion (Node version) {
Node attrLast = version.getAttributes ().getNamedItem (ATTR_LAST);
assert attrLast != null : "ELEMENT_VERSION must contain ATTR_LAST attribute.";
if (Boolean.valueOf (attrLast.getNodeValue ()).booleanValue ()) {
return version;
} else {
return null;
}
}
private Set<String> readModuleFiles (Node version) {
if (version == null) {
return Collections.emptySet();
}
NodeList fileNodes = version.getChildNodes ();
if (fileNodes == null) {
return Collections.emptySet();
}
Set<String> files = new HashSet<String> ();
for (int i = 0; i < fileNodes.getLength (); i++) {
if (fileNodes.item (i).hasAttributes ()) {
NamedNodeMap map = fileNodes.item (i).getAttributes ();
files.add (map.getNamedItem (ATTR_FILE_NAME).getNodeValue ());
err.log(Level.FINE,
"Mark to delete: " +
map.getNamedItem(ATTR_FILE_NAME).getNodeValue());
}
}
return files;
}
private void refreshModuleList () {
// XXX: the modules list should be delete automatically when config/Modules/module.xml is removed
FileObject modulesRoot = FileUtil.getConfigFile(ModuleDeactivator.MODULES); // NOI18N
err.log (Level.FINE, "Call refresh on " + modulesRoot + " file object.");
if (modulesRoot != null) {
modulesRoot.refresh ();
}
}
private Set<File> getStorageFilesForDelete () {
if (storageFilesForDelete == null) {
storageFilesForDelete = new HashSet<File> ();
}
return storageFilesForDelete;
}
}