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
33package com.jme3.export.binary;
34
35import com.jme3.export.FormatVersion;
36import com.jme3.export.JmeExporter;
37import com.jme3.export.Savable;
38import com.jme3.export.SavableClassUtil;
39import com.jme3.math.FastMath;
40import java.io.*;
41import java.util.ArrayList;
42import java.util.HashMap;
43import java.util.IdentityHashMap;
44import java.util.logging.Level;
45import 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
143public 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}