• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
BinaryExporter()165     public BinaryExporter() {
166     }
167 
getInstance()168     public static BinaryExporter getInstance() {
169         return new BinaryExporter();
170     }
171 
save(Savable object, OutputStream os)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 
getChunk(BinaryIdContentPair pair)302     protected String getChunk(BinaryIdContentPair pair) {
303         return new String(pair.getContent().bytes, 0, Math.min(64, pair
304                 .getContent().bytes.length));
305     }
306 
findPrevMatch(BinaryIdContentPair oldPair, ArrayList<BinaryIdContentPair> bucket)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 
fixClassAlias(byte[] bytes, int width)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 
save(Savable object, File f)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 
getCapsule(Savable object)341     public BinaryOutputCapsule getCapsule(Savable object) {
342         return contentTable.get(object).getContent();
343     }
344 
createClassObject(Class clazz)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 
processBinarySavable(Savable object)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 
generateTag()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 
generateIdContentPair(BinaryClassObject bco)396     protected BinaryIdContentPair generateIdContentPair(BinaryClassObject bco) {
397         BinaryIdContentPair pair = new BinaryIdContentPair(idCount++,
398                 new BinaryOutputCapsule(this, bco));
399         return pair;
400     }
401 }