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 */
32package com.jme3.scene.plugins.blender.file;
33
34import com.jme3.asset.AssetManager;
35import com.jme3.scene.plugins.blender.exceptions.BlenderFileException;
36import java.io.BufferedInputStream;
37import java.io.ByteArrayInputStream;
38import java.io.IOException;
39import java.io.InputStream;
40import java.util.logging.Logger;
41import java.util.zip.GZIPInputStream;
42
43/**
44 * An input stream with random access to data.
45 * @author Marcin Roguski
46 */
47public 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     */
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     */
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     */
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
378    public void close() throws IOException {
379		inputStream.close();
380//		cachedBuffer = null;
381//		size = position = 0;
382    }
383}
384