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