001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.util.resource;
018
019import java.io.BufferedInputStream;
020import java.io.ByteArrayInputStream;
021import java.io.ByteArrayOutputStream;
022import java.io.FileInputStream;
023import java.io.IOException;
024import java.io.InputStream;
025import java.time.Instant;
026import java.util.zip.ZipEntry;
027import java.util.zip.ZipOutputStream;
028import org.apache.wicket.util.file.File;
029import org.apache.wicket.util.lang.Args;
030import org.apache.wicket.util.lang.Bytes;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034
035/**
036 * An IResourceStream that ZIPs a directory's contents on the fly
037 * 
038 * <p>
039 * <b>NOTE 1.</b> As a future improvement, cache a map of generated ZIP files for every directory
040 * and use a Watcher to detect modifications in this directory. Using ehcache would be good for
041 * that, but it's not in Wicket dependencies yet. <b>No caching of the generated ZIP files is done
042 * yet.</b>
043 * </p>
044 * 
045 * <p>
046 * <b>NOTE 2.</b> As a future improvement, implement getLastModified() and request
047 * ResourceStreamRequestTarget to generate Last-Modified and Expires HTTP headers. <b>No HTTP cache
048 * headers are provided yet</b>. See WICKET-385
049 * </p>
050 * 
051 * @author <a href="mailto:jbq@apache.org">Jean-Baptiste Quenot</a>
052 */
053public class ZipResourceStream extends AbstractResourceStream
054{
055        private static final long serialVersionUID = 1L;
056
057        private static final Logger log = LoggerFactory.getLogger(ZipResourceStream.class);
058
059        private final transient ByteArrayOutputStream bytearray;
060
061        /**
062         * Construct.
063         * 
064         * @param dir
065         *            The directory where to look for files. The directory itself will not be included
066         *            in the ZIP.
067         * @param recursive
068         *            If true, all subdirs will be zipped as well
069         */
070        public ZipResourceStream(final File dir, final boolean recursive)
071        {
072                Args.notNull(dir, "dir");
073                Args.isTrue(dir.isDirectory(), "Not a directory: '{}'", dir);
074
075                bytearray = new ByteArrayOutputStream();
076                try
077                {
078                        ZipOutputStream out = new ZipOutputStream(bytearray);
079                        try
080                        {
081                                zipDir(dir, out, "", recursive);
082                        } finally {
083                                out.close();
084                        }
085                }
086                catch (RuntimeException e)
087                {
088                        throw e;
089                }
090                catch (Exception e)
091                {
092                        throw new RuntimeException(e);
093                }
094        }
095
096        /**
097         * Construct. Until Wicket 1.4-RC3 recursive zip was not supported. In order not to change the
098         * behavior, using this constructor will default to recursive == false.
099         * 
100         * @param dir
101         *            The directory where to look for files. The directory itself will not be included
102         *            in the ZIP.
103         */
104        public ZipResourceStream(final File dir)
105        {
106                this(dir, false);
107        }
108
109        /**
110         * Recursive method for zipping the contents of a directory including nested directories.
111         * 
112         * @param dir
113         *            dir to be zipped
114         * @param out
115         *            ZipOutputStream to write to
116         * @param path
117         *            Path to nested dirs (used in resursive calls)
118         * @param recursive
119         *            If true, all subdirs will be zipped as well
120         * @throws IOException
121         */
122        private static void zipDir(final File dir, final ZipOutputStream out, final String path,
123                final boolean recursive) throws IOException
124        {
125                Args.notNull(dir, "dir");
126                Args.isTrue(dir.isDirectory(), "Not a directory: '{}'", dir);
127
128                String[] files = dir.list();
129
130                int BUFFER = 2048;
131                BufferedInputStream origin;
132                byte data[] = new byte[BUFFER];
133
134                if (files != null)
135                {
136                        for (String file : files)
137                        {
138                                log.debug("Adding: '{}'", file);
139
140                                File f = new File(dir, file);
141                                if (f.isDirectory())
142                                {
143                                        if (recursive)
144                                        {
145                                                zipDir(f, out, path + f.getName() + "/", recursive);
146                                        }
147                                } else
148                                {
149                                        out.putNextEntry(new ZipEntry(path + f.getName()));
150
151                                        FileInputStream fi = new FileInputStream(f);
152                                        origin = new BufferedInputStream(fi, BUFFER);
153
154                                        try
155                                        {
156                                                int count;
157                                                while ((count = origin.read(data, 0, BUFFER)) != -1)
158                                                {
159                                                        out.write(data, 0, count);
160                                                }
161                                        } finally
162                                        {
163                                                origin.close();
164                                        }
165                                }
166                        }
167                }
168
169                if (path.isEmpty())
170                {
171                        out.close();
172                }
173        }
174
175        /**
176         * @see org.apache.wicket.util.resource.IResourceStream#close()
177         */
178        @Override
179        public void close() throws IOException
180        {
181        }
182
183        /**
184         * @see org.apache.wicket.util.resource.IResourceStream#getContentType()
185         */
186        @Override
187        public String getContentType()
188        {
189                return null;
190        }
191
192        /**
193         * @see org.apache.wicket.util.resource.IResourceStream#getInputStream()
194         */
195        @Override
196        public InputStream getInputStream() throws ResourceStreamNotFoundException
197        {
198                return new ByteArrayInputStream(bytearray.toByteArray());
199        }
200
201        /**
202         * @see org.apache.wicket.util.resource.AbstractResourceStream#length()
203         */
204        @Override
205        public Bytes length()
206        {
207                return Bytes.bytes(bytearray.size());
208        }
209
210        /**
211         * @see org.apache.wicket.util.resource.AbstractResourceStream#lastModifiedTime()
212         */
213        @Override
214        public Instant lastModifiedTime()
215        {
216                return null;
217        }
218}