001package com.hfg.util.io;
002
003
004import java.io.BufferedInputStream;
005import java.io.ByteArrayInputStream;
006import java.io.ByteArrayOutputStream;
007import java.io.IOException;
008import java.io.InputStream;
009import java.io.OutputStream;
010import java.util.ArrayList;
011import java.util.Arrays;
012import java.util.Collection;
013import java.util.List;
014import java.util.zip.ZipEntry;
015import java.util.zip.ZipOutputStream;
016
017import com.hfg.exception.ProgrammingException;
018import com.hfg.util.collection.CollectionUtil;
019
020//------------------------------------------------------------------------------
021/**
022 Container for a file name plus file contents as bytes.
023 Useful for holding temp files created in memory before producing a zip archive.
024 <div>
025 @author J. Alex Taylor, hairyfatguy.com
026 </div>
027 */
028//------------------------------------------------------------------------------
029// com.hfg XML/HTML Coding Library
030//
031// This library is free software; you can redistribute it and/or
032// modify it under the terms of the GNU Lesser General Public
033// License as published by the Free Software Foundation; either
034// version 2.1 of the License, or (at your option) any later version.
035//
036// This library is distributed in the hope that it will be useful,
037// but WITHOUT ANY WARRANTY; without even the implied warranty of
038// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
039// Lesser General Public License for more details.
040//
041// You should have received a copy of the GNU Lesser General Public
042// License along with this library; if not, write to the Free Software
043// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
044//
045// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
046// jataylor@hairyfatguy.com
047//------------------------------------------------------------------------------
048
049public class FileBytes extends OutputStream
050{
051   private String  mName;
052//   List<byte[]>    mDataChunks;
053   private Long    mLength;
054
055   private List<byte[]> mCompressedDataChunks;
056   private byte[] mUncompressedDataChunk;
057   private int    mUncompressedDataChunkSize;
058
059   private static int sDefaultChunkSize = 8192;
060
061   //###########################################################################
062   // CONSTRUCTORS
063   //###########################################################################
064
065   //--------------------------------------------------------------------------
066   public FileBytes(String inName)
067   {
068      mName = inName;
069   }
070
071   //###########################################################################
072   // PUBLIC METHODS
073   //###########################################################################
074
075   //--------------------------------------------------------------------------
076   public String name()
077   {
078      return mName;
079   }
080
081   //--------------------------------------------------------------------------
082   public long length()
083   {
084      return mLength != null ? mLength : 0;
085   }
086
087   //--------------------------------------------------------------------------
088   public FileBytes setData(byte[] inData)
089         throws IOException
090   {
091      if (inData != null)
092      {
093         setData(new ByteArrayInputStream(inData));
094      }
095      else
096      {
097         clearData();
098      }
099
100      return this;
101   }
102
103   //--------------------------------------------------------------------------
104   public FileBytes setData(InputStream inStream)
105         throws IOException
106   {
107      clearData();
108
109      if (inStream != null)
110      {
111         mCompressedDataChunks = new ArrayList<>(50);
112         BufferedInputStream bufferedStream = null;
113         try
114         {
115            bufferedStream = new BufferedInputStream(inStream);
116
117            byte[] readBuffer = new byte[4096];
118            int readSize = 0;
119            while ((readSize = bufferedStream.read(readBuffer)) >= 0)
120            {
121               mCompressedDataChunks.add(GZIP.compress(readBuffer, 0, readSize));
122               mLength += readSize;
123            }
124         }
125         finally
126         {
127            if (bufferedStream != null)
128            {
129               bufferedStream.close();
130            }
131         }
132      }
133
134      return this;
135   }
136
137
138   //--------------------------------------------------------------------------
139   public void write(int inByte)
140         throws IOException
141   {
142      appendData(inByte);
143   }
144
145   //--------------------------------------------------------------------------
146   public void write(byte[] inBytes)
147         throws IOException
148   {
149      appendData(inBytes);
150   }
151
152   //--------------------------------------------------------------------------
153   public void write(byte[] inBytes, int inOffset, int inLength)
154         throws IOException
155   {
156      appendData(inBytes, inOffset, inLength);
157   }
158
159   //--------------------------------------------------------------------------
160   public void appendData(int inByte)
161         throws IOException
162   {
163      if (null == mUncompressedDataChunk)
164      {
165         mUncompressedDataChunk = new byte[sDefaultChunkSize];
166         mUncompressedDataChunkSize = 0;
167      }
168
169      mUncompressedDataChunk[mUncompressedDataChunkSize++] = (byte) inByte;
170      if (mUncompressedDataChunkSize == mUncompressedDataChunk.length)
171      {
172         if (null == mCompressedDataChunks)
173         {
174            mCompressedDataChunks = new ArrayList<>(50);
175         }
176
177         mCompressedDataChunks.add(GZIP.compress(mUncompressedDataChunk, 0, mUncompressedDataChunkSize));
178         mUncompressedDataChunkSize = 0;
179      }
180      else
181      {
182         mUncompressedDataChunkSize++;
183      }
184
185      if (mLength != null)
186      {
187         mLength++;
188      }
189      else
190      {
191         mLength = 1L;
192      }
193   }
194
195   //--------------------------------------------------------------------------
196   public FileBytes appendData(byte[] inBytes)
197         throws IOException
198   {
199      return appendData(inBytes, 0, inBytes.length);
200   }
201
202   //--------------------------------------------------------------------------
203      public FileBytes appendData(byte[] inBytes, int inOffset, int inLength)
204         throws IOException
205   {
206      if (null == mUncompressedDataChunk)
207      {
208         mUncompressedDataChunk = new byte[sDefaultChunkSize];
209         mUncompressedDataChunkSize = 0;
210      }
211
212      int totalBytesAdded = 0;
213      while (totalBytesAdded < inLength)
214      {
215         int lengthRemainingToCopy = inLength - totalBytesAdded;
216
217         int bytesCopied = Math.min(lengthRemainingToCopy, mUncompressedDataChunk.length - mUncompressedDataChunkSize);
218         System.arraycopy(inBytes, inOffset, mUncompressedDataChunk, mUncompressedDataChunkSize, bytesCopied);
219
220         if (mUncompressedDataChunkSize == mUncompressedDataChunk.length)
221         {
222            if (null == mCompressedDataChunks)
223            {
224               mCompressedDataChunks = new ArrayList<>(50);
225            }
226
227            mCompressedDataChunks.add(GZIP.compress(mUncompressedDataChunk, 0, mUncompressedDataChunkSize));
228            mUncompressedDataChunkSize = 0;
229         }
230         else
231         {
232            mUncompressedDataChunkSize += bytesCopied;
233         }
234
235         totalBytesAdded += bytesCopied;
236      }
237
238      if (mLength != null)
239      {
240         mLength += inBytes.length;
241      }
242      else
243      {
244         mLength = (long) inLength;
245      }
246
247      return this;
248   }
249
250   //--------------------------------------------------------------------------
251   public FileBytes appendData(FileBytes inFileBytes)
252         throws IOException
253   {
254      if (inFileBytes.length() > 0)
255      {
256         if (null == mCompressedDataChunks)
257         {
258            mCompressedDataChunks = new ArrayList<>(50);
259         }
260
261         // Add whatever we have sitting uncompressed at the moment
262         if (mUncompressedDataChunkSize > 0)
263         {
264            mCompressedDataChunks.add(GZIP.compress(mUncompressedDataChunk, 0, mUncompressedDataChunkSize));
265            mUncompressedDataChunkSize = 0;
266         }
267
268         // Add the new compressed chunks
269         for (byte[] bytes : inFileBytes.mCompressedDataChunks)
270         {
271            mCompressedDataChunks.add(bytes);
272         }
273
274         // Add the new uncompressed chunk
275         if (inFileBytes.mUncompressedDataChunk != null)
276         {
277            mUncompressedDataChunk = Arrays.copyOf(inFileBytes.mUncompressedDataChunk, sDefaultChunkSize);
278            mUncompressedDataChunkSize = inFileBytes.mUncompressedDataChunkSize;
279         }
280         else
281         {
282            mUncompressedDataChunkSize = 0;
283         }
284
285         if (mLength != null)
286         {
287            mLength += inFileBytes.length();
288         }
289         else
290         {
291            mLength = inFileBytes.length();
292         }
293      }
294
295      return this;
296   }
297
298   //--------------------------------------------------------------------------
299   public byte[] getBytes()
300   {
301      ByteArrayOutputStream stream;
302      try
303      {
304         stream = new ByteArrayOutputStream();
305         StreamUtil.writeToStream(getDataStream(), stream);
306      }
307      catch (IOException e)
308      {
309         throw new ProgrammingException(e);
310      }
311
312      return stream.toByteArray();
313   }
314
315   //--------------------------------------------------------------------------
316   public InputStream getDataStream()
317   {
318      return new DataStreamer();
319   }
320
321   //--------------------------------------------------------------------------
322   public void writeData(OutputStream inStream)
323      throws IOException
324   {
325      if (CollectionUtil.hasValues(mCompressedDataChunks))
326      {
327         for (byte[] chunk : mCompressedDataChunks)
328         {
329            inStream.write(GZIP.uncompress(chunk));
330         }
331      }
332
333      if (mUncompressedDataChunkSize > 0)
334      {
335         inStream.write(mUncompressedDataChunk, 0, mUncompressedDataChunkSize);
336      }
337
338      inStream.flush();
339   }
340
341   //--------------------------------------------------------------------------
342   public static void streamAsZipFile(OutputStream inWriteTarget, String inBaseZipFileName, Collection<FileBytes> inFiles)
343         throws IOException
344   {
345      ZipOutputStream zipStream = null;
346      try
347      {
348         zipStream = new ZipOutputStream(inWriteTarget);
349
350         for (FileBytes file : inFiles)
351         {
352            ZipEntry zipEntry = new ZipEntry(inBaseZipFileName + "/" + file.name());
353            zipStream.putNextEntry(zipEntry);
354            file.writeData(zipStream);
355            zipStream.closeEntry();
356         }
357      }
358      finally
359      {
360         if (zipStream != null)
361         {
362            zipStream.finish();
363         }
364      }
365   }
366
367
368   //###########################################################################
369   // PRIVATE METHODS
370   //###########################################################################
371
372   //--------------------------------------------------------------------------
373   private void clearData()
374   {
375      mCompressedDataChunks = null;
376      mLength = 0L;
377   }
378
379   //###########################################################################
380   // INNER CLASS
381   //###########################################################################
382
383   private class DataStreamer extends InputStream
384   {
385      private byte[]    mCurrentCompressedChunk;
386      private int       mCurrentCompressedChunkIndex;
387      private int       mUncompressedChunkIndex;
388      private int       mByteIndex;
389      private boolean   mDone = false;
390
391      int count = 0;
392      //-----------------------------------------------------------------------
393      public DataStreamer()
394      {
395         mCurrentCompressedChunkIndex = 0;
396      }
397
398      //-----------------------------------------------------------------------
399      public int read()
400      {
401         return (mDone ? -1 : getNextByte());
402      }
403
404
405      //-----------------------------------------------------------------------
406      @Override
407      public int read(byte b[], int off, int len)
408            throws IOException
409      {
410         if (b == null)
411         {
412            throw new NullPointerException();
413         }
414         else if (off < 0 || len < 0 || len > b.length - off)
415         {
416            throw new IndexOutOfBoundsException();
417         }
418         else if (len == 0)
419         {
420            return 0;
421         }
422
423         int c = read();
424         if (c == -1 && mDone)
425         {
426            return -1;
427         }
428         b[off] = (byte) c;
429
430         int i = 1;
431         for (; i < len; i++)
432         {
433            c = read();
434            if (c == -1 && mDone)
435            {
436               break;
437            }
438            b[off + i] = (byte) c;
439         }
440
441         return i;
442      }
443
444      //-----------------------------------------------------------------------
445      private byte getNextByte()
446      {
447         byte nextByte = -1;
448
449         if (null == mCurrentCompressedChunk
450             && mCompressedDataChunks != null)
451         {
452            mCurrentCompressedChunk = GZIP.uncompress(mCompressedDataChunks.get(mCurrentCompressedChunkIndex));
453            mByteIndex = 0;
454         }
455
456         if (null == mCurrentCompressedChunk)
457         {
458            // Done with compressed chunks. Read from the uncompressed chunk.
459            nextByte = mUncompressedDataChunk[mUncompressedChunkIndex++];
460            if (mUncompressedChunkIndex >= mUncompressedDataChunkSize)
461            {
462               // This was the last chunk.
463               mDone = true;
464            }
465         }
466         else
467         {
468            nextByte = mCurrentCompressedChunk[mByteIndex++];
469
470            if (mByteIndex >= mCurrentCompressedChunk.length)
471            {
472               // This is the last byte in this chunk.
473               mCurrentCompressedChunk = null;
474               mCurrentCompressedChunkIndex++;
475               if (mCurrentCompressedChunkIndex < 0 || mCurrentCompressedChunkIndex == mCompressedDataChunks.size())
476               {
477                  if (mUncompressedDataChunkSize > 0)
478                  {
479                     mUncompressedChunkIndex = 0;
480                  }
481                  else
482                  {
483                     // This was the last chunk.
484                     mDone = true;
485                  }
486               }
487            }
488         }
489
490         return nextByte;
491      }
492   }
493
494}