/* * RapidMiner * * Copyright (C) 2001-2011 by Rapid-I and the contributors * * Complete list of developers available at our web site: * * http://rapid-i.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.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.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.Enumeration; import java.util.HashSet; 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.xml.datatype.XMLGregorianCalendar; import javax.xml.namespace.QName; import com.rapid_i.Launcher; 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.tools.ProgressThread; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.gui.tools.dialogs.ConfirmDialog; import com.rapidminer.io.process.XMLTools; import com.rapidminer.tools.FileSystemService; import com.rapidminer.tools.LogService; import com.rapidminer.tools.ParameterService; import com.rapidminer.tools.ProgressListener; import com.rapidminer.tools.Tools; /** * This class manages the updates of the core and installation and updates of extensions. * * @author Simon Fischer */ public class UpdateManager { 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://rapidupdate.de:80/UpdateServer"; private final UpdateService service; static final String COMMERCIAL_LICENSE_NAME = "RIC"; public UpdateManager(UpdateService service) { super(); this.service = service; } public void performUpdates(List<PackageDescriptor> downloadList, ProgressListener progressListener) throws IOException, UpdateServiceException_Exception { int i = 0; try { for (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 (desc.getPackageTypeName().equals("RAPIDMINER_PLUGIN")) { 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(); if (incremental) { LogService.getRoot().info("Updating "+desc.getPackageId()+" incrementally."); try { updatePluginIncrementally(extension, openStream(url, progressListener, minProgress, maxProgress), baseVersion, desc.getVersion()); } 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..."); incremental = false; } } // try standard non incremental way if (!incremental){ LogService.getRoot().info("Updating "+desc.getPackageId()+"."); updatePlugin(extension, openStream(url, progressListener, minProgress, maxProgress), desc.getVersion()); } extension.addAndSelectVersion(desc.getVersion()); } else { URL url = UpdateManager.getUpdateServerURI(urlString + (incremental ? "?baseVersion="+URLEncoder.encode(RapidMiner.getLongVersion(), "UTF-8") : "")).toURL(); LogService.getRoot().info("Updating RapidMiner core."); updateRapidMiner(openStream(url, progressListener, minProgress, maxProgress), desc.getVersion()); } i++; progressListener.setCompleted(20 + 80*i/downloadList.size()); } } catch (URISyntaxException e) { throw new IOException(e); } finally { progressListener.complete(); } } /** * @throws IOException */ private InputStream openStream(URL url, ProgressListener listener, int minProgress, int maxProgress) throws IOException { HttpURLConnection con = (HttpURLConnection) url.openConnection(); 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."); 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); return urlIn; } } } private void updatePlugin(ManagedExtension extension, InputStream updateIn, String newVersion) throws IOException { File outFile = extension.getDestinationFile(newVersion); OutputStream out = new FileOutputStream(outFile); try { Tools.copyStreamSynchronously(updateIn, out, true); } finally { try { out.close(); } catch (IOException e) { } } } private void updateRapidMiner(InputStream openStream, String version) throws IOException { File updateDir = new File(FileSystemService.getRapidMinerHome(), "update"); if (!updateDir.exists()) { if (!updateDir.mkdir()) { throw new IOException("Cannot create update directory. Please ensure you have administrator permissions."); } } if (!updateDir.canWrite()) { throw new IOException("Cannot write to update directory. Please ensure you have administrator permissions."); } File updateFile = new File(updateDir, "rmupdate-"+version+".jar"); Tools.copyStreamSynchronously(openStream, new FileOutputStream(updateFile), true); File ruInstall = new File(FileSystemService.getRapidMinerHome(), "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(updateDir, "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."); } /** 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. */ private void updatePluginIncrementally(ManagedExtension extension, InputStream diffJarIn, String fromVersion, String newVersion) throws IOException { ByteArrayOutputStream diffJarBuffer = new ByteArrayOutputStream(); Tools.copyStreamSynchronously(diffJarIn, diffJarBuffer, true); LogService.getRoot().fine("Downloaded incremental zip."); InMemoryZipFile diffJar = new InMemoryZipFile(diffJarBuffer.toByteArray()); 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) { throw new IOException("Illegal entry in update script: "+line); } if ("DELETE".equals(split[0])) { toDelete.add(split[1].trim()); } else { throw new IOException("Illegal entry in update script: "+line); } } LogService.getRoot().fine("Extracted update script, "+toDelete.size()+ " items to delete."); // 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."); 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); continue; } newJar.putNextEntry(new ZipEntry(name)); if (diffJar.containsEntry(name)) { newJar.write(diffJar.getContents(name)); LogService.getRoot().finest("UPDATE "+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); } 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; 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")); theService = uss.getUpdateServicePort(); } lastUsedUri = uri; return theService; } public synchronized static AccountService getAccountService() throws MalformedURLException, URISyntaxException { URI uri = getUpdateServerURI("/AccountService?wsdl"); AccountServiceService ass = new AccountServiceService(uri.toURL(), new QName("http://ws.update.deployment.rapid_i.com/", "AccountServiceService")); return ass.getAccountServicePort(); } 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) { e.printStackTrace(); } finally { if (out != null) { out.close(); } } } 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); 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 (Launcher.isDevelopmentBuild()) { LogService.getRoot().config("This is a development build. Ignoring update check."); return; } if (RapidMiner.getExecutionMode() == ExecutionMode.WEBSTART) { LogService.getRoot().config("Ignoring update check in 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().config("Ignoring update check. Last update check was on "+lastCheckDate); } } if (check) { new ProgressThread("check_for_updates") { @Override public void run() { LogService.getRoot().info("Checking for updates."); XMLGregorianCalendar xmlGregorianCalendar; if (lastCheckDate != null) { try { xmlGregorianCalendar = XMLTools.getXMLGregorianCalendar(lastCheckDate); } catch (Exception e) { LogService.getRoot().log(Level.WARNING, "Error checking for updates: "+e, e); return; } } else { xmlGregorianCalendar = null; } boolean updatesExist; try { updatesExist = getService().anyUpdatesSince(xmlGregorianCalendar); } catch (Exception e) { LogService.getRoot().log(Level.WARNING, "Error checking for updates: "+e, e); return; } if (updatesExist) { if (SwingTools.showConfirmDialog("updates_exist", ConfirmDialog.YES_NO_OPTION) == ConfirmDialog.YES_OPTION) { UpdateDialog.showUpdateDialog(); } else { saveLastUpdateCheckDate(); } } else { LogService.getRoot().info("No updates since "+lastCheckDate+"."); saveLastUpdateCheckDate(); } } }.start(); } } } }