blob: a2c20dceab8442fde8467bcc9726c994ab14fb76 [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.pluginimporter;
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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.autoupdate.InstallSupport;
import org.netbeans.api.autoupdate.OperationContainer;
import org.netbeans.api.autoupdate.UpdateElement;
import org.netbeans.api.autoupdate.UpdateManager;
import org.netbeans.api.autoupdate.UpdateUnit;
import org.netbeans.api.progress.ProgressHandle;
import org.openide.DialogDisplayer;
import org.openide.LifecycleManager;
import org.openide.NotifyDescriptor;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileSystem;
import org.openide.filesystems.FileUtil;
import org.openide.modules.SpecificationVersion;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
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;
/**
*
* @author Jiri Rechtacek
*/
public class PluginImporter {
private final Collection<UpdateUnit> plugins;
private boolean inspected = false;
private Collection<UpdateElement> installed = null;
private Collection<UpdateElement> toInstall = null;
private Collection<UpdateElement> toImport = null;
private Collection<UpdateElement> broken = null;
private static final String TRACKING_FILE_NAME = "update_tracking"; // NOI18N
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 String MODULES = "Modules"; // NOI18N
private static final String LAST_MODIFIED = ".lastModified"; // NOI18N
private static final Logger LOG = Logger.getLogger (PluginImporter.class.getName ());
public PluginImporter (Collection<UpdateUnit> foundPlugins) {
plugins = foundPlugins;
}
public void reinspect () {
inspected = false;
inspect();
}
private void inspect () {
if (inspected) {
return ;
}
long start = System.currentTimeMillis();
installed = new HashSet<UpdateElement> ();
toImport = new HashSet<UpdateElement> ();
toInstall = new HashSet<UpdateElement> ();
broken = new HashSet<UpdateElement> ();
Collection<UpdateElement> candidate2import = new HashSet<UpdateElement> ();
List<UpdateUnit> updateUnits = UpdateManager.getDefault ().getUpdateUnits (UpdateManager.TYPE.MODULE);
Map<String, UpdateUnit> cnb2uu = new HashMap<String, UpdateUnit> (updateUnits.size ());
for (UpdateUnit u : updateUnits) {
cnb2uu.put (u.getCodeName (), u);
}
for (UpdateUnit unit : plugins) {
// save information about plugins on common Update Center
UpdateUnit remoteUnit = cnb2uu.get (unit.getCodeName ());
UpdateElement remoteElement = null;
SpecificationVersion remoteSpec = null;
if (remoteUnit != null && ! remoteUnit.getAvailableUpdates ().isEmpty ()) {
remoteElement = remoteUnit.getAvailableUpdates ().get (0);
remoteSpec = remoteElement.getSpecificationVersion() == null ? null : new SpecificationVersion(remoteElement.getSpecificationVersion());
}
if (unit.getInstalled () != null) {
if (! unit.getAvailableUpdates ().isEmpty ()) {
UpdateElement el = unit.getAvailableUpdates ().get (0);
if (remoteElement != null) {
SpecificationVersion spec = el.getSpecificationVersion () == null ? null : new SpecificationVersion (el.getSpecificationVersion ());
if (spec != null && spec.compareTo (remoteSpec) > 0) {
candidate2import.add (el);
}
} else {
candidate2import.add (el);
}
}
installed.add (unit.getInstalled ());
} else if (unit.isPending()) {
LOG.log(Level.INFO, "Plugin " + unit.getCodeName() + " is not installed but is in pending state - i.e. will be installed upon restart, skipping");
} else {
assert ! unit.getAvailableUpdates ().isEmpty () : "If " + unit + " isn't installed thus has available updates.";
UpdateElement el = unit.getAvailableUpdates ().get (0);
if (remoteElement != null) {
SpecificationVersion spec = el.getSpecificationVersion () == null ? null : new SpecificationVersion (el.getSpecificationVersion ());
if (spec != null && spec.compareTo (remoteSpec) > 0) {
candidate2import.add (el);
} else {
toInstall.add (remoteElement);
}
} else {
candidate2import.add (el);
}
}
}
for (UpdateElement el : candidate2import) {
OperationContainer<InstallSupport> oc = el.getUpdateUnit ().getInstalled () == null ?
OperationContainer.createForInstall () :
OperationContainer.createForUpdate ();
try {
OperationContainer.OperationInfo info = oc.add (el);
oc.add (candidate2import);
if (isBlacklisted(el)) {
LOG.info("Plugin " + el + " is on blacklist thus will not be imported.");
} else if (info.getBrokenDependencies ().isEmpty ()) {
toImport.add (el);
} else {
LOG.log (Level.INFO, "Plugin " + el + // NOI18N
" cannot be install because not all dependencies can be match: " + info.getBrokenDependencies ()); // NOI18N
broken.add (el);
}
} catch (IllegalArgumentException iae) {
LOG.log (Level.INFO, iae.getLocalizedMessage (), iae);
broken.add (el);
}
}
long end = System.currentTimeMillis();
LOG.log (Level.INFO, "Inspecting plugins took " + (end - start) + " ms"); // NOI18N
inspected = true;
}
public Collection<UpdateElement> getPluginsToImport () {
inspect ();
return toImport;
}
public Collection<UpdateElement> getInstalledPlugins () {
inspect ();
return installed;
}
public Collection<UpdateElement> getPluginsAvailableToInstall () {
inspect ();
return toInstall;
}
public Collection<UpdateElement> getBrokenPlugins () {
inspect ();
return broken;
}
public void importPlugins (Collection<UpdateElement> plugins, File src, File dest, ProgressHandle handle) throws IOException {
if(handle!=null) {
handle.setInitialDelay(0);
handle.start(plugins.size());
}
List<String> configs = new ArrayList<String> (plugins.size ());
int completed = 0;
for (UpdateElement el : plugins) {
if(handle != null) {
String name = el.getDisplayName();
if(name==null) {
name = el.getCodeName();
}
String detail = NbBundle.getMessage(PluginImporter.class, "PluginImporter.Importing.Plugin", name);//NOI18N
handle.progress(detail, completed ++);
}
String cnb = el.getCodeName ();
// 1. find all plugin's resources
Collection<String> toCopy = getPluginFiles (src, cnb, locateUpdateTracking (cnb, src));
if (toCopy.isEmpty()) {
continue;
}
// 2. copy them
for (String path : toCopy) {
copy (path, src, dest);
}
// 3. find config file
String path = "config/Modules/" + cnb.replace ('.', '-') + ".xml"; // NOI18N
configs.add (path);
}
// 4. find and copy config files in the end
for (String path : configs) {
copy (path, src, dest);
}
// #252928 (fragment modules)
if (getPluginsToImport().isEmpty()) {
refreshModuleList();
} else {
String restartMsg = NbBundle.getMessage(PluginImporter.class, "PluginImporter.Importing.RestartNeeded");//NOI18N
NotifyDescriptor nd = new NotifyDescriptor.Confirmation(restartMsg, NotifyDescriptor.YES_NO_OPTION);
Object result = DialogDisplayer.getDefault().notify(nd);
if (result.equals(NotifyDescriptor.OK_OPTION)) {
LifecycleManager.getDefault().markForRestart();
LifecycleManager.getDefault().exit();
}
}
if(handle!=null) {
handle.finish();
}
}
private static void copy (String path, File sourceFolder, File destFolder) throws IOException {
LOG.finest ("Copy " + path + " from " + sourceFolder + " to " + destFolder);
File src = new File (sourceFolder, path);
assert src.exists () : src + " exists.";
src = FileUtil.normalizeFile (src);
FileObject srcFO = FileUtil.toFileObject (src);
File destFO = new File (destFolder, path);
destFO.getParentFile ().mkdirs ();
File dest = destFO.getParentFile ();
dest = FileUtil.normalizeFile (dest);
FileObject destFolderFO = FileUtil.toFileObject (dest);
File destFile;
if ((destFile = new File (dest, srcFO.getNameExt ())).exists ()) {
if (! destFile.delete ()) {
// if failed delete of the destFile => don't copy, otherwise will cause #159188
return ;
}
}
FileObject res = FileUtil.copyFile (srcFO, destFolderFO, srcFO.getName ());
LOG.finest (srcFO + " was copied to " + destFolderFO + ". Result is: " + res);
}
private static Collection<String> getPluginFiles (File cluster, String cnb, File updateTracking) {
Collection<String> res = new HashSet<String> ();
LOG.log(Level.FINE, "Read update_tracking " + updateTracking + " file.");
Node updateTrackingConf = getUpdateTrackingConf(updateTracking);
if (updateTrackingConf == null) {
return Collections.emptySet();
}
Set<String> moduleFiles = readModuleFiles(updateTrackingConf);
String configFile = "config/Modules/" + cnb.replace ('.', '-') + ".xml"; // NOI18N
moduleFiles.remove (configFile);
for (String fileName : moduleFiles) {
File file = new File (cluster, fileName);
if (! file.exists ()) {
LOG.log (Level.WARNING, "File " + file + " doesn't exist for module " + cnb);
continue;
}
if (file.equals (updateTracking)) {
continue;
}
res.add (fileName);
}
res.add (TRACKING_FILE_NAME + '/' + cnb.replace ('.', '-') + ".xml"); // NOI18N);
LOG.log(Level.FINEST, cnb + " has files: " + res);
return res;
}
private static File locateUpdateTracking (String cnb, File cluster) {
String fileNameToFind = TRACKING_FILE_NAME + '/' + cnb.replace ('.', '-') + ".xml"; // NOI18N
File ut = new File (cluster, fileNameToFind);
if (ut.exists ()) {
return ut;
}
throw new IllegalArgumentException (ut + " doesn't exist."); // NOI18N
}
private static Node getUpdateTrackingConf (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) {
LOG.log(Level.WARNING, "SAXException when reading " + moduleUpdateTracking + ", cause: " + saxe);
//for issue #217118 investigation 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);
}
LOG.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) {
LOG.log(Level.WARNING, null, ioe);
}
assert document.getDocumentElement () != null : "File " + moduleUpdateTracking + " must contain <module> element.";
return getModuleElement (document.getDocumentElement ());
}
private static 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 static 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 static Set<String> readModuleFiles (Node version) {
Set<String> files = new HashSet<String> ();
NodeList fileNodes = version.getChildNodes ();
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 ());
LOG.log(Level.FINE,
"File for import: " +
map.getNamedItem(ATTR_FILE_NAME).getNodeValue());
}
}
return files;
}
private static void refreshModuleList () {
// XXX: the modules list should be refresh automatically when config/Modules/ changes
final FileObject modulesRoot = FileUtil.getConfigFile(MODULES);
LOG.log (Level.FINE,
"It\'s a hack: Call refresh on " + modulesRoot +
" file object.");
if (modulesRoot != null) {
try {
FileUtil.runAtomicAction (new FileSystem.AtomicAction () {
@Override
public void run () throws IOException {
modulesRoot.getParent ().refresh ();
modulesRoot.refresh ();
}
});
} catch (IOException ex) {
Exceptions.printStackTrace (ex);
}
}
}
public static void touchLastModified (File cluster) {
try {
File stamp = new File (cluster, LAST_MODIFIED);
if (! stamp.createNewFile ()) {
stamp.setLastModified (System.currentTimeMillis ());
if (! stamp.setLastModified (System.currentTimeMillis ())) {
stamp.delete ();
stamp = new File (cluster, LAST_MODIFIED);
stamp.setLastModified (System.currentTimeMillis ());
}
}
} catch (IOException ex) {
LOG.log(Level.INFO, ex.getMessage(), ex);
}
}
private static boolean isBlacklisted(UpdateElement el) {
String blacklist = System.getProperty ("plugin.import.blacklist", ""); // NOI18N
if (! blacklist.isEmpty()) {
blacklist = blacklist + ','; // NOI18N
}
blacklist = blacklist + NbBundle.getMessage(PluginImporter.class, "plugin.import.blacklist"); // NOI18N
LOG.fine("Blacklist: " + blacklist);
StringTokenizer tokens = new StringTokenizer(blacklist, ",");
while(tokens.hasMoreTokens()) {
if (el.getCodeName().equals(tokens.nextToken())) {
return true;
}
}
return false;
}
}