1 /* 2 * Copyright (c) 2009-2010 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 33 package com.jme3.export.binary; 34 35 import com.jme3.export.FormatVersion; 36 import com.jme3.export.JmeExporter; 37 import com.jme3.export.Savable; 38 import com.jme3.export.SavableClassUtil; 39 import com.jme3.math.FastMath; 40 import java.io.*; 41 import java.util.ArrayList; 42 import java.util.HashMap; 43 import java.util.IdentityHashMap; 44 import java.util.logging.Level; 45 import java.util.logging.Logger; 46 47 /** 48 * Exports to the jME Binary Format. Format descriptor: (each numbered item 49 * denotes a series of bytes that follows sequentially one after the next.) 50 * <p> 51 * 1. "number of classes" - four bytes - int value representing the number of 52 * entries in the class lookup table. 53 * </p> 54 * <p> 55 * CLASS TABLE: There will be X blocks each consisting of numbers 2 thru 9, 56 * where X = the number read in 1. 57 * </p> 58 * <p> 59 * 2. "class alias" - 1...X bytes, where X = ((int) FastMath.log(aliasCount, 60 * 256) + 1) - an alias used when writing object data to match an object to its 61 * appropriate object class type. 62 * </p> 63 * <p> 64 * 3. "full class name size" - four bytes - int value representing number of 65 * bytes to read in for next field. 66 * </p> 67 * <p> 68 * 4. "full class name" - 1...X bytes representing a String value, where X = the 69 * number read in 3. The String is the fully qualified class name of the Savable 70 * class, eg "<code>com.jme.math.Vector3f</code>" 71 * </p> 72 * <p> 73 * 5. "number of fields" - four bytes - int value representing number of blocks 74 * to read in next (numbers 6 - 9), where each block represents a field in this 75 * class. 76 * </p> 77 * <p> 78 * 6. "field alias" - 1 byte - the alias used when writing out fields in a 79 * class. Because it is a single byte, a single class can not save out more than 80 * a total of 256 fields. 81 * </p> 82 * <p> 83 * 7. "field type" - 1 byte - a value representing the type of data a field 84 * contains. This value is taken from the static fields of 85 * <code>com.jme.util.export.binary.BinaryClassField</code>. 86 * </p> 87 * <p> 88 * 8. "field name size" - 4 bytes - int value representing the size of the next 89 * field. 90 * </p> 91 * <p> 92 * 9. "field name" - 1...X bytes representing a String value, where X = the 93 * number read in 8. The String is the full String value used when writing the 94 * current field. 95 * </p> 96 * <p> 97 * 10. "number of unique objects" - four bytes - int value representing the 98 * number of data entries in this file. 99 * </p> 100 * <p> 101 * DATA LOOKUP TABLE: There will be X blocks each consisting of numbers 11 and 102 * 12, where X = the number read in 10. 103 * </p> 104 * <p> 105 * 11. "data id" - four bytes - int value identifying a single unique object 106 * that was saved in this data file. 107 * </p> 108 * <p> 109 * 12. "data location" - four bytes - int value representing the offset in the 110 * object data portion of this file where the object identified in 11 is 111 * located. 112 * </p> 113 * <p> 114 * 13. "future use" - four bytes - hardcoded int value 1. 115 * </p> 116 * <p> 117 * 14. "root id" - four bytes - int value identifying the top level object. 118 * </p> 119 * <p> 120 * OBJECT DATA SECTION: There will be X blocks each consisting of numbers 15 121 * thru 19, where X = the number of unique location values named in 12. 122 * <p> 123 * 15. "class alias" - see 2. 124 * </p> 125 * <p> 126 * 16. "data length" - four bytes - int value representing the length in bytes 127 * of data stored in fields 17 and 18 for this object. 128 * </p> 129 * <p> 130 * FIELD ENTRY: There will be X blocks each consisting of numbers 18 and 19 131 * </p> 132 * <p> 133 * 17. "field alias" - see 6. 134 * </p> 135 * <p> 136 * 18. "field data" - 1...X bytes representing the field data. The data length 137 * is dependent on the field type and contents. 138 * </p> 139 * 140 * @author Joshua Slack 141 */ 142 143 public class BinaryExporter implements JmeExporter { 144 private static final Logger logger = Logger.getLogger(BinaryExporter.class 145 .getName()); 146 147 protected int aliasCount = 1; 148 protected int idCount = 1; 149 150 protected IdentityHashMap<Savable, BinaryIdContentPair> contentTable 151 = new IdentityHashMap<Savable, BinaryIdContentPair>(); 152 153 protected HashMap<Integer, Integer> locationTable 154 = new HashMap<Integer, Integer>(); 155 156 // key - class name, value = bco 157 private HashMap<String, BinaryClassObject> classes 158 = new HashMap<String, BinaryClassObject>(); 159 160 private ArrayList<Savable> contentKeys = new ArrayList<Savable>(); 161 162 public static boolean debug = false; 163 public static boolean useFastBufs = true; 164 165 public BinaryExporter() { 166 } 167 168 public static BinaryExporter getInstance() { 169 return new BinaryExporter(); 170 } 171 172 public boolean save(Savable object, OutputStream os) throws IOException { 173 // reset some vars 174 aliasCount = 1; 175 idCount = 1; 176 classes.clear(); 177 contentTable.clear(); 178 locationTable.clear(); 179 contentKeys.clear(); 180 181 // write signature and version 182 os.write(ByteUtils.convertToBytes(FormatVersion.SIGNATURE)); 183 os.write(ByteUtils.convertToBytes(FormatVersion.VERSION)); 184 185 int id = processBinarySavable(object); 186 187 // write out tag table 188 int classTableSize = 0; 189 int classNum = classes.keySet().size(); 190 int aliasSize = ((int) FastMath.log(classNum, 256) + 1); // make all 191 // aliases a 192 // fixed width 193 194 os.write(ByteUtils.convertToBytes(classNum)); 195 for (String key : classes.keySet()) { 196 BinaryClassObject bco = classes.get(key); 197 198 // write alias 199 byte[] aliasBytes = fixClassAlias(bco.alias, 200 aliasSize); 201 os.write(aliasBytes); 202 classTableSize += aliasSize; 203 204 // jME3 NEW: Write class hierarchy version numbers 205 os.write( bco.classHierarchyVersions.length ); 206 for (int version : bco.classHierarchyVersions){ 207 os.write(ByteUtils.convertToBytes(version)); 208 } 209 classTableSize += 1 + bco.classHierarchyVersions.length * 4; 210 211 // write classname size & classname 212 byte[] classBytes = key.getBytes(); 213 os.write(ByteUtils.convertToBytes(classBytes.length)); 214 os.write(classBytes); 215 classTableSize += 4 + classBytes.length; 216 217 // for each field, write alias, type, and name 218 os.write(ByteUtils.convertToBytes(bco.nameFields.size())); 219 for (String fieldName : bco.nameFields.keySet()) { 220 BinaryClassField bcf = bco.nameFields.get(fieldName); 221 os.write(bcf.alias); 222 os.write(bcf.type); 223 224 // write classname size & classname 225 byte[] fNameBytes = fieldName.getBytes(); 226 os.write(ByteUtils.convertToBytes(fNameBytes.length)); 227 os.write(fNameBytes); 228 classTableSize += 2 + 4 + fNameBytes.length; 229 } 230 } 231 232 ByteArrayOutputStream out = new ByteArrayOutputStream(); 233 // write out data to a seperate stream 234 int location = 0; 235 // keep track of location for each piece 236 HashMap<String, ArrayList<BinaryIdContentPair>> alreadySaved = new HashMap<String, ArrayList<BinaryIdContentPair>>( 237 contentTable.size()); 238 for (Savable savable : contentKeys) { 239 // look back at previous written data for matches 240 String savableName = savable.getClass().getName(); 241 BinaryIdContentPair pair = contentTable.get(savable); 242 ArrayList<BinaryIdContentPair> bucket = alreadySaved 243 .get(savableName + getChunk(pair)); 244 int prevLoc = findPrevMatch(pair, bucket); 245 if (prevLoc != -1) { 246 locationTable.put(pair.getId(), prevLoc); 247 continue; 248 } 249 250 locationTable.put(pair.getId(), location); 251 if (bucket == null) { 252 bucket = new ArrayList<BinaryIdContentPair>(); 253 alreadySaved.put(savableName + getChunk(pair), bucket); 254 } 255 bucket.add(pair); 256 byte[] aliasBytes = fixClassAlias(classes.get(savableName).alias, aliasSize); 257 out.write(aliasBytes); 258 location += aliasSize; 259 BinaryOutputCapsule cap = contentTable.get(savable).getContent(); 260 out.write(ByteUtils.convertToBytes(cap.bytes.length)); 261 location += 4; // length of bytes 262 out.write(cap.bytes); 263 location += cap.bytes.length; 264 } 265 266 // write out location table 267 // tag/location 268 int numLocations = locationTable.keySet().size(); 269 os.write(ByteUtils.convertToBytes(numLocations)); 270 int locationTableSize = 0; 271 for (Integer key : locationTable.keySet()) { 272 os.write(ByteUtils.convertToBytes(key)); 273 os.write(ByteUtils.convertToBytes(locationTable.get(key))); 274 locationTableSize += 8; 275 } 276 277 // write out number of root ids - hardcoded 1 for now 278 os.write(ByteUtils.convertToBytes(1)); 279 280 // write out root id 281 os.write(ByteUtils.convertToBytes(id)); 282 283 // append stream to the output stream 284 out.writeTo(os); 285 286 287 out = null; 288 os = null; 289 290 if (debug ) { 291 logger.info("Stats:"); 292 logger.log(Level.INFO, "classes: {0}", classNum); 293 logger.log(Level.INFO, "class table: {0} bytes", classTableSize); 294 logger.log(Level.INFO, "objects: {0}", numLocations); 295 logger.log(Level.INFO, "location table: {0} bytes", locationTableSize); 296 logger.log(Level.INFO, "data: {0} bytes", location); 297 } 298 299 return true; 300 } 301 302 protected String getChunk(BinaryIdContentPair pair) { 303 return new String(pair.getContent().bytes, 0, Math.min(64, pair 304 .getContent().bytes.length)); 305 } 306 307 protected int findPrevMatch(BinaryIdContentPair oldPair, 308 ArrayList<BinaryIdContentPair> bucket) { 309 if (bucket == null) 310 return -1; 311 for (int x = bucket.size(); --x >= 0;) { 312 BinaryIdContentPair pair = bucket.get(x); 313 if (pair.getContent().equals(oldPair.getContent())) 314 return locationTable.get(pair.getId()); 315 } 316 return -1; 317 } 318 319 protected byte[] fixClassAlias(byte[] bytes, int width) { 320 if (bytes.length != width) { 321 byte[] newAlias = new byte[width]; 322 for (int x = width - bytes.length; x < width; x++) 323 newAlias[x] = bytes[x - bytes.length]; 324 return newAlias; 325 } 326 return bytes; 327 } 328 329 public boolean save(Savable object, File f) throws IOException { 330 File parentDirectory = f.getParentFile(); 331 if(parentDirectory != null && !parentDirectory.exists()) { 332 parentDirectory.mkdirs(); 333 } 334 335 FileOutputStream fos = new FileOutputStream(f); 336 boolean rVal = save(object, fos); 337 fos.close(); 338 return rVal; 339 } 340 341 public BinaryOutputCapsule getCapsule(Savable object) { 342 return contentTable.get(object).getContent(); 343 } 344 345 private BinaryClassObject createClassObject(Class clazz) throws IOException{ 346 BinaryClassObject bco = new BinaryClassObject(); 347 bco.alias = generateTag(); 348 bco.nameFields = new HashMap<String, BinaryClassField>(); 349 bco.classHierarchyVersions = SavableClassUtil.getSavableVersions(clazz); 350 351 classes.put(clazz.getName(), bco); 352 353 return bco; 354 } 355 356 public int processBinarySavable(Savable object) throws IOException { 357 if (object == null) { 358 return -1; 359 } 360 Class<? extends Savable> clazz = object.getClass(); 361 BinaryClassObject bco = classes.get(object.getClass().getName()); 362 // is this class been looked at before? in tagTable? 363 if (bco == null) { 364 bco = createClassObject(object.getClass()); 365 } 366 367 // is object in contentTable? 368 if (contentTable.get(object) != null) { 369 return (contentTable.get(object).getId()); 370 } 371 BinaryIdContentPair newPair = generateIdContentPair(bco); 372 BinaryIdContentPair old = contentTable.put(object, newPair); 373 if (old == null) { 374 contentKeys.add(object); 375 } 376 object.write(this); 377 newPair.getContent().finish(); 378 return newPair.getId(); 379 380 } 381 382 protected byte[] generateTag() { 383 int width = ((int) FastMath.log(aliasCount, 256) + 1); 384 int count = aliasCount; 385 aliasCount++; 386 byte[] bytes = new byte[width]; 387 for (int x = width - 1; x >= 0; x--) { 388 int pow = (int) FastMath.pow(256, x); 389 int factor = count / pow; 390 bytes[width - x - 1] = (byte) factor; 391 count %= pow; 392 } 393 return bytes; 394 } 395 396 protected BinaryIdContentPair generateIdContentPair(BinaryClassObject bco) { 397 BinaryIdContentPair pair = new BinaryIdContentPair(idCount++, 398 new BinaryOutputCapsule(this, bco)); 399 return pair; 400 } 401 }