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}