/*
* RapidMiner
*
* Copyright (C) 2001-2014 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.rapid_i.deployment.update.client;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import javax.swing.SwingUtilities;
import javax.xml.namespace.QName;
import javax.xml.ws.BindingProvider;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import com.rapidminer.RapidMiner;
import com.rapidminer.RapidMiner.ExecutionMode;
import com.rapidminer.deployment.client.wsimport.AccountService;
import com.rapidminer.deployment.client.wsimport.AccountServiceService;
import com.rapidminer.deployment.client.wsimport.PackageDescriptor;
import com.rapidminer.deployment.client.wsimport.UpdateService;
import com.rapidminer.deployment.client.wsimport.UpdateServiceException_Exception;
import com.rapidminer.deployment.client.wsimport.UpdateServiceService;
import com.rapidminer.gui.RapidMinerGUI;
import com.rapidminer.gui.security.UserCredential;
import com.rapidminer.gui.security.Wallet;
import com.rapidminer.gui.tools.PasswordDialog;
import com.rapidminer.gui.tools.ProgressThread;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.gui.tools.VersionNumber;
import com.rapidminer.gui.tools.dialogs.ConfirmDialog;
import com.rapidminer.gui.tools.dialogs.ExtendedErrorDialog;
import com.rapidminer.io.process.XMLTools;
import com.rapidminer.tools.FileSystemService;
import com.rapidminer.tools.I18N;
import com.rapidminer.tools.LogService;
import com.rapidminer.tools.ParameterService;
import com.rapidminer.tools.PasswortInputCanceledException;
import com.rapidminer.tools.ProgressListener;
import com.rapidminer.tools.Tools;
import com.rapidminer.tools.WebServiceTools;
/**
* This class manages the updates of the core and installation and updates of extensions.
*
* @author Simon Fischer
*/
public class UpdateManager {
public static final String PACKAGE_TYPE_RAPIDMINER_PLUGIN = "RAPIDMINER_PLUGIN";
public static final String PACKAGE_TYPE_STAND_ALONE = "STAND_ALONE";
/** The platform architecture as specified in the manifest. */
public static final String TARGET_PLATFORM;
/** If true, the {@link #TARGET_PLATFORM} was not correctly identified in the manifest, so we
* are probably using a development version which means we cannot update RapidMiner itself. */
public static final boolean DEVELOPMENT_BUILD;
public static final String PARAMETER_UPDATE_INCREMENTALLY = "rapidminer.update.incremental";
public static final String PARAMETER_UPDATE_URL = "rapidminer.update.url";
public static final String PARAMETER_INSTALL_TO_HOME = "rapidminer.update.to_home";
public static final String UPDATESERVICE_URL = "http://marketplace.rapid-i.com:80/UpdateServer";
public static final String PACKAGEID_RAPIDMINER = "rapidminer";
public static final String COMMERCIAL_LICENSE_NAME = "RIC";
public static final String NEVER_REMIND_INSTALL_EXTENSIONS_FILE_NAME = "ignored_extensions.xml";
static {
String implementationVersion = UpdateManager.class.getPackage().getImplementationVersion();
if (implementationVersion == null) {
LogService.getRoot().log(Level.WARNING, "com.rapid_i.deployment.update.client.UpdateManager.error_development_build");
TARGET_PLATFORM = "ANY";
DEVELOPMENT_BUILD = true;
} else {
int dashPos = implementationVersion.lastIndexOf("-") + 1;
if (dashPos != -1) {
String suffix = implementationVersion.substring(dashPos).trim();
if (!suffix.isEmpty()) {
TARGET_PLATFORM = suffix;
DEVELOPMENT_BUILD = false;
} else {
LogService.getRoot().log(Level.WARNING, "com.rapid_i.deployment.update.client.UpdateManager.illegal_implementation_version", implementationVersion);
TARGET_PLATFORM = "ANY";
DEVELOPMENT_BUILD = true;
}
} else {
LogService.getRoot().log(Level.WARNING, "com.rapid_i.deployment.update.client.UpdateManager.illegal_implementation_version", implementationVersion);
TARGET_PLATFORM = "ANY";
DEVELOPMENT_BUILD = true;
}
}
}
private static final UpdateServerAccount usAccount = new UpdateServerAccount();
private final UpdateService service;
public UpdateManager(UpdateService service) {
super();
this.service = service;
}
/**
* @throws IOException */
private InputStream openStream(URL url, ProgressListener listener, int minProgress, int maxProgress) throws IOException {
HttpURLConnection con = (HttpURLConnection) url.openConnection();
WebServiceTools.setURLConnectionDefaults(con);
con.setDoInput(true);
con.setDoOutput(false);
String lengthStr = con.getHeaderField("Content-Length");
InputStream urlIn;
try {
urlIn = con.getInputStream();
} catch (IOException e) {
throw new IOException(con.getResponseCode() + ": " + con.getResponseMessage(), e);
}
if (lengthStr == null || lengthStr.isEmpty()) {
//LogService.getRoot().warning("Server did not send content length.");
LogService.getRoot().log(Level.WARNING, "com.rapid_i.deployment.update.client.UpdateManager.sending_content_length_error");
return urlIn;
} else {
try {
long length = Long.parseLong(lengthStr);
return new ProgressReportingInputStream(urlIn, listener, minProgress, maxProgress, length);
} catch (NumberFormatException e) {
//LogService.getRoot().log(Level.WARNING, "Server sent illegal content length: "+lengthStr, e);
LogService.getRoot().log(Level.WARNING,
I18N.getMessage(LogService.getRoot().getResourceBundle(),
"com.rapid_i.deployment.update.client.UpdateManager.sending_illegal_content_length_error",
lengthStr),
e);
return urlIn;
}
}
}
/**
* method loads down the required packages and installs them
* @param downloadList list of the required Packages
* @param progressListener current ProgressListener
* @return returns the number of successful updates or downloads
* @throws IOException
* @throws UpdateServiceException_Exception
*/
public List<PackageDescriptor> performUpdates(List<PackageDescriptor> downloadList, ProgressListener progressListener) throws IOException, UpdateServiceException_Exception {
for (Iterator<PackageDescriptor> iterator = downloadList.iterator(); iterator.hasNext();) {
PackageDescriptor desc = iterator.next();
if (PACKAGE_TYPE_STAND_ALONE.equals(desc.getPackageTypeName()) && DEVELOPMENT_BUILD) {
SwingTools.showVerySimpleErrorMessageAndWait("update_error_development_build");
iterator.remove();
continue;
}
}
int i = 0;
//number of failed Downloads
int failedLoads = 0;
//number of all available Downloads
int availableLoads = downloadList.size();
List<PackageDescriptor> installedPackage = new LinkedList<PackageDescriptor>();
try {
for (final PackageDescriptor desc : downloadList) {
String urlString = service.getDownloadURL(desc.getPackageId(), desc.getVersion(), desc.getPlatformName());
int minProgress = 20 + 80 * i / downloadList.size();
int maxProgress = 20 + 80 * (i + 1) / downloadList.size();
boolean incremental = UpdateManager.isIncrementalUpdate();
if (PACKAGE_TYPE_RAPIDMINER_PLUGIN.equals(desc.getPackageTypeName())) {
ManagedExtension extension = ManagedExtension.getOrCreate(desc.getPackageId(), desc.getName(), desc.getLicenseName());
String baseVersion = extension.getLatestInstalledVersionBefore(desc.getVersion());
incremental &= baseVersion != null;
URL url = UpdateManager.getUpdateServerURI(urlString +
(incremental ? "?baseVersion=" + URLEncoder.encode(baseVersion, "UTF-8") : "")).toURL();
boolean succesful = false;
if (incremental) {
//LogService.getRoot().info("Updating "+desc.getPackageId()+" incrementally.");
LogService.getRoot().log(Level.INFO, "com.rapid_i.deployment.update.client.UpdateManager.updating_package_id_incrementally", desc.getPackageId());
try {
updatePluginIncrementally(extension, openStream(url, progressListener, minProgress, maxProgress), baseVersion, desc.getVersion(), urlString + "?baseVersion=" + URLEncoder.encode(baseVersion, "UTF-8") + "&md5");
succesful = true;
} catch (IOException e) {
// if encountering problems during incremental installation, try using standard.
//LogService.getRoot().warning("Incremental Update failed. Trying to fall back on non incremental Update...");
LogService.getRoot().warning("com.rapid_i.deployment.update.client.UpdateManager.incremental_update_error");
incremental = false;
url = UpdateManager.getUpdateServerURI(urlString).toURL();
} catch (final ChecksumException e) {
// if encountering problems during incremental installation, try using standard.
//LogService.getRoot().warning("Incremental Update failed. Trying to fall back on non incremental Update...");
LogService.getRoot().warning("com.rapid_i.deployment.update.client.UpdateManager.incremental_update_error");
incremental = false;
url = UpdateManager.getUpdateServerURI(urlString).toURL();
}
}
// try standard non incremental way
if (!incremental) {
//LogService.getRoot().info("Updating "+desc.getPackageId()+".");
LogService.getRoot().log(Level.INFO, "com.rapid_i.deployment.update.client.UpdateManager.updating_package_id", desc.getPackageId());
try {
updatePlugin(extension, openStream(url, progressListener, minProgress, maxProgress), desc.getVersion(), urlString + "?md5");
succesful = true;
} catch (final IOException e) {
failedLoads++;
reportDownloadError(e, desc.getName());
} catch (final ChecksumException e) {
failedLoads++;
reportChecksumError(e, desc.getName());
}
}
if(succesful) {
installedPackage.add(desc);
extension.addAndSelectVersion(desc.getVersion());
} else {
// if extension was not installed before, remove it from managed extensions
if(baseVersion == null) {
ManagedExtension.remove(desc.getPackageId());
}
}
} else if (PACKAGE_TYPE_STAND_ALONE.equals(desc.getPackageTypeName())) {
if (DEVELOPMENT_BUILD) {
SwingTools.showVerySimpleErrorMessageAndWait("update_error_development_build");
continue;
}
URL url = UpdateManager.getUpdateServerURI(urlString +
(incremental ? "?baseVersion=" + URLEncoder.encode(RapidMiner.getLongVersion(), "UTF-8") : "")).toURL();
LogService.getRoot().log(Level.INFO, "com.rapid_i.deployment.update.client.UpdateManager.updating_rapidminer_core");
try {
updateRapidMiner(openStream(url, progressListener, minProgress, maxProgress), desc.getVersion(), urlString + (incremental ? "?baseVersion=" + URLEncoder.encode(RapidMiner.getLongVersion(), "UTF-8") + "&md5" : "?md5"));
installedPackage.add(desc);
} catch (ChecksumException e) {
failedLoads++;
reportChecksumError(e, desc.getName());
} catch (IOException e) {
failedLoads++;
reportDownloadError(e, desc.getName());
}
} else {
SwingTools.showVerySimpleErrorMessageAndWait("updatemanager.unknown_package_type", desc.getName(), desc.getPackageTypeName());
}
i++;
progressListener.setCompleted(20 + 80 * i / downloadList.size());
}
} catch (URISyntaxException e) {
throw new IOException(e);
} finally {
progressListener.complete();
}
if ((availableLoads == failedLoads) && (availableLoads > 0)) {
SwingTools.showVerySimpleErrorMessageAndWait("updatemanager.updates_installed_partially", String.valueOf(availableLoads-failedLoads), String.valueOf(availableLoads));
}
return installedPackage;
}
private void reportChecksumError(final ChecksumException e, final String failedPackageName) {
LogService.getRoot().log(Level.INFO,
I18N.getMessage(LogService.getRoot().getResourceBundle(), "com.rapid_i.deployment.update.client.UpdateManager.md5_failed", failedPackageName, e.getMessage()),
e);
//show MD5-Error to the user
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
ExtendedErrorDialog dialog = new ExtendedErrorDialog("update_md5_error", e, true, new Object[] { failedPackageName, e.getMessage() });
dialog.setModal(true);
dialog.setVisible(true);
}
});
} catch (InvocationTargetException e1) {
LogService.getRoot().log(Level.WARNING, "Error showing error message: "+e, e);
} catch (InterruptedException e1) { }
}
private void reportDownloadError(final IOException e, final String failedPackageName) {
LogService.getRoot().log(Level.INFO,
I18N.getMessage(LogService.getRoot().getResourceBundle(), "com.rapid_i.deployment.update.client.UpdateManager.md5_failed", failedPackageName, e.getMessage()),
e);
//show MD5-Error to the user
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
ExtendedErrorDialog dialog = new ExtendedErrorDialog("error_downloading_package", e, true, new Object[] { failedPackageName, e.getMessage() });
dialog.setModal(true);
dialog.setVisible(true);
}
});
} catch (InvocationTargetException e1) {
LogService.getRoot().log(Level.WARNING, "Error showing error message: "+e, e);
} catch (InterruptedException e1) { }
}
/*
* Creates a path to the directory of the User-directory instead of the program files-directory.
* so that there is no io excetption because of the userlevel
private File getDestinationPluginFile(ManagedExtension extension, String newVersion) throws IOException {
File outFile = extension.getDestinationFile(newVersion);
String destPath = outFile.getPath();
String[] parts = destPath.split("/");
destPath = "";
boolean go = false;
for (int i = 0; i < parts.length; i++) {
if (go) {
destPath = destPath + "\\" + parts[i];
}
if (parts[i].compareTo("RapidMiner") == 0)
go = true;
}
outFile = new File(FileSystemService.getUserRapidMinerDir().getPath() + "\\RUinstall" + destPath);
return outFile;
}
*/
private void updatePlugin(ManagedExtension extension, InputStream updateIn, String newVersion, String md5Adress) throws IOException, ChecksumException {
File outFile = extension.getDestinationFile(newVersion);
OutputStream out = new FileOutputStream(outFile);
try {
Tools.copyStreamSynchronously(updateIn, out, true);
} finally {
try {
out.close();
} catch (IOException e) {}
}
if (!compareMD5(outFile, md5Adress)) {
Tools.delete(outFile);
throw new ChecksumException();
}
}
@SuppressWarnings("resource")
// stream is closed by copyStreamSynchronously
private void updateRapidMiner(InputStream openStream, String version, String md5adress) throws IOException, ChecksumException {
if (DEVELOPMENT_BUILD) {
SwingTools.showVerySimpleErrorMessage("update_error_development_build");
}
//File updateDir = new File(FileSystemService.getRapidMinerHome(), "update");
File updateRootDir = new File(FileSystemService.getUserRapidMinerDir(), "update");
if (!updateRootDir.exists()) {
if (!updateRootDir.mkdir()) {
throw new IOException("Cannot create update directory. Please ensure you have administrator permissions.");
}
}
if (!updateRootDir.canWrite()) {
throw new IOException("Cannot write to update directory. Please ensure you have administrator permissions.");
}
File updateFile = new File(updateRootDir, "rmupdate-" + version + ".jar");
// output stream is closed in utility method
Tools.copyStreamSynchronously(openStream, new FileOutputStream(updateFile), true);
//check MD5 hash
if (!compareMD5(updateFile, md5adress)) {
Tools.delete(updateFile);
Tools.delete(updateRootDir);
throw new ChecksumException();
}
File ruInstall = new File(updateRootDir, "RUinstall");
ZipFile zip = new ZipFile(updateFile);
Enumeration<? extends ZipEntry> en = zip.entries();
while (en.hasMoreElements()) {
ZipEntry entry = en.nextElement();
if (entry.isDirectory()) {
continue;
}
String name = entry.getName();
if ("META-INF/UPDATE".equals(name)) {
// extract directly to update directory and leave extraction to Launcher.
Tools.copyStreamSynchronously(zip.getInputStream(entry),
new FileOutputStream(new File(updateRootDir, "UPDATE")), true);
continue;
}
if (name.startsWith("rapidminer/")) {
name = name.substring("rapidminer/".length());
}
File dest = new File(ruInstall, name);
File parent = dest.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
Tools.copyStreamSynchronously(zip.getInputStream(entry), new FileOutputStream(dest), true);
}
zip.close();
updateFile.delete();
//LogService.getRoot().info("Prepared RapidMiner for update. Restart required.");
LogService.getRoot().log(Level.INFO, "com.rapid_i.deployment.update.client.UpdateManager.prepared_rapidminer_for_update");
}
/** This method takes the entries contained in the plugin archive and in the
* jar read from the given input stream and merges the entries.
* The new jar is scanned for a file META-INF/UPDATE that contains
* instructions about files to delete. Files found in this list
* are removed from the destination jar.
* return -1 on error
* @throws ChecksumException */
private void updatePluginIncrementally(ManagedExtension extension, InputStream diffJarIn, String fromVersion, String newVersion, String md5Adress) throws IOException, ChecksumException {
ByteArrayOutputStream diffJarBuffer = new ByteArrayOutputStream();
Tools.copyStreamSynchronously(diffJarIn, diffJarBuffer, true);
//save byte[] to create the MD5-hash later
byte[] downloadedFile = diffJarBuffer.toByteArray();
//LogService.getRoot().fine("Downloaded incremental zip.");
LogService.getRoot().log(Level.FINE, "com.rapid_i.deployment.update.client.UpdateManager.downloaded_incremental_zip");
InMemoryZipFile diffJar = new InMemoryZipFile(downloadedFile);
//create MD5-hash and compare to server-hash
if (!compareMD5(downloadedFile, md5Adress)) {
throw new ChecksumException();
}
Set<String> toDelete = new HashSet<String>();
byte[] updateEntry = diffJar.getContents("META-INF/UPDATE");
if (updateEntry == null) {
throw new IOException("META-INFO/UPDATE entry missing");
}
BufferedReader updateReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(updateEntry), "UTF-8"));
String line;
while ((line = updateReader.readLine()) != null) {
String[] split = line.split(" ", 2);
if (split.length != 2) {
diffJarBuffer.close();
throw new IOException("Illegal entry in update script: " + line);
}
if ("DELETE".equals(split[0])) {
toDelete.add(split[1].trim());
} else {
diffJarBuffer.close();
throw new IOException("Illegal entry in update script: " + line);
}
}
//LogService.getRoot().fine("Extracted update script, "+toDelete.size()+ " items to delete.");
LogService.getRoot().log(Level.FINE, "com.rapid_i.deployment.update.client.UpdateManager.extracted_update_script", toDelete.size());
// find all names listed in both files.
Set<String> allNames = new HashSet<String>();
allNames.addAll(diffJar.entryNames());
JarFile fromJar = extension.findArchive(fromVersion);
Enumeration<? extends ZipEntry> e = fromJar.entries();
while (e.hasMoreElements()) {
ZipEntry entry = e.nextElement();
allNames.add(entry.getName());
}
//LogService.getRoot().info("Extracted entry names, "+allNames.size()+ " entries in total.");
LogService.getRoot().log(Level.INFO, "com.rapid_i.deployment.update.client.UpdateManager.extracted_entry_names", allNames.size());
File newFile = extension.getDestinationFile(newVersion);
ZipOutputStream newJar = new ZipOutputStream(new FileOutputStream(newFile));
ZipFile oldArchive = extension.findArchive();
for (String name : allNames) {
if (toDelete.contains(name)) {
//LogService.getRoot().finest("DELETE "+name);
LogService.getRoot().log(Level.FINEST, "com.rapid_i.deployment.update.client.UpdateManager.delete_name", name);
continue;
}
newJar.putNextEntry(new ZipEntry(name));
if (diffJar.containsEntry(name)) {
newJar.write(diffJar.getContents(name));
//LogService.getRoot().finest("UPDATE "+name);
LogService.getRoot().log(Level.FINEST, "com.rapid_i.deployment.update.client.UpdateManager.update_name", name);
} else {
// cannot be null since it must be contained in at least one jarfile
ZipEntry oldEntry = oldArchive.getEntry(name);
Tools.copyStreamSynchronously(oldArchive.getInputStream(oldEntry), newJar, false);
//LogService.getRoot().finest("STORE "+name);
LogService.getRoot().log(Level.FINEST, "com.rapid_i.deployment.update.client.UpdateManager.store_name", name);
}
newJar.closeEntry();
}
newJar.finish();
newJar.close();
}
public static String getBaseUrl() {
String property = ParameterService.getParameterValue(PARAMETER_UPDATE_URL);
if (property == null) {
return UPDATESERVICE_URL;
} else {
return property;
}
}
public static URI getUpdateServerURI(String suffix) throws URISyntaxException {
String property = ParameterService.getParameterValue(PARAMETER_UPDATE_URL);
if (property == null) {
return new URI(UPDATESERVICE_URL + suffix);
} else {
return new URI(property + suffix);
}
}
public static boolean isIncrementalUpdate() {
return !"false".equals(ParameterService.getParameterValue(PARAMETER_UPDATE_INCREMENTALLY));
}
private static UpdateService theService = null;
private static URI lastUsedUri = null;
private static AccountService accountService;
private static String packageIdRapidMiner = PACKAGEID_RAPIDMINER;
public synchronized static UpdateService getService() throws MalformedURLException, URISyntaxException {
URI uri = getUpdateServerURI("/UpdateServiceService?wsdl");
if (theService == null || lastUsedUri != null && !lastUsedUri.equals(uri)) {
UpdateServiceService uss = new UpdateServiceService(uri.toURL(),
new QName("http://ws.update.deployment.rapid_i.com/", "UpdateServiceService"));
try {
theService = uss.getUpdateServicePort();
} catch (Error e) {
// can throw an error if the web service method does not exists. We have to convert it to an runtime exception
throw new RuntimeException(e);
}
}
lastUsedUri = uri;
return theService;
}
public synchronized static void resetService() {
lastUsedUri = null;
theService = null;
}
public static final boolean isAccountServiceCreated() {
return accountService != null;
}
public static void clearAccountSerive() {
accountService = null;
WebServiceTools.clearAuthCache();
}
public synchronized static AccountService getAccountService() throws MalformedURLException, URISyntaxException {
URI uri = getUpdateServerURI("/AccountService?wsdl");
if (accountService == null) {
AccountServiceService ass = new AccountServiceService(uri.toURL(),
new QName("http://ws.update.deployment.rapid_i.com/", "AccountServiceService"));
accountService = ass.getAccountServicePort();
WebServiceTools.setCredentials((BindingProvider) accountService, usAccount.getUserName(), usAccount.getPassword());
}
return accountService;
}
public static void saveLastUpdateCheckDate() {
File file = FileSystemService.getUserConfigFile("updatecheck.date");
PrintWriter out = null;
try {
out = new PrintWriter(new FileWriter(file));
out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
} catch (IOException e) {
LogService.getRoot().log(Level.WARNING, "Failed to save update timestamp: " + e, e);
} finally {
if (out != null) {
out.close();
}
}
}
/**
* method to get the MD5 hash from the server
* @param md5Stream Inputstream from server user with md5hash of the download
* @return returns a hexString to be compatible to the local MD5-hash-method
*/
private String getServerMD5(InputStream md5Stream) throws IOException {
byte[] md5Hash = {};
try {
ByteArrayOutputStream md5Buffer = new ByteArrayOutputStream();
Tools.copyStreamSynchronously(md5Stream, md5Buffer, true);
md5Hash = md5Buffer.toByteArray();
} catch (IOException e) {
md5Stream.close();
throw new IOException("failure while downloading the hash from Server: " + e.getMessage());
}
return new String(md5Hash);
}
/**
* compares the MD5-hash of the given File with the value which will be loaded from the urlString with ?md5 added
* @param toCompare File to compare with server data
* @param urlString download-address of the given File
* @return returns true if the hashes from the file and the download-hash are equal
* @throws URISyntaxException
* @throws IOException
*/
private boolean compareMD5(File toCompare, String urlString) {
if (urlString == null || toCompare == null || urlString.equals(""))
throw new IllegalArgumentException("parameter is empty");
try {
//create MD5 hash from File on disk
String localMD5 = UpdateManager.getMD5hash(toCompare);
//download MD5 from File on server
URL url = UpdateManager.getUpdateServerURI(urlString).toURL();
HttpURLConnection con = (HttpURLConnection) url.openConnection();
WebServiceTools.setURLConnectionDefaults(con);
con.setDoInput(true);
con.setDoOutput(false);
String serverMD5;
try {
serverMD5 = getServerMD5(con.getInputStream());
} catch (IOException e) {
throw new IOException(con.getResponseCode() + ": " + con.getResponseMessage(), e);
}
//compare MD5 hashes
if (serverMD5.compareTo(localMD5) == 0)
return true;
return false;
} catch (Exception e) {
// will delete the data of this downloadpart and show message to user to laod this data again
return false;
}
}
/**
* compares the MD5-hash of the given File with the value which will be loaded from the urlString with ?md5 added
* @param toCompare byte[] to compare with server data
* @param urlString download-address of the given File
* @return returns true if equal
* @throws URISyntaxException
* @throws IOException
*/
private boolean compareMD5(byte[] toCompare, String urlString) {
if (urlString == null || toCompare == null || urlString.equals(""))
throw new IllegalArgumentException("parameter is empty");
//create MD5 hash from File on disk
String localMD5 = UpdateManager.getMD5hash(toCompare);
//download MD5 from File on server
try {
URL url = UpdateManager.getUpdateServerURI(urlString).toURL();
HttpURLConnection con = (HttpURLConnection) url.openConnection();
WebServiceTools.setURLConnectionDefaults(con);
con.setDoInput(true);
con.setDoOutput(false);
String serverMD5;
try {
serverMD5 = getServerMD5(con.getInputStream());
} catch (IOException e) {
throw new IOException(con.getResponseCode() + ": " + con.getResponseMessage(), e);
}
//compare MD5 hashes
if (serverMD5.compareTo(localMD5) == 0)
return true;
return false;
} catch (Exception e) {
// will delete the data of this downloadpart and show message to user to laod this data again
return false;
}
}
/**
* method to get the MD5Hash of a File
* @param toHash
* @return returns a hexString which represents the MD5-hash
* @throws FileNotFoundException if the given File was not found
*/
public static String getMD5hash(File toHash) throws FileNotFoundException {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
InputStream inputStream = new FileInputStream(toHash);
byte[] buffer = new byte[8192];
int read = 0;
try {
while ((read = inputStream.read(buffer)) > 0) {
digest.update(buffer, 0, read);
}
byte[] md5sum = digest.digest();
//convert array to hexString because a wrong representation in the digest return value
StringBuffer hex = new StringBuffer();
for (byte one : md5sum) {
//delete minus-signs and make sure that every byte has exactly two chars
hex.append(Integer.toHexString((one & 0xFF) | 0x100).toLowerCase().substring(1, 3));
}
return hex.toString();
} catch (IOException e) {
throw new RuntimeException("Unable to process file for MD5", e);
} finally {
try {
inputStream.close();
} catch (IOException e) {
throw new RuntimeException("Unable to close input stream for MD5 calculation", e);
}
}
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException("No implementation of MD5 found.");
}
}
/**
* method to get the MD5Hash of a Byte-Array
* @param toHash byte[] of the Object of which the MD5-hash should be created
* @return returns a hexString which represents the MD5-hash
* @throws FileNotFoundException if the given File was not found
*/
public static String getMD5hash(byte[] toHash) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(toHash, 0, toHash.length);
byte[] md5sum = digest.digest();
//convert array to hexString because a wrong representation in the digest return value
StringBuffer hex = new StringBuffer();
for (byte one : md5sum) {
//delete minus-signs and make sure that every byte has exactly two chars
hex.append(Integer.toHexString((one & 0xFF) | 0x100).toLowerCase().substring(1, 3));
}
return hex.toString();
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException("No implementation of MD5 found.");
}
}
/**
* method to get the MD5Hash of a Stream
* @param toHash InputStream of a given File
* @return returns a hexString which represents the MD5-hash
*/
public static String getMD5hash(InputStream toHash) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] buffer = new byte[8192];
int read = 0;
try {
while ((read = toHash.read(buffer)) > 0) {
digest.update(buffer, 0, read);
}
byte[] md5sum = digest.digest();
//convert array to hexString because a wrong representation in the digest return value
StringBuffer hex = new StringBuffer();
for (byte one : md5sum) {
//delete minus-signs and make sure that every byte has exactly two chars
hex.append(Integer.toHexString((one & 0xFF) | 0x100).toLowerCase().substring(1, 3));
}
return hex.toString();
} catch (IOException e) {
throw new RuntimeException("Unable to process file for MD5", e);
} finally {
try {
toHash.close();
} catch (IOException e) {
throw new RuntimeException("Unable to close input stream for MD5 calculation", e);
}
}
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException("No implementation of MD5 found.");
}
}
private static Date loadLastUpdateCheckDate() {
File file = FileSystemService.getUserConfigFile("updatecheck.date");
if (!file.exists())
return null;
BufferedReader in = null;
try {
in = new BufferedReader(new FileReader(file));
String date = in.readLine();
if (date == null) {
return null;
} else {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(date);
}
} catch (Exception e) {
//LogService.getRoot().log(Level.WARNING, "Cannot read last date of update check.", e);
LogService.getRoot().log(Level.WARNING,
I18N.getMessage(LogService.getRoot().getResourceBundle(),
"com.rapid_i.deployment.update.client.UpdateManager.reading_update_check_error"),
e);
return null;
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
// cannot happen
}
}
}
}
/** Checks whether the last update is at least 7 days ago, then checks whether there
* are any updates, and opens a dialog if desired by the user. */
public static void checkForUpdates() {
String updateProperty = ParameterService.getParameterValue(RapidMinerGUI.PROPERTY_RAPIDMINER_GUI_UPDATE_CHECK);
if (Tools.booleanValue(updateProperty, true)) {
if (RapidMiner.getExecutionMode() == ExecutionMode.WEBSTART) {
LogService.getRoot().config("com.rapid_i.deployment.update.client.UpdateManager.ignoring_update_check_webstart_mode");
return;
}
boolean check = true;
final Date lastCheckDate = loadLastUpdateCheckDate();
if (lastCheckDate != null) {
Calendar lastCheck = Calendar.getInstance();
lastCheck.setTime(lastCheckDate);
Calendar currentDate = Calendar.getInstance();
currentDate.add(Calendar.DAY_OF_YEAR, -2);
if (!lastCheck.before(currentDate)) {
check = false;
LogService.getRoot().log(Level.CONFIG, "com.rapid_i.deployment.update.client.UpdateManager.ignoring_update_check_last_checkdate", lastCheckDate);
}
}
if (check) {
new ProgressThread("check_for_updates") {
@Override
public void run() {
LogService.getRoot().info("com.rapid_i.deployment.update.client.UpdateManager.update_checking");
boolean updatesExist = false;
try {
updatesExist = !RapidMiner.getVersion().isAtLeast(new VersionNumber(getService().getLatestVersion(getRMPackageId(), TARGET_PLATFORM)));
} catch (Exception e) {
//LogService.getRoot().log(Level.WARNING, "Error checking for updates: "+e, e);
LogService.getRoot().log(Level.WARNING,
I18N.getMessage(LogService.getRoot().getResourceBundle(),
"com.rapid_i.deployment.update.client.UpdateManager.checking_for_updates_error",
e),
e);
return;
}
saveLastUpdateCheckDate();
if (updatesExist) {
if (SwingTools.showConfirmDialog("updates_exist", ConfirmDialog.YES_NO_OPTION) == ConfirmDialog.YES_OPTION) {
UpdateDialog.showUpdateDialog(true);
}
} else {
//LogService.getRoot().info("No updates since "+lastCheckDate+".");
LogService.getRoot().log(Level.INFO, "com.rapid_i.deployment.update.client.UpdateManager.no_updates_aviable", lastCheckDate);
}
}
}.start();
}
}
}
/** Checks whether the user bought any purchased extensions recently which aren't yet installed and opens the PurchasedNotInstalled Dialog in that case. */
public static void checkForPurchasedNotInstalled() {
String updateProperty = ParameterService.getParameterValue(RapidMinerGUI.PROPERTY_RAPIDMINER_GUI_PURCHASED_NOT_INSTALLED_CHECK);
if (Tools.booleanValue(updateProperty, true)) {
try {
Class.forName("com.rapid_i.deployment.update.client.UpdateServerAccount");
} catch (ClassNotFoundException e) {
LogService.getRoot().log(Level.WARNING, "The class 'UpdateServerAccount' could not be found.");
}
String updateServerURI = null;
try {
updateServerURI = UpdateManager.getUpdateServerURI("").toString();
} catch (URISyntaxException e) {
LogService.getRoot().log(Level.WARNING,
I18N.getMessage(LogService.getRoot().getResourceBundle(),
"com.rapid_i.deployment.update.client.UpdateManager.malformed_update_server_uri",
e),
e);
return;
}
UserCredential authentication = Wallet.getInstance().getEntry(Wallet.ID_MARKETPLACE, updateServerURI);
if ((authentication == null) || (authentication.getPassword() == null))
return;
PasswordAuthentication passwordAuthentication = null;
try {
passwordAuthentication = PasswordDialog.getPasswordAuthentication(Wallet.ID_MARKETPLACE, updateServerURI, false, true, "authentication.marketplace");
} catch (PasswortInputCanceledException e1) {}
UpdateServerAccount.setPasswordAuthentication(passwordAuthentication);
boolean check;
try {
UpdateManager.getAccountService();
check = true;
} catch (Exception e) {
check = false;
}
if (check) {
new ProgressThread("check_for_recently_purchased_extensions") {
@Override
public void run() {
LogService.getRoot().info("com.rapid_i.deployment.update.client.UpdateManager.purchased_extensions_checking");
boolean updatesExist = false;
List<String> purchasedExtensions = new ArrayList<String>();
try {
purchasedExtensions = getAccountService().getLicensedProducts();
// delete all extensions which are already installed
Iterator<String> i = purchasedExtensions.iterator();
while (i.hasNext()) {
String packageId = i.next();
if (ManagedExtension.get(packageId) != null) {
i.remove();
}
}
updatesExist = !purchasedExtensions.isEmpty();
if (updatesExist) {
// delete all extensions which should be ignored
purchasedExtensions.removeAll(readNeverRemindInstallExtensions());
updatesExist = !purchasedExtensions.isEmpty();
}
} catch (Exception e) {
LogService.getRoot().log(Level.WARNING,
I18N.getMessage(LogService.getRoot().getResourceBundle(),
"com.rapid_i.deployment.update.client.UpdateManager.checking_for_purchased_extensions_error",
e),
e);
return;
}
if (updatesExist) {
PendingPurchasesInstallationDialog pnid = new PendingPurchasesInstallationDialog(purchasedExtensions);
pnid.setVisible(true);
} else {
LogService.getRoot().log(Level.INFO, "com.rapid_i.deployment.update.client.UpdateManager.no_purchased_extensions", "");
}
}
}.start();
}
}
}
/** Returns extensions saved in the configuration xml-file which shold be ignored and not shown. **/
private static List<String> readNeverRemindInstallExtensions() {
final File userConfigFile = FileSystemService.getUserConfigFile(NEVER_REMIND_INSTALL_EXTENSIONS_FILE_NAME);
if (!userConfigFile.exists()) {
return new ArrayList<String>();
}
LogService.getRoot().log(Level.CONFIG, "com.rapid_i.deployment.update.client.UpdateManager.reading_ignored_extensions_file");
Document doc;
try {
doc = XMLTools.parse(userConfigFile);
} catch (Exception e) {
LogService.getRoot().log(Level.WARNING,
I18N.getMessage(LogService.getRoot().getResourceBundle(),
"com.rapid_i.deployment.update.client.PurchasedNotInstalledDialog.creating_xml_document_error",
e),
e);
return new ArrayList<String>();
}
List<String> ignoreList = new ArrayList<String>();
NodeList extensionElems = doc.getDocumentElement().getElementsByTagName("extension_name");
for (int i = 0; i < extensionElems.getLength(); i++) {
Element extensionElem = (Element) extensionElems.item(i);
ignoreList.add(extensionElem.getTextContent());
}
return ignoreList;
}
public static UpdateServerAccount getUpdateServerAccount() {
return usAccount;
}
public static String getRMPackageId() {
return packageIdRapidMiner;
}
public static void setRMPackageId(String packageIdRapidMiner) {
UpdateManager.packageIdRapidMiner = packageIdRapidMiner;
}
}