001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2007-2008 Sun Microsystems, Inc. 025 * Portions Copyright 2011-2015 ForgeRock AS 026 */ 027package org.opends.quicksetup.util; 028 029import static com.forgerock.opendj.cli.Utils.*; 030import static com.forgerock.opendj.util.OperatingSystem.*; 031 032import static org.opends.messages.QuickSetupMessages.*; 033import static org.opends.server.util.CollectionUtils.*; 034 035import java.io.File; 036import java.io.FileInputStream; 037import java.io.FileNotFoundException; 038import java.io.IOException; 039import java.io.InputStream; 040import java.util.ArrayList; 041import java.util.HashMap; 042import java.util.Map; 043import java.util.zip.ZipEntry; 044import java.util.zip.ZipInputStream; 045 046import org.forgerock.i18n.LocalizableMessage; 047import org.forgerock.i18n.slf4j.LocalizedLogger; 048import org.opends.quicksetup.Application; 049import org.opends.quicksetup.ApplicationException; 050import org.opends.quicksetup.ReturnCode; 051 052/** 053 * Class for extracting the contents of a zip file and managing 054 * the reporting of progress during extraction. 055 */ 056public class ZipExtractor { 057 058 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 059 060 /** Path separator for zip file entry names on Windows and *nix. */ 061 private static final char ZIP_ENTRY_NAME_SEP = '/'; 062 063 private InputStream is; 064 private int minRatio; 065 private int maxRatio; 066 private int numberZipEntries; 067 private String zipFileName; 068 private Application application; 069 070 /** 071 * Creates an instance of an ZipExtractor. 072 * @param zipFile File the zip file to extract 073 * @throws FileNotFoundException if the specified file does not exist 074 * @throws IllegalArgumentException if the zip file is not a zip file 075 */ 076 public ZipExtractor(File zipFile) 077 throws FileNotFoundException, IllegalArgumentException 078 { 079 this(zipFile, 0, 0, 1, null); 080 } 081 082 /** 083 * Creates an instance of an ZipExtractor. 084 * @param in InputStream for zip content 085 * @param zipFileName name of the input zip file 086 * @throws FileNotFoundException if the specified file does not exist 087 * @throws IllegalArgumentException if the zip file is not a zip file 088 */ 089 public ZipExtractor(InputStream in, String zipFileName) 090 throws FileNotFoundException, IllegalArgumentException 091 { 092 this(in, 0, 0, 1, zipFileName, null); 093 } 094 095 /** 096 * Creates an instance of an ZipExtractor. 097 * @param zipFile File the zip file to extract 098 * @param minRatio int indicating the max ration 099 * @param maxRatio int indicating the min ration 100 * @param numberZipEntries number of entries in the input stream 101 * @param app application to be notified about progress 102 * @throws FileNotFoundException if the specified file does not exist 103 * @throws IllegalArgumentException if the zip file is not a zip file 104 */ 105 public ZipExtractor(File zipFile, int minRatio, int maxRatio, 106 int numberZipEntries, 107 Application app) 108 throws FileNotFoundException, IllegalArgumentException 109 { 110 this(new FileInputStream(zipFile), 111 minRatio, 112 maxRatio, 113 numberZipEntries, 114 zipFile.getName(), 115 app); 116 if (!zipFile.getName().endsWith(".zip")) { 117 throw new IllegalArgumentException("File must have extension .zip"); 118 } 119 } 120 121 /** 122 * Creates an instance of an ZipExtractor. 123 * @param is InputStream of zip file content 124 * @param minRatio int indicating the max ration 125 * @param maxRatio int indicating the min ration 126 * @param numberZipEntries number of entries in the input stream 127 * @param zipFileName name of the input zip file 128 * @param app application to be notified about progress 129 */ 130 public ZipExtractor(InputStream is, int minRatio, int maxRatio, 131 int numberZipEntries, 132 String zipFileName, 133 Application app) { 134 this.is = is; 135 this.minRatio = minRatio; 136 this.maxRatio = maxRatio; 137 this.numberZipEntries = numberZipEntries; 138 this.zipFileName = zipFileName; 139 this.application = app; 140 } 141 142 /** 143 * Performs the zip extraction. 144 * @param destination File where the zip file will be extracted 145 * @throws ApplicationException if something goes wrong 146 */ 147 public void extract(File destination) throws ApplicationException { 148 extract(Utils.getPath(destination)); 149 } 150 151 /** 152 * Performs the zip extraction. 153 * @param destination File where the zip file will be extracted 154 * @throws ApplicationException if something goes wrong 155 */ 156 public void extract(String destination) throws ApplicationException { 157 extract(destination, true); 158 } 159 160 /** 161 * Performs the zip extraction. 162 * @param destDir String representing the directory where the zip file will 163 * be extracted 164 * @param removeFirstPath when true removes each zip entry's initial path 165 * when copied to the destination folder. So for instance if the zip entry's 166 * name was /OpenDJ-2.4.x/some_file the file would appear in the destination 167 * directory as 'some_file'. 168 * @throws ApplicationException if something goes wrong 169 */ 170 public void extract(String destDir, boolean removeFirstPath) 171 throws ApplicationException 172 { 173 ZipInputStream zipIn = new ZipInputStream(is); 174 int nEntries = 1; 175 176 /* This map is updated in the copyZipEntry method with the permissions 177 * of the files that have been copied. Once all the files have 178 * been copied to the file system we will update the file permissions of 179 * these files. This is done this way to group the number of calls to 180 * Runtime.exec (which is required to update the file system permissions). 181 */ 182 Map<String, ArrayList<String>> permissions = new HashMap<>(); 183 permissions.put(getProtectedDirectoryPermissionUnix(), newArrayList(destDir)); 184 try { 185 if(application != null) { 186 application.checkAbort(); 187 } 188 ZipEntry entry = zipIn.getNextEntry(); 189 while (entry != null) { 190 if(application != null) { 191 application.checkAbort(); 192 } 193 int ratioBeforeCompleted = minRatio 194 + ((nEntries - 1) * (maxRatio - minRatio) / numberZipEntries); 195 int ratioWhenCompleted = 196 minRatio + (nEntries * (maxRatio - minRatio) / numberZipEntries); 197 198 String name = entry.getName(); 199 if (name != null && removeFirstPath) { 200 int sepPos = name.indexOf(ZIP_ENTRY_NAME_SEP); 201 if (sepPos != -1) { 202 name = name.substring(sepPos + 1); 203 } else { 204 logger.warn(LocalizableMessage.raw( 205 "zip entry name does not contain a path separator")); 206 } 207 } 208 if (name != null && name.length() > 0) { 209 try { 210 File destination = new File(destDir, name); 211 copyZipEntry(entry, destination, zipIn, 212 ratioBeforeCompleted, ratioWhenCompleted, permissions); 213 } catch (IOException ioe) { 214 throw new ApplicationException( 215 ReturnCode.FILE_SYSTEM_ACCESS_ERROR, 216 getThrowableMsg(INFO_ERROR_COPYING.get(entry.getName()), ioe), 217 ioe); 218 } 219 } 220 221 zipIn.closeEntry(); 222 entry = zipIn.getNextEntry(); 223 nEntries++; 224 } 225 226 if (isUnix()) { 227 // Change the permissions for UNIX systems 228 for (String perm : permissions.keySet()) { 229 ArrayList<String> paths = permissions.get(perm); 230 try { 231 int result = Utils.setPermissionsUnix(paths, perm); 232 if (result != 0) { 233 throw new IOException("Could not set permissions on files " 234 + paths + ". The chmod error code was: " + result); 235 } 236 } catch (InterruptedException ie) { 237 throw new IOException("Could not set permissions on files " + paths 238 + ". The chmod call returned an InterruptedException.", ie); 239 } 240 } 241 } 242 } catch (IOException ioe) { 243 throw new ApplicationException( 244 ReturnCode.FILE_SYSTEM_ACCESS_ERROR, 245 getThrowableMsg(INFO_ERROR_ZIP_STREAM.get(zipFileName), ioe), 246 ioe); 247 } 248 } 249 250 /** 251 * Copies a zip entry in the file system. 252 * @param entry the ZipEntry object. 253 * @param destination File where the entry will be copied. 254 * @param is the ZipInputStream that contains the contents to be copied. 255 * @param ratioBeforeCompleted the progress ratio before the zip file is copied. 256 * @param ratioWhenCompleted the progress ratio after the zip file is copied. 257 * @param permissions an ArrayList with permissions whose contents will be updated. 258 * @throws IOException if an error occurs. 259 */ 260 private void copyZipEntry(ZipEntry entry, File destination, 261 ZipInputStream is, int ratioBeforeCompleted, 262 int ratioWhenCompleted, Map<String, ArrayList<String>> permissions) 263 throws IOException 264 { 265 if (application != null) { 266 LocalizableMessage progressSummary = 267 INFO_PROGRESS_EXTRACTING.get(Utils.getPath(destination)); 268 if (application.isVerbose()) 269 { 270 application.notifyListenersWithPoints(ratioBeforeCompleted, 271 progressSummary); 272 } 273 else 274 { 275 application.notifyListenersRatioChange(ratioBeforeCompleted); 276 } 277 } 278 logger.info(LocalizableMessage.raw("extracting " + Utils.getPath(destination))); 279 if (Utils.insureParentsExist(destination)) 280 { 281 if (entry.isDirectory()) 282 { 283 String perm = getDirectoryFileSystemPermissions(destination); 284 ArrayList<String> list = permissions.get(perm); 285 if (list == null) 286 { 287 list = new ArrayList<>(); 288 } 289 list.add(Utils.getPath(destination)); 290 permissions.put(perm, list); 291 292 if (!Utils.createDirectory(destination)) 293 { 294 throw new IOException("Could not create path: " + destination); 295 } 296 } else 297 { 298 String perm = Utils.getFileSystemPermissions(destination); 299 ArrayList<String> list = permissions.get(perm); 300 if (list == null) 301 { 302 list = new ArrayList<>(); 303 } 304 list.add(Utils.getPath(destination)); 305 permissions.put(perm, list); 306 Utils.createFile(destination, is); 307 } 308 } else 309 { 310 throw new IOException("Could not create parent path: " + destination); 311 } 312 if (application != null && application.isVerbose()) 313 { 314 application.notifyListenersDone(ratioWhenCompleted); 315 } 316 } 317 318 /** 319 * Returns the UNIX permissions to be applied to a protected directory. 320 * @return the UNIX permissions to be applied to a protected directory. 321 */ 322 private String getProtectedDirectoryPermissionUnix() 323 { 324 return "700"; 325 } 326 327 /** 328 * Returns the file system permissions for a directory. 329 * @param path the directory for which we want the file permissions. 330 * @return the file system permissions for the directory. 331 */ 332 private String getDirectoryFileSystemPermissions(File path) 333 { 334 // TODO We should get this dynamically during build? 335 return "755"; 336 } 337}