001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.utils;
014
015import java.io.*;
016import java.util.*;
017import java.util.zip.*;
018
019import org.apache.juneau.*;
020import org.apache.juneau.http.annotation.*;
021
022/**
023 * Utility class for representing the contents of a zip file as a list of entries whose contents don't resolve until
024 * serialization time.
025 *
026 * <p>
027 * Generally associated with <c>RestServlets</c> using the <c>responseHandlers</c> annotation so that
028 * REST methods can easily create ZIP file responses by simply returning instances of this class.
029 */
030@Response
031public class ZipFileList extends LinkedList<ZipFileList.ZipFileEntry> implements Streamable {
032
033   private static final long serialVersionUID = 1L;
034
035   /**
036    * The name of the zip file.
037    */
038   public final String fileName;
039
040   @Header("Content-Type")
041   @Override /* Streamable */
042   public String getMediaType() {
043      return "application/zip";
044   }
045
046   /**
047    * Returns the value for the <c>Content-Disposition</c> header.
048    *
049    * @return The value for the <c>Content-Disposition</c> header.
050    */
051   @Header("Content-Disposition")
052   public String getContentDisposition() {
053      return "attachment;filename=" + fileName;
054   }
055
056   @ResponseBody
057   @Override /* Streamable */
058   public void streamTo(OutputStream os) throws IOException {
059      try (ZipOutputStream zos = new ZipOutputStream(os)) {
060         for (ZipFileEntry e : this)
061            e.write(zos);
062      }
063      os.flush();
064   }
065
066   /**
067    * Constructor.
068    *
069    * @param fileName The file name of the zip file to create.
070    */
071   public ZipFileList(String fileName) {
072      this.fileName = fileName;
073   }
074
075   /**
076    * Add an entry to this list.
077    *
078    * @param e The zip file entry.
079    * @return This object (for method chaining).
080    */
081   public ZipFileList append(ZipFileEntry e) {
082      add(e);
083      return this;
084   }
085
086   /**
087    * Interface for ZipFileList entries.
088    */
089   public static interface ZipFileEntry {
090      /**
091       * Write this entry to the specified output stream.
092       *
093       * @param zos The output stream to write to.
094       * @throws IOException Thrown by underlying stream.
095       */
096      void write(ZipOutputStream zos) throws IOException;
097   }
098
099   /**
100    * ZipFileList entry for File entry types.
101    */
102   public static class FileEntry implements ZipFileEntry {
103
104      /** The root file to base the entry paths on. */
105      protected File root;
106
107      /** The file being zipped. */
108      protected File file;
109
110      /**
111       * Constructor.
112       *
113       * @param root The root file that represents the base path.
114       * @param file The file to add to the zip file.
115       */
116      public FileEntry(File root, File file) {
117         this.root = root;
118         this.file = file;
119      }
120
121      /**
122       * Constructor.
123       *
124       * @param file The file to add to the zip file.
125       */
126      public FileEntry(File file) {
127         this.file = file;
128         this.root = (file.isDirectory() ? file : file.getParentFile());
129      }
130
131      @Override /* ZipFileEntry */
132      public void write(ZipOutputStream zos) throws IOException {
133         addFile(zos, file);
134      }
135
136      /**
137       * Subclasses can override this method to customize which files get added to a zip file.
138       *
139       * @param f The file being added to the zip file.
140       * @return Always returns <jk>true</jk>.
141       */
142      public boolean doAdd(File f) {
143         return true;
144      }
145
146      /**
147       * Adds the specified file to the specified output stream.
148       *
149       * @param zos The output stream.
150       * @param f The file to add.
151       * @throws IOException Thrown by underlying stream.
152       */
153      protected void addFile(ZipOutputStream zos, File f) throws IOException {
154         if (doAdd(f)) {
155            if (f.isDirectory()) {
156               File[] fileList = f.listFiles();
157               if (fileList == null)
158                  throw new IOException(f.toString());
159               for (File fc : fileList)
160                  addFile(zos, fc);
161            } else if (f.canRead()) {
162               String path = f.getAbsolutePath().substring(root.getAbsolutePath().length() + 1).replace('\\', '/');
163               ZipEntry e = new ZipEntry(path);
164               e.setSize(f.length());
165               zos.putNextEntry(e);
166               try (FileInputStream fis = new FileInputStream(f)) {
167                  IOPipe.create(fis, zos).run();
168               }
169            }
170         }
171      }
172   }
173}