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