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.asset.AssetInfo;
36 import com.jme3.asset.AssetManager;
37 import com.jme3.export.*;
38 import com.jme3.math.FastMath;
39 import java.io.*;
40 import java.net.URL;
41 import java.nio.ByteOrder;
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  * @author Joshua Slack
49  * @author Kirill Vainer - Version number, Fast buffer reading
50  */
51 public final class BinaryImporter implements JmeImporter {
52     private static final Logger logger = Logger.getLogger(BinaryImporter.class
53             .getName());
54 
55     private AssetManager assetManager;
56 
57     //Key - alias, object - bco
58     private HashMap<String, BinaryClassObject> classes
59              = new HashMap<String, BinaryClassObject>();
60     //Key - id, object - the savable
61     private HashMap<Integer, Savable> contentTable
62             = new HashMap<Integer, Savable>();
63     //Key - savable, object - capsule
64     private IdentityHashMap<Savable, BinaryInputCapsule> capsuleTable
65              = new IdentityHashMap<Savable, BinaryInputCapsule>();
66     //Key - id, opject - location in the file
67     private HashMap<Integer, Integer> locationTable
68              = new HashMap<Integer, Integer>();
69 
70     public static boolean debug = false;
71 
72     private byte[] dataArray;
73     private int aliasWidth;
74     private int formatVersion;
75 
76     private static final boolean fastRead = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN;
77 
BinaryImporter()78     public BinaryImporter() {
79     }
80 
getFormatVersion()81     public int getFormatVersion(){
82         return formatVersion;
83     }
84 
canUseFastBuffers()85     public static boolean canUseFastBuffers(){
86         return fastRead;
87     }
88 
getInstance()89     public static BinaryImporter getInstance() {
90         return new BinaryImporter();
91     }
92 
setAssetManager(AssetManager manager)93     public void setAssetManager(AssetManager manager){
94         this.assetManager = manager;
95     }
96 
getAssetManager()97     public AssetManager getAssetManager(){
98         return assetManager;
99     }
100 
load(AssetInfo info)101     public Object load(AssetInfo info){
102 //        if (!(info.getKey() instanceof ModelKey))
103 //            throw new IllegalArgumentException("Model assets must be loaded using a ModelKey");
104 
105         assetManager = info.getManager();
106 
107         InputStream is = null;
108         try {
109             is = info.openStream();
110             Savable s = load(is);
111 
112             return s;
113         } catch (IOException ex) {
114             logger.log(Level.SEVERE, "An error occured while loading jME binary object", ex);
115         } finally {
116             if (is != null){
117                 try {
118                     is.close();
119                 } catch (IOException ex) {}
120             }
121         }
122         return null;
123     }
124 
load(InputStream is)125     public Savable load(InputStream is) throws IOException {
126         return load(is, null, null);
127     }
128 
load(InputStream is, ReadListener listener)129     public Savable load(InputStream is, ReadListener listener) throws IOException {
130         return load(is, listener, null);
131     }
132 
load(InputStream is, ReadListener listener, ByteArrayOutputStream baos)133     public Savable load(InputStream is, ReadListener listener, ByteArrayOutputStream baos) throws IOException {
134         contentTable.clear();
135         BufferedInputStream bis = new BufferedInputStream(is);
136 
137         int numClasses;
138 
139         // Try to read signature
140         int maybeSignature = ByteUtils.readInt(bis);
141         if (maybeSignature == FormatVersion.SIGNATURE){
142             // this is a new version J3O file
143             formatVersion = ByteUtils.readInt(bis);
144             numClasses = ByteUtils.readInt(bis);
145 
146             // check if this binary is from the future
147             if (formatVersion > FormatVersion.VERSION){
148                 throw new IOException("The binary file is of newer version than expected! " +
149                                       formatVersion + " > " + FormatVersion.VERSION);
150             }
151         }else{
152             // this is an old version J3O file
153             // the signature was actually the class count
154             numClasses = maybeSignature;
155 
156             // 0 indicates version before we started adding
157             // version numbers
158             formatVersion = 0;
159         }
160 
161         int bytes = 4;
162         aliasWidth = ((int)FastMath.log(numClasses, 256) + 1);
163 
164         classes.clear();
165         for(int i = 0; i < numClasses; i++) {
166             String alias = readString(bis, aliasWidth);
167 
168             // jME3 NEW: Read class version number
169             int[] classHierarchyVersions;
170             if (formatVersion >= 1){
171                 int classHierarchySize = bis.read();
172                 classHierarchyVersions = new int[classHierarchySize];
173                 for (int j = 0; j < classHierarchySize; j++){
174                     classHierarchyVersions[j] = ByteUtils.readInt(bis);
175                 }
176             }else{
177                 classHierarchyVersions = new int[]{ 0 };
178             }
179 
180             // read classname and classname size
181             int classLength = ByteUtils.readInt(bis);
182             String className = readString(bis, classLength);
183 
184             BinaryClassObject bco = new BinaryClassObject();
185             bco.alias = alias.getBytes();
186             bco.className = className;
187             bco.classHierarchyVersions = classHierarchyVersions;
188 
189             int fields = ByteUtils.readInt(bis);
190             bytes += (8 + aliasWidth + classLength);
191 
192             bco.nameFields = new HashMap<String, BinaryClassField>(fields);
193             bco.aliasFields = new HashMap<Byte, BinaryClassField>(fields);
194             for (int x = 0; x < fields; x++) {
195                 byte fieldAlias = (byte)bis.read();
196                 byte fieldType = (byte)bis.read();
197 
198                 int fieldNameLength = ByteUtils.readInt(bis);
199                 String fieldName = readString(bis, fieldNameLength);
200                 BinaryClassField bcf = new BinaryClassField(fieldName, fieldAlias, fieldType);
201                 bco.nameFields.put(fieldName, bcf);
202                 bco.aliasFields.put(fieldAlias, bcf);
203                 bytes += (6 + fieldNameLength);
204             }
205             classes.put(alias, bco);
206         }
207         if (listener != null) listener.readBytes(bytes);
208 
209         int numLocs = ByteUtils.readInt(bis);
210         bytes = 4;
211 
212         capsuleTable.clear();
213         locationTable.clear();
214         for(int i = 0; i < numLocs; i++) {
215             int id = ByteUtils.readInt(bis);
216             int loc = ByteUtils.readInt(bis);
217             locationTable.put(id, loc);
218             bytes += 8;
219         }
220 
221         @SuppressWarnings("unused")
222         int numbIDs = ByteUtils.readInt(bis); // XXX: NOT CURRENTLY USED
223         int id = ByteUtils.readInt(bis);
224         bytes += 8;
225         if (listener != null) listener.readBytes(bytes);
226 
227         if (baos == null) {
228                 baos = new ByteArrayOutputStream(bytes);
229         } else {
230                 baos.reset();
231         }
232         int size = -1;
233         byte[] cache = new byte[4096];
234         while((size = bis.read(cache)) != -1) {
235             baos.write(cache, 0, size);
236             if (listener != null) listener.readBytes(size);
237         }
238         bis = null;
239 
240         dataArray = baos.toByteArray();
241         baos = null;
242 
243         Savable rVal = readObject(id);
244         if (debug) {
245             logger.info("Importer Stats: ");
246             logger.log(Level.INFO, "Tags: {0}", numClasses);
247             logger.log(Level.INFO, "Objects: {0}", numLocs);
248             logger.log(Level.INFO, "Data Size: {0}", dataArray.length);
249         }
250         dataArray = null;
251         return rVal;
252     }
253 
load(URL f)254     public Savable load(URL f) throws IOException {
255         return load(f, null);
256     }
257 
load(URL f, ReadListener listener)258     public Savable load(URL f, ReadListener listener) throws IOException {
259         InputStream is = f.openStream();
260         Savable rVal = load(is, listener);
261         is.close();
262         return rVal;
263     }
264 
load(File f)265     public Savable load(File f) throws IOException {
266         return load(f, null);
267     }
268 
load(File f, ReadListener listener)269     public Savable load(File f, ReadListener listener) throws IOException {
270         FileInputStream fis = new FileInputStream(f);
271         Savable rVal = load(fis, listener);
272         fis.close();
273         return rVal;
274     }
275 
load(byte[] data)276     public Savable load(byte[] data) throws IOException {
277         ByteArrayInputStream bais = new ByteArrayInputStream(data);
278         Savable rVal = load(bais);
279         bais.close();
280         return rVal;
281     }
282 
283     @Override
getCapsule(Savable id)284     public InputCapsule getCapsule(Savable id) {
285         return capsuleTable.get(id);
286     }
287 
readString(InputStream f, int length)288     protected String readString(InputStream f, int length) throws IOException {
289         byte[] data = new byte[length];
290         for(int j = 0; j < length; j++) {
291             data[j] = (byte)f.read();
292         }
293 
294         return new String(data);
295     }
296 
readString(int length, int offset)297     protected String readString(int length, int offset) throws IOException {
298         byte[] data = new byte[length];
299         for(int j = 0; j < length; j++) {
300             data[j] = dataArray[j+offset];
301         }
302 
303         return new String(data);
304     }
305 
readObject(int id)306     public Savable readObject(int id) {
307 
308         if(contentTable.get(id) != null) {
309             return contentTable.get(id);
310         }
311 
312         try {
313             int loc = locationTable.get(id);
314 
315             String alias = readString(aliasWidth, loc);
316             loc+=aliasWidth;
317 
318             BinaryClassObject bco = classes.get(alias);
319 
320             if(bco == null) {
321                 logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "NULL class object: " + alias);
322                 return null;
323             }
324 
325             int dataLength = ByteUtils.convertIntFromBytes(dataArray, loc);
326             loc+=4;
327 
328             Savable out = null;
329             if (assetManager != null) {
330                 out = SavableClassUtil.fromName(bco.className, assetManager.getClassLoaders());
331             } else {
332                 out = SavableClassUtil.fromName(bco.className);
333             }
334 
335             BinaryInputCapsule cap = new BinaryInputCapsule(this, out, bco);
336             cap.setContent(dataArray, loc, loc+dataLength);
337 
338             capsuleTable.put(out, cap);
339             contentTable.put(id, out);
340 
341             out.read(this);
342 
343             capsuleTable.remove(out);
344 
345             return out;
346 
347         } catch (IOException e) {
348             logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "Exception", e);
349             return null;
350         } catch (ClassNotFoundException e) {
351             logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "Exception", e);
352             return null;
353         } catch (InstantiationException e) {
354             logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "Exception", e);
355             return null;
356         } catch (IllegalAccessException e) {
357             logger.logp(Level.SEVERE, this.getClass().toString(), "readObject(int id)", "Exception", e);
358             return null;
359         }
360     }
361 }