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.asset.AssetInfo;
36import com.jme3.asset.AssetManager;
37import com.jme3.export.*;
38import com.jme3.math.FastMath;
39import java.io.*;
40import java.net.URL;
41import java.nio.ByteOrder;
42import java.util.HashMap;
43import java.util.IdentityHashMap;
44import java.util.logging.Level;
45import java.util.logging.Logger;
46
47/**
48 * @author Joshua Slack
49 * @author Kirill Vainer - Version number, Fast buffer reading
50 */
51public 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
78    public BinaryImporter() {
79    }
80
81    public int getFormatVersion(){
82        return formatVersion;
83    }
84
85    public static boolean canUseFastBuffers(){
86        return fastRead;
87    }
88
89    public static BinaryImporter getInstance() {
90        return new BinaryImporter();
91    }
92
93    public void setAssetManager(AssetManager manager){
94        this.assetManager = manager;
95    }
96
97    public AssetManager getAssetManager(){
98        return assetManager;
99    }
100
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
125    public Savable load(InputStream is) throws IOException {
126        return load(is, null, null);
127    }
128
129    public Savable load(InputStream is, ReadListener listener) throws IOException {
130        return load(is, listener, null);
131    }
132
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
254    public Savable load(URL f) throws IOException {
255        return load(f, null);
256    }
257
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
265    public Savable load(File f) throws IOException {
266        return load(f, null);
267    }
268
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
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
284    public InputCapsule getCapsule(Savable id) {
285        return capsuleTable.get(id);
286    }
287
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
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
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}