Home | History | Annotate | Download | only in file
      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      */
     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