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}