/***************************************************************************** * Copyright (c) 2006-2007, Cloudsmith Inc. * The code, documentation and other materials contained herein have been * licensed under the Eclipse Public License - v 1.0 by the copyright holder * listed above, as the Initial Contributor under such license. The text of * such license is available at www.eclipse.org. *****************************************************************************/ package org.eclipse.buckminster.jnlp.product; import static org.eclipse.buckminster.jnlp.bootstrap.BootstrapConstants.*; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Enumeration; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.jar.Pack200; import java.util.jar.Pack200.Unpacker; import java.util.zip.GZIPInputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.eclipse.buckminster.jnlp.bootstrap.BootstrapConstants; import org.eclipse.buckminster.jnlp.bootstrap.CorruptedFileException; import org.eclipse.buckminster.jnlp.bootstrap.IProductInstaller; import org.eclipse.buckminster.jnlp.bootstrap.JNLPException; import org.eclipse.buckminster.jnlp.bootstrap.Main; import org.eclipse.buckminster.jnlp.bootstrap.Messages; import org.eclipse.buckminster.jnlp.bootstrap.OperationCanceledException; import org.eclipse.buckminster.jnlp.bootstrap.ProgressFacade; public class ProductInstaller implements IProductInstaller { private static final String INSTALL_FOLDER = "installer"; //$NON-NLS-1$ private static final String PACK_PROPERTIES_FILE = "pack.properties"; //$NON-NLS-1$ private static final String INSTALL_DONE_FOLDER = "installation.completed"; //$NON-NLS-1$ private static final String PACK_SUFFIX = ".pack.gz"; //$NON-NLS-1$ private static final int PACK_SUFFIX_LEN = PACK_SUFFIX.length(); private static final char s_fileSep; static { String fileSep = System.getProperty("file.separator"); //$NON-NLS-1$ s_fileSep = (fileSep == null || fileSep.length() < 1) ? '/' : fileSep.charAt(0); } private static void deleteRecursive(File file) throws JNLPException { if(!file.exists()) return; try { File[] list = file.listFiles(); int count = (list == null) ? 0 : list.length; if(count > 0) { while(--count >= 0) deleteRecursive(list[count]); } if(!file.delete() && file.exists()) throw new JNLPException( Messages.getString("unable_to_delete") + file.getAbsolutePath(), Messages.getString("check_file_permissions"), //$NON-NLS-1$ //$NON-NLS-2$ BootstrapConstants.ERROR_CODE_FILE_IO_EXCEPTION); } catch(SecurityException e) { throw new JNLPException( Messages.getString("unable_to_delete") + file.getAbsolutePath() + ": " + e.getMessage(), //$NON-NLS-1$ //$NON-NLS-2$ Messages.getString("check_file_permissions"), BootstrapConstants.ERROR_CODE_FILE_IO_EXCEPTION, e); //$NON-NLS-1$ } } private static String encrypt(byte[] bytes, String algorithmName) { String md5val = ""; //$NON-NLS-1$ MessageDigest algorithm = null; try { algorithm = MessageDigest.getInstance(algorithmName); } catch(NoSuchAlgorithmException nsae) { throw new IllegalArgumentException(Messages.getString("unknown_encrypt_algorithm_colon") + algorithmName); //$NON-NLS-1$ } algorithm.reset(); algorithm.update(bytes); byte messageDigest[] = algorithm.digest(); StringBuffer hexString = new StringBuffer(); for(int i = 0; i < messageDigest.length; i++) { String hex = Integer.toHexString(0xFF & messageDigest[i]); if(hex.length() == 1) { hexString.append('0'); } hexString.append(hex); } md5val = hexString.toString(); return md5val; } private static String osAdjustName(String name) { if(s_fileSep != '/') name = name.replace('/', s_fileSep); return name; } private static void renameFile(File from, File to) { if(!from.renameTo(to)) throw new RuntimeException(Messages.getString("unable_to_rename_0_to_1", from, to)); //$NON-NLS-1$ } private Main m_main; private Unpacker m_unpacker; private static final String PROP_UNPACK_COUNT = "unpackCount"; //$NON-NLS-1$ private static final int DEFAULT_UNPACK_COUNT = 80; public String getApplicationFolder() { return INSTALL_FOLDER; } public String[] getInstallFolders() { return new String[] { INSTALL_FOLDER }; } public void installProduct(Main main, ProgressFacade monitor) throws JNLPException, OperationCanceledException, CorruptedFileException { m_main = main; // We know that we have about 80 packed files to process. Since we're streaming // everything in one go, it's a bit hard to find the exact number. // int unpackCount = Integer.getInteger(PROP_UNPACK_COUNT, DEFAULT_UNPACK_COUNT).intValue(); monitor.setTask(Messages.getString("unpacking"), unpackCount + 30); //$NON-NLS-1$ for(String folder : getInstallFolders()) { deleteRecursive(new File(m_main.getInstallLocation(), folder)); } monitor.taskIncrementalProgress(10); monitor.checkCanceled(); installResource("product.zip", monitor); //$NON-NLS-1$ installResource("platform.zip", monitor); //$NON-NLS-1$ installResource("extensions.zip", monitor, false); //$NON-NLS-1$ File appFolder = new File(m_main.getInstallLocation(), getApplicationFolder()); try { new File(appFolder, INSTALL_DONE_FOLDER).createNewFile(); } catch(IOException e) { throw new JNLPException( Messages.getString("can_not_create_a_new_file"), //$NON-NLS-1$ Messages.getString("check_disk_space_system_permissions_and_try_again"), ERROR_CODE_FILE_IO_EXCEPTION, e); //$NON-NLS-1$ } monitor.taskDone(); } public boolean isInstalled(File installLocation) throws JNLPException { File appFolder = new File(installLocation, getApplicationFolder()); return new File(appFolder, INSTALL_DONE_FOLDER).exists(); } private String[] getSkipFiles() { return new String[] { PACK_PROPERTIES_FILE }; } private void installFromStream(InputStream productZip, InputStream productZipMD5, ProgressFacade monitor) throws JNLPException, OperationCanceledException, CorruptedFileException { try { byte[] productBytes = readStream(productZip); String computedMD5 = encrypt(productBytes, "MD5").trim(); //$NON-NLS-1$ String originalMD5 = new String(readStream(productZipMD5)).trim(); if(!computedMD5.equals(originalMD5)) throw new CorruptedFileException(); File installLocation = m_main.getInstallLocation(); ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(productBytes)); ZipEntry zipEntry; zipEntryCycle: while((zipEntry = zipInput.getNextEntry()) != null) { monitor.checkCanceled(); for(String skipFile : getSkipFiles()) if(zipEntry.getName().equals(skipFile)) continue zipEntryCycle; boolean folderOK = false; for(String folder : getInstallFolders()) { if(zipEntry.getName().startsWith(folder)) { folderOK = true; break; } } if(!folderOK) { throw new JNLPException( Messages.getString("materializer_error"), Messages.getString("materializer_is_probably_corrupted_report_the_error"), ERROR_CODE_MATERIALIZER_INSTALL_EXCEPTION); //$NON-NLS-1$ //$NON-NLS-2$ } String name = osAdjustName(zipEntry.getName()); File file; if(zipEntry.isDirectory()) { file = new File(installLocation, name); file.mkdirs(); } else { if(name.endsWith(PACK_SUFFIX)) { // Pack200 compressed file // file = new File(installLocation, name.substring(0, name.length() - PACK_SUFFIX_LEN)); OutputStream output; try { output = new FileOutputStream(file); } catch(FileNotFoundException e) { throw new JNLPException( Messages.getString("can_not_create_file_colon") + file.toString(), //$NON-NLS-1$ Messages.getString("check_disk_space_system_permissions_and_try_again"), ERROR_CODE_FILE_IO_EXCEPTION, //$NON-NLS-1$ e); } try { try { storeUnpacked(zipInput, output); } catch(IOException e) { throw new JNLPException( Messages.getString("can_not_unzip_and_save_to_file_colon") + file.toString(), //$NON-NLS-1$ Messages.getString("check_disk_space_system_permissions_and_try_again"), //$NON-NLS-1$ ERROR_CODE_FILE_IO_EXCEPTION, e); } monitor.taskIncrementalProgress(1); } finally { Main.close(output); } } else { // This is so quick that it doesn't generate a progress tick // file = new File(installLocation, name); try { storeVerbatim(file, zipInput); } catch(IOException e) { throw new JNLPException( Messages.getString("can_not_save_to_file_colon") + file.toString(), //$NON-NLS-1$ Messages.getString("check_disk_space_system_permissions_and_try_again"), ERROR_CODE_FILE_IO_EXCEPTION, //$NON-NLS-1$ e); } } if(file.getName().endsWith(".jar")) //$NON-NLS-1$ { try { if(recursiveUnpack(file)) monitor.taskIncrementalProgress(1); } catch(IOException e) { throw new JNLPException( Messages.getString("can_not_unpack_file_colon") + file.toString(), //$NON-NLS-1$ Messages.getString("check_disk_space_system_permissions_and_try_again"), ERROR_CODE_FILE_IO_EXCEPTION, //$NON-NLS-1$ e); } } } long tz = zipEntry.getTime(); if(tz != -1L) file.setLastModified(tz); } } catch(IOException e) { throw new JNLPException( Messages.getString("can_not_read_materialization_wizard_resource"), //$NON-NLS-1$ Messages.getString("check_your_internet_connection_and_try_again"), ERROR_CODE_REMOTE_IO_EXCEPTION, e); //$NON-NLS-1$ } } private void installResource(String resourceName, ProgressFacade monitor) throws JNLPException, OperationCanceledException, CorruptedFileException { installResource(resourceName, monitor, true); } private void installResource(String resourceName, ProgressFacade monitor, boolean required) throws JNLPException, OperationCanceledException, CorruptedFileException { monitor.taskIncrementalProgress(5); InputStream resourceZip = getClass().getResourceAsStream(resourceName); InputStream resourceZipMD5 = getClass().getResourceAsStream(resourceName + ".MD5"); //$NON-NLS-1$ monitor.taskIncrementalProgress(5); if(resourceZip == null) { // // Nothing to install // if(required) throw new RuntimeException(Messages.getString("missing_0_resource", resourceName)); //$NON-NLS-1$ return; } try { installFromStream(resourceZip, resourceZipMD5, monitor); } finally { Main.close(resourceZip); Main.close(resourceZipMD5); } } private byte[] readStream(InputStream input) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] copyBuf = new byte[8192]; int count; while((count = input.read(copyBuf)) > 0) output.write(copyBuf, 0, count); return output.toByteArray(); } private boolean recursiveUnpack(File file) throws IOException { // Make sure this jar file doesn't contain packed entries // JarOutputStream jarOutput = null; JarFile jarFile = new JarFile(file); File tempFile = null; try { boolean needRepack = false; Enumeration<JarEntry> entries = jarFile.entries(); while(entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); if(entry.getName().endsWith(PACK_SUFFIX)) { needRepack = true; break; } } if(!needRepack) return false; // We need to repack this jar // byte[] copyBuf = new byte[8192]; tempFile = File.createTempFile("unpack", ".jar", file.getParentFile()); //$NON-NLS-1$ //$NON-NLS-2$ jarOutput = new JarOutputStream(new FileOutputStream(tempFile), jarFile.getManifest()); entries = jarFile.entries(); while(entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); String entryName = entry.getName(); if(entry.isDirectory()) { if(!entryName.equalsIgnoreCase("meta-inf")) //$NON-NLS-1$ // // It's there already. // jarOutput.putNextEntry(entry); continue; } if(entryName.equalsIgnoreCase("meta-inf/manifest.mf")) //$NON-NLS-1$ // // Manifest is already written to output // continue; InputStream input = jarFile.getInputStream(entry); try { if(entryName.endsWith(PACK_SUFFIX)) { String unpackedName = entryName.substring(0, entryName.length() - PACK_SUFFIX_LEN); JarEntry unpackedEntry = new JarEntry(unpackedName); unpackedEntry.setMethod(ZipEntry.DEFLATED); jarOutput.putNextEntry(unpackedEntry); storeUnpacked(input, jarOutput); } else { jarOutput.putNextEntry(entry); int count; while((count = input.read(copyBuf)) > 0) jarOutput.write(copyBuf, 0, count); } } finally { Main.close(input); } } } finally { Main.close(jarOutput); jarFile.close(); } File mvTemp = new File(file.getAbsolutePath() + ".move"); //$NON-NLS-1$ mvTemp.delete(); renameFile(file, mvTemp); renameFile(tempFile, file); mvTemp.delete(); return true; } private synchronized void storeUnpacked(InputStream packedInput, OutputStream result) throws IOException { if(m_unpacker == null) m_unpacker = Pack200.newUnpacker(); GZIPInputStream gzipInput = null; try { // Wrap the incoming stream to prevent it from being closed // when the GZIPInputStream is closed. // gzipInput = new GZIPInputStream(new FilterInputStream(packedInput) { @Override public void close() { } }); JarOutputStream jarOut = new JarOutputStream(result); m_unpacker.unpack(gzipInput, jarOut); gzipInput = null; // Closed by unpack jarOut.finish(); } finally { Main.close(gzipInput); } } private synchronized void storeVerbatim(File file, InputStream packedInput) throws IOException { OutputStream out = null; try { int count; byte[] buffer = new byte[0x1000]; out = new FileOutputStream(file); while((count = packedInput.read(buffer)) > 0) out.write(buffer, 0, count); } finally { Main.close(out); } } }