Home | History | Annotate | Download | only in binary
      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 }