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  public 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  public 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  public void extract(String destination) throws ApplicationException {
148    extract(destination, true);
149  }
150
151  /**
152   * Performs the zip extraction.
153   * @param destDir String representing the directory where the zip file will
154   * be extracted
155   * @param removeFirstPath when true removes each zip entry's initial path
156   * when copied to the destination folder.  So for instance if the zip entry's
157   * name was /OpenDJ-2.4.x/some_file the file would appear in the destination
158   * directory as 'some_file'.
159   * @throws ApplicationException if something goes wrong
160   */
161  public void extract(String destDir, boolean removeFirstPath)
162          throws ApplicationException
163  {
164    ZipInputStream zipIn = new ZipInputStream(is);
165    int nEntries = 1;
166
167    /* This map is updated in the copyZipEntry method with the permissions
168     * of the files that have been copied.  Once all the files have
169     * been copied to the file system we will update the file permissions of
170     * these files.  This is done this way to group the number of calls to
171     * Runtime.exec (which is required to update the file system permissions).
172     */
173    Map<String, List<String>> permissions = new HashMap<>();
174    permissions.put(getProtectedDirectoryPermissionUnix(), newArrayList(destDir));
175    try {
176      if(application != null) {
177        application.checkAbort();
178      }
179      ZipEntry entry = zipIn.getNextEntry();
180      while (entry != null) {
181        if(application != null) {
182          application.checkAbort();
183        }
184        int ratioBeforeCompleted = minRatio
185                + ((nEntries - 1) * (maxRatio - minRatio) / numberZipEntries);
186        int ratioWhenCompleted =
187                minRatio + (nEntries * (maxRatio - minRatio) / numberZipEntries);
188
189        String name = entry.getName();
190        if (name != null && removeFirstPath) {
191          int sepPos = name.indexOf(ZIP_ENTRY_NAME_SEP);
192          if (sepPos != -1) {
193            name = name.substring(sepPos + 1);
194          } else {
195            logger.warn(LocalizableMessage.raw(
196                    "zip entry name does not contain a path separator"));
197          }
198        }
199        if (name != null && name.length() > 0) {
200          try {
201            File destination = new File(destDir, name);
202            copyZipEntry(entry, destination, zipIn,
203                    ratioBeforeCompleted, ratioWhenCompleted, permissions);
204          } catch (IOException ioe) {
205            throw new ApplicationException(
206                ReturnCode.FILE_SYSTEM_ACCESS_ERROR,
207                getThrowableMsg(INFO_ERROR_COPYING.get(entry.getName()), ioe),
208                ioe);
209          }
210        }
211
212        zipIn.closeEntry();
213        entry = zipIn.getNextEntry();
214        nEntries++;
215      }
216
217      if (isUnix()) {
218        // Change the permissions for UNIX systems
219        for (String perm : permissions.keySet()) {
220          List<String> paths = permissions.get(perm);
221          try {
222            int result = Utils.setPermissionsUnix(paths, perm);
223            if (result != 0) {
224              throw new IOException("Could not set permissions on files "
225                      + paths + ".  The chmod error code was: " + result);
226            }
227          } catch (InterruptedException ie) {
228            throw new IOException("Could not set permissions on files " + paths
229                + ".  The chmod call returned an InterruptedException.", ie);
230          }
231        }
232      }
233    } catch (IOException ioe) {
234      throw new ApplicationException(
235          ReturnCode.FILE_SYSTEM_ACCESS_ERROR,
236          getThrowableMsg(INFO_ERROR_ZIP_STREAM.get(zipFileName), ioe),
237          ioe);
238    }
239  }
240
241  /**
242    * Copies a zip entry in the file system.
243    * @param entry the ZipEntry object.
244    * @param destination File where the entry will be copied.
245    * @param is the ZipInputStream that contains the contents to be copied.
246    * @param ratioBeforeCompleted the progress ratio before the zip file is copied.
247    * @param ratioWhenCompleted the progress ratio after the zip file is copied.
248    * @param permissions an ArrayList with permissions whose contents will be updated.
249    * @throws IOException if an error occurs.
250    */
251  private void copyZipEntry(ZipEntry entry, File destination,
252      ZipInputStream is, int ratioBeforeCompleted,
253      int ratioWhenCompleted, Map<String, List<String>> permissions)
254      throws IOException
255  {
256    if (application != null) {
257      LocalizableMessage progressSummary =
258              INFO_PROGRESS_EXTRACTING.get(Utils.getPath(destination));
259      if (application.isVerbose())
260      {
261        application.notifyListenersWithPoints(ratioBeforeCompleted,
262            progressSummary);
263      }
264      else
265      {
266        application.notifyListenersRatioChange(ratioBeforeCompleted);
267      }
268    }
269    logger.info(LocalizableMessage.raw("extracting " + Utils.getPath(destination)));
270
271    if (!Utils.ensureParentsExist(destination))
272    {
273      throw new IOException("Could not create parent path: " + destination);
274    }
275
276    if (entry.isDirectory())
277    {
278      String perm = getDirectoryFileSystemPermissions(destination);
279      addPermission(destination, permissions, perm);
280      if (!Utils.createDirectory(destination))
281      {
282        throw new IOException("Could not create path: " + destination);
283      }
284    } else
285    {
286      String perm = Utils.getFileSystemPermissions(destination);
287      addPermission(destination, permissions, perm);
288      Utils.createFile(destination, is);
289    }
290    if (application != null && application.isVerbose())
291    {
292      application.notifyListenersDone(ratioWhenCompleted);
293    }
294  }
295
296  private void addPermission(File destination, Map<String, List<String>> permissions, String perm)
297  {
298    List<String> list = permissions.get(perm);
299    if (list == null)
300    {
301      list = new ArrayList<>();
302      permissions.put(perm, list);
303    }
304    list.add(Utils.getPath(destination));
305  }
306
307  /**
308   * Returns the UNIX permissions to be applied to a protected directory.
309   * @return the UNIX permissions to be applied to a protected directory.
310   */
311  private String getProtectedDirectoryPermissionUnix()
312  {
313    return "700";
314  }
315
316  /**
317   * Returns the file system permissions for a directory.
318   * @param path the directory for which we want the file permissions.
319   * @return the file system permissions for the directory.
320   */
321  private String getDirectoryFileSystemPermissions(File path)
322  {
323    // TODO We should get this dynamically during build?
324    return "755";
325  }
326}