| /* |
| * 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; |
| } |
| } |