1 /*
2  * Copyright (c) 2009-2012 jMonkeyEngine
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  * * Redistributions of source code must retain the above copyright
10  *   notice, this list of conditions and the following disclaimer.
11  *
12  * * Redistributions in binary form must reproduce the above copyright
13  *   notice, this list of conditions and the following disclaimer in the
14  *   documentation and/or other materials provided with the distribution.
15  *
16  * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17  *   may be used to endorse or promote products derived from this software
18  *   without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32 package com.jme3.scene.plugins.blender.file;
33 
34 import com.jme3.asset.AssetManager;
35 import com.jme3.scene.plugins.blender.exceptions.BlenderFileException;
36 import java.io.BufferedInputStream;
37 import java.io.ByteArrayInputStream;
38 import java.io.IOException;
39 import java.io.InputStream;
40 import java.util.logging.Logger;
41 import java.util.zip.GZIPInputStream;
42 
43 /**
44  * An input stream with random access to data.
45  * @author Marcin Roguski
46  */
47 public class BlenderInputStream extends InputStream {
48 
49     private static final Logger LOGGER = Logger.getLogger(BlenderInputStream.class.getName());
50     /** The default size of the blender buffer. */
51     private static final int DEFAULT_BUFFER_SIZE = 1048576;												//1MB
52     /** The application's asset manager. */
53     private AssetManager assetManager;
54     /**
55      * Size of a pointer; all pointers in the file are stored in this format. '_' means 4 bytes and '-' means 8 bytes.
56      */
57     private int pointerSize;
58     /**
59      * Type of byte ordering used; 'v' means little endian and 'V' means big endian.
60      */
61     private char endianess;
62     /** Version of Blender the file was created in; '248' means version 2.48. */
63     private String versionNumber;
64     /** The buffer we store the read data to. */
65     protected byte[] cachedBuffer;
66     /** The total size of the stored data. */
67     protected int size;
68     /** The current position of the read cursor. */
69     protected int position;
70 	/** The input stream we read the data from. */
71 	protected InputStream		inputStream;
72 
73     /**
74      * Constructor. The input stream is stored and used to read data.
75      * @param inputStream
76      *        the stream we read data from
77      * @param assetManager
78      *        the application's asset manager
79      * @throws BlenderFileException
80      *         this exception is thrown if the file header has some invalid data
81      */
BlenderInputStream(InputStream inputStream, AssetManager assetManager)82     public BlenderInputStream(InputStream inputStream, AssetManager assetManager) throws BlenderFileException {
83         this.assetManager = assetManager;
84         this.inputStream = inputStream;
85         //the size value will canche while reading the file; the available() method cannot be counted on
86         try {
87             size = inputStream.available();
88         } catch (IOException e) {
89             size = 0;
90         }
91         if (size <= 0) {
92             size = BlenderInputStream.DEFAULT_BUFFER_SIZE;
93         }
94 
95         //buffered input stream is used here for much faster file reading
96         BufferedInputStream bufferedInputStream;
97         if (inputStream instanceof BufferedInputStream) {
98             bufferedInputStream = (BufferedInputStream) inputStream;
99         } else {
100             bufferedInputStream = new BufferedInputStream(inputStream);
101         }
102 
103         try {
104             this.readStreamToCache(bufferedInputStream);
105         } catch (IOException e) {
106             throw new BlenderFileException("Problems occured while caching the file!", e);
107         }
108 
109         try {
110             this.readFileHeader();
111         } catch (BlenderFileException e) {//the file might be packed, don't panic, try one more time ;)
112             this.decompressFile();
113             this.position = 0;
114             this.readFileHeader();
115         }
116     }
117 
118     /**
119      * This method reads the whole stream into a buffer.
120      * @param inputStream
121      *        the stream to read the file data from
122      * @throws IOException
123      * 		   an exception is thrown when data read from the stream is invalid or there are problems with i/o
124      *         operations
125      */
readStreamToCache(InputStream inputStream)126     private void readStreamToCache(InputStream inputStream) throws IOException {
127         int data = inputStream.read();
128         cachedBuffer = new byte[size];
129         size = 0;//this will count the actual size
130         while (data != -1) {
131             cachedBuffer[size++] = (byte) data;
132             if (size >= cachedBuffer.length) {//widen the cached array
133                 byte[] newBuffer = new byte[cachedBuffer.length + (cachedBuffer.length >> 1)];
134                 System.arraycopy(cachedBuffer, 0, newBuffer, 0, cachedBuffer.length);
135                 cachedBuffer = newBuffer;
136             }
137             data = inputStream.read();
138         }
139     }
140 
141     /**
142      * This method is used when the blender file is gzipped. It decompresses the data and stores it back into the
143      * cachedBuffer field.
144      */
decompressFile()145     private void decompressFile() {
146         GZIPInputStream gis = null;
147         try {
148             gis = new GZIPInputStream(new ByteArrayInputStream(cachedBuffer));
149             this.readStreamToCache(gis);
150         } catch (IOException e) {
151             throw new IllegalStateException("IO errors occured where they should NOT! "
152                     + "The data is already buffered at this point!", e);
153         } finally {
154             try {
155                 if (gis != null) {
156                     gis.close();
157                 }
158             } catch (IOException e) {
159                 LOGGER.warning(e.getMessage());
160             }
161         }
162     }
163 
164     /**
165      * This method loads the header from the given stream during instance creation.
166      * @param inputStream
167      *        the stream we read the header from
168      * @throws BlenderFileException
169      *         this exception is thrown if the file header has some invalid data
170      */
readFileHeader()171     private void readFileHeader() throws BlenderFileException {
172         byte[] identifier = new byte[7];
173         int bytesRead = this.readBytes(identifier);
174         if (bytesRead != 7) {
175             throw new BlenderFileException("Error reading header identifier. Only " + bytesRead + " bytes read and there should be 7!");
176         }
177         String strIdentifier = new String(identifier);
178         if (!"BLENDER".equals(strIdentifier)) {
179             throw new BlenderFileException("Wrong file identifier: " + strIdentifier + "! Should be 'BLENDER'!");
180         }
181         char pointerSizeSign = (char) this.readByte();
182         if (pointerSizeSign == '-') {
183             pointerSize = 8;
184         } else if (pointerSizeSign == '_') {
185             pointerSize = 4;
186         } else {
187             throw new BlenderFileException("Invalid pointer size character! Should be '_' or '-' and there is: " + pointerSizeSign);
188         }
189         endianess = (char) this.readByte();
190         if (endianess != 'v' && endianess != 'V') {
191             throw new BlenderFileException("Unknown endianess value! 'v' or 'V' expected and found: " + endianess);
192         }
193         byte[] versionNumber = new byte[3];
194         bytesRead = this.readBytes(versionNumber);
195         if (bytesRead != 3) {
196             throw new BlenderFileException("Error reading version numberr. Only " + bytesRead + " bytes read and there should be 3!");
197         }
198         this.versionNumber = new String(versionNumber);
199     }
200 
201     @Override
read()202     public int read() throws IOException {
203         return this.readByte();
204     }
205 
206     /**
207      * This method reads 1 byte from the stream.
208      * It works just in the way the read method does.
209      * It just not throw an exception because at this moment the whole file
210      * is loaded into buffer, so no need for IOException to be thrown.
211      * @return a byte from the stream (1 bytes read)
212      */
readByte()213     public int readByte() {
214         return cachedBuffer[position++] & 0xFF;
215     }
216 
217     /**
218      * This method reads a bytes number big enough to fill the table.
219      * It does not throw exceptions so it is for internal use only.
220      * @param bytes
221      *            an array to be filled with data
222      * @return number of read bytes (a length of array actually)
223      */
readBytes(byte[] bytes)224     private int readBytes(byte[] bytes) {
225         for (int i = 0; i < bytes.length; ++i) {
226             bytes[i] = (byte) this.readByte();
227         }
228         return bytes.length;
229     }
230 
231     /**
232      * This method reads 2-byte number from the stream.
233      * @return a number from the stream (2 bytes read)
234      */
readShort()235     public int readShort() {
236         int part1 = this.readByte();
237         int part2 = this.readByte();
238         if (endianess == 'v') {
239             return (part2 << 8) + part1;
240         } else {
241             return (part1 << 8) + part2;
242         }
243     }
244 
245     /**
246      * This method reads 4-byte number from the stream.
247      * @return a number from the stream (4 bytes read)
248      */
readInt()249     public int readInt() {
250         int part1 = this.readByte();
251         int part2 = this.readByte();
252         int part3 = this.readByte();
253         int part4 = this.readByte();
254         if (endianess == 'v') {
255             return (part4 << 24) + (part3 << 16) + (part2 << 8) + part1;
256         } else {
257             return (part1 << 24) + (part2 << 16) + (part3 << 8) + part4;
258         }
259     }
260 
261     /**
262      * This method reads 4-byte floating point number (float) from the stream.
263      * @return a number from the stream (4 bytes read)
264      */
readFloat()265     public float readFloat() {
266         int intValue = this.readInt();
267         return Float.intBitsToFloat(intValue);
268     }
269 
270     /**
271      * This method reads 8-byte number from the stream.
272      * @return a number from the stream (8 bytes read)
273      */
readLong()274     public long readLong() {
275         long part1 = this.readInt();
276         long part2 = this.readInt();
277         long result = -1;
278         if (endianess == 'v') {
279             result = part2 << 32 | part1;
280         } else {
281             result = part1 << 32 | part2;
282         }
283         return result;
284     }
285 
286     /**
287      * This method reads 8-byte floating point number (double) from the stream.
288      * @return a number from the stream (8 bytes read)
289      */
readDouble()290     public double readDouble() {
291         long longValue = this.readLong();
292         return Double.longBitsToDouble(longValue);
293     }
294 
295     /**
296      * This method reads the pointer value. Depending on the pointer size defined in the header, the stream reads either
297      * 4 or 8 bytes of data.
298      * @return the pointer value
299      */
readPointer()300     public long readPointer() {
301         if (pointerSize == 4) {
302             return this.readInt();
303         }
304         return this.readLong();
305     }
306 
307     /**
308      * This method reads the string. It assumes the string is terminated with zero in the stream.
309      * @return the string read from the stream
310      */
readString()311     public String readString() {
312         StringBuilder stringBuilder = new StringBuilder();
313         int data = this.readByte();
314         while (data != 0) {
315             stringBuilder.append((char) data);
316             data = this.readByte();
317         }
318         return stringBuilder.toString();
319     }
320 
321     /**
322      * This method sets the current position of the read cursor.
323      * @param position
324      *        the position of the read cursor
325      */
setPosition(int position)326     public void setPosition(int position) {
327         this.position = position;
328     }
329 
330     /**
331      * This method returns the position of the read cursor.
332      * @return the position of the read cursor
333      */
getPosition()334     public int getPosition() {
335         return position;
336     }
337 
338     /**
339      * This method returns the blender version number where the file was created.
340      * @return blender version number
341      */
getVersionNumber()342     public String getVersionNumber() {
343         return versionNumber;
344     }
345 
346     /**
347      * This method returns the size of the pointer.
348      * @return the size of the pointer
349      */
getPointerSize()350     public int getPointerSize() {
351         return pointerSize;
352     }
353 
354     /**
355      * This method returns the application's asset manager.
356      * @return the application's asset manager
357      */
getAssetManager()358     public AssetManager getAssetManager() {
359         return assetManager;
360     }
361 
362     /**
363      * This method aligns cursor position forward to a given amount of bytes.
364      * @param bytesAmount
365      *        the byte amount to which we aligh the cursor
366      */
alignPosition(int bytesAmount)367     public void alignPosition(int bytesAmount) {
368         if (bytesAmount <= 0) {
369             throw new IllegalArgumentException("Alignment byte number shoulf be positivbe!");
370         }
371         long move = position % bytesAmount;
372         if (move > 0) {
373             position += bytesAmount - move;
374         }
375     }
376 
377     @Override
close()378     public void close() throws IOException {
379 		inputStream.close();
380 //		cachedBuffer = null;
381 //		size = position = 0;
382     }
383 }
384