Projekt

Obecné

Profil

Stáhnout (50.3 KB) Statistiky
| Větev: | Tag: | Revize:
1
package jdeserialize;
2

    
3
import java.io.*;
4
import java.util.*;
5
import java.util.regex.*;
6

    
7
/**
8
 * The main user-facing class for the jdeserialize library.  Also the implementation of
9
 * the command-line tool.<br/>
10
 * <br/>
11
 * Library:<br/>
12
 * <br/>
13
 * The jdeserialize class parses the stream (method run()).  From there, call the
14
 * getContent() method to get an itemized list of all items written to the stream, 
15
 * or getHandleMaps() to get a list of all handle->content maps generated during parsing.
16
 * The objects are generally instances that implement the interface "content"; see the
17
 * documentation of various implementors to get more information about the inner
18
 * representations.<br/>
19
 * <br/>
20
 * To enable debugging on stdout, use the enableDebug() or disableDebug() options.   <br/> 
21
 * <br/>
22
 * <br/>
23
 * Command-line tool:   <br/>
24
 * <br/>
25
 * The tool reads in a set of files and generates configurable output on stdout.  The
26
 * primary output consists of three separate stages.  The first stage is  a textual 
27
 * description of every piece of content in the stream, in the order it was written.
28
 * There is generally a one-to-one mapping between ObjectOutputStream.writeXXX() calls and
29
 * items printed in the first stage.  The first stage may be suppressed with the
30
 * -nocontent command-line option. <br/>
31
 * <br/>
32
 * The second stage is a list of every class declaration serialized in the file.  These
33
 * are formatted as normal Java language class declarations.  Several options are
34
 * available to govern this stage, including -filter, -showarrays, -noclasses, and
35
 * -fixnames. <br/>
36
 * <br/>
37
 * The third stage is a dump of every instance embedded inside the stream, including
38
 * textual descriptions of field values.  This is useful for casual viewing of class data. 
39
 * To suppress this stage, use -noinstances. <br/>
40
 * <br/>
41
 * You can also get debugging information generated during the parse phase by supplying
42
 * -debug.
43
 * <br/>
44
 * The data from block data objects can be extracted with the -blockdata <file> option.
45
 * Additionally, a manifest describing the size of each individual block can be generated
46
 * with the -blockdatamanifest <file> option.
47
 * <br/>
48
 * References: <br/>
49
 *     - Java Object Serialization Specification ch. 6 (Object Serialization Stream
50
 *       Protocol): <br/>
51
 *       http://download.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html <br/>
52
 *     - "Modified UTF-8 Strings" within the JNI specification: 
53
 *       http://download.oracle.com/javase/1.5.0/docs/guide/jni/spec/types.html#wp16542 <br/>
54
 *     - "Inner Classes Specification" within the JDK 1.1.8 docs:
55
 *       http://java.sun.com/products/archive/jdk/1.1/ <br/>
56
 *     - "Java Language Specification", third edition, particularly section 3:
57
 *       http://java.sun.com/docs/books/jls/third_edition/html/j3TOC.html <br/>
58
 *
59
 * @see content
60
 */
61
public class jdeserialize {
62
    public static final long serialVersionUID = 78790714646095L;
63
    public static final String INDENT = "    ";
64
    public static final int CODEWIDTH = 90;
65
    public static final String linesep = System.getProperty("line.separator");
66
    public static final String[] keywords = new String[] {
67
        "abstract", "continue", "for", "new", "switch", "assert", "default", "if",
68
        "package", "synchronized", "boolean", "do", "goto", "private", "this",
69
        "break", "double", "implements", "protected", "throw", "byte", "else",
70
        "import", "public", "throws", "case", "enum", "instanceof", "return",
71
        "transient", "catch", "extends", "int", "short", "try", "char", "final",
72
        "interface", "static", "void", "class", "finally", "long", "strictfp",
73
        "volatile", "const", "float", "native", "super", "while" }; 
74
    public static HashSet<String> keywordSet;
75

    
76
    private String filename;
77
    private HashMap<Integer,content> handles = new HashMap<Integer,content>();
78
    private ArrayList<Map<Integer,content>> handlemaps = new ArrayList<Map<Integer,content>>();
79
    private ArrayList<content> content;
80
    private int curhandle;
81
    private boolean debugEnabled;
82

    
83
    static {
84
        keywordSet = new HashSet<String>();
85
        for(String kw: keywords) {
86
            keywordSet.add(kw);
87
        }
88
    }
89

    
90
    /**
91
     * <p>
92
     * Retrieves the list of content objects that were written to the stream.  Each item
93
     * generally corresponds to an invocation of an ObjectOutputStream writeXXX() method.
94
     * A notable exception is the class exceptionstate, which represents an embedded
95
     * exception that was caught during serialization.
96
     * </p>
97
     *
98
     * <p>
99
     * See the various implementors of content to get information about what data is
100
     * available.  
101
     * </p>
102
     *
103
     * <p>
104
     * Entries in the list may be null, because it's perfectly legitimate to write a null
105
     * reference to the stream.  
106
     * </p>
107
     *
108
     * @return a list of content objects
109
     * @see content
110
     * @see exceptionstate
111
     */
112
    public List<content> getContent() {
113
        return content;
114
    }
115

    
116
    /**
117
     * <p>
118
     * Return a list of Maps containing every object with a handle.  The keys are integers
119
     * -- the handles themselves -- and the values are instances of type content.
120
     * </p>
121
     *
122
     * <p>
123
     * Although there is only one map active at a given point, a stream may have multiple
124
     * logical maps: when a reset happens (indicated by TC_RESET), the current map is
125
     * cleared.  
126
     * </p>
127
     *
128
     * <p>
129
     * See the spec for details on handles.
130
     * </p>
131
     * @return a list of <Integer,content> maps
132
     */
133
    public List<Map<Integer,content>> getHandleMaps() {
134
        return handlemaps;
135
    }
136

    
137
    /**
138
     * Suitably escapes non-printable-ASCII characters (and doublequotes) for use 
139
     * in a Java string literal.
140
     *
141
     * @param str string to escape
142
     * @return an escaped version of the string
143
     */
144
    public static String unicodeEscape(String str) {
145
        StringBuffer sb = new StringBuffer();
146
        int cplen = str.codePointCount(0, str.length());
147
        for(int i = 0; i < cplen; i++) {
148
            int cp = str.codePointAt(i);
149
            if(cp == '"') {
150
                sb.append("\\\"");
151
            }
152
            if(cp < 0x20 || cp > 0x7f) {
153
                sb.append("\\u" + hexnoprefix(4));
154
            } else {
155
                sb.appendCodePoint(cp);
156
            }
157
        }
158
        return sb.toString();
159
    }
160

    
161
    public static String indent(int level) {
162
        StringBuffer sb = new StringBuffer("");
163
        for(int i = 0; i < level; i++) {
164
            sb.append(INDENT);
165
        }
166
        return sb.toString();
167
    }
168

    
169
    public void read_Classdata(DataInputStream dis, instance inst) throws IOException {
170
        ArrayList<classdesc> classes = new ArrayList<classdesc>();
171
        inst.classdesc.getHierarchy(classes);
172
        Map<classdesc, Map<field, Object>> alldata = new HashMap<classdesc, Map<field, Object>>();
173
        Map<classdesc, List<content>> ann = new HashMap<classdesc, List<content>>();
174
        for(classdesc cd: classes) {
175
            Map<field, Object> values = new HashMap<field, Object>();
176
            if((cd.descflags & ObjectStreamConstants.SC_SERIALIZABLE) != 0) {
177
                if((cd.descflags & ObjectStreamConstants.SC_EXTERNALIZABLE) != 0) {
178
                    throw new IOException("SC_EXTERNALIZABLE & SC_SERIALIZABLE encountered");
179
                }
180
                for(field f: cd.fields) {
181
                    Object o = read_FieldValue(f.type, dis);
182
                    values.put(f, o);
183
                }
184
                alldata.put(cd, values);
185
                if((cd.descflags & ObjectStreamConstants.SC_WRITE_METHOD) != 0) {
186
                    if((cd.descflags & ObjectStreamConstants.SC_ENUM) != 0) {
187
                        throw new IOException("SC_ENUM & SC_WRITE_METHOD encountered!");
188
                    }
189
                    ann.put(cd, read_classAnnotation(dis));
190
                }
191
            } else if((cd.descflags & ObjectStreamConstants.SC_EXTERNALIZABLE) != 0) {
192
                if((cd.descflags & ObjectStreamConstants.SC_SERIALIZABLE) != 0) {
193
                    throw new IOException("SC_SERIALIZABLE & SC_EXTERNALIZABLE encountered");
194
                }
195
                if((cd.descflags & ObjectStreamConstants.SC_BLOCK_DATA) != 0) {
196
                    throw new EOFException("hit externalizable with nonzero SC_BLOCK_DATA; can't interpret data");
197
                } else {
198
                    ann.put(cd, read_classAnnotation(dis));
199
                }
200
            }
201
        }
202
        inst.annotations = ann;
203
        inst.fielddata = alldata;
204
    }
205

    
206
    public Object read_FieldValue(fieldtype f, DataInputStream dis) throws IOException {
207
        switch(f) {
208
            case BYTE:
209
                return Byte.valueOf(dis.readByte());
210
            case CHAR:
211
                return Character.valueOf(dis.readChar());
212
            case DOUBLE:
213
                return Double.valueOf(dis.readDouble());
214
            case FLOAT:
215
                return Float.valueOf(dis.readFloat());
216
            case INTEGER:
217
                return Integer.valueOf(dis.readInt());
218
            case LONG:
219
                return Long.valueOf(dis.readLong());
220
            case SHORT:
221
                return Short.valueOf(dis.readShort());
222
            case BOOLEAN:
223
                return Boolean.valueOf(dis.readBoolean());
224
            case OBJECT:
225
            case ARRAY:
226
                byte stc = dis.readByte();
227
                if(f == fieldtype.ARRAY && stc != ObjectStreamConstants.TC_ARRAY) {
228
                    throw new IOException("array type listed, but typecode is not TC_ARRAY: " + hex(stc));
229
                }
230
                content c = read_Content(stc, dis, false);
231
                if(c != null && c.isExceptionObject()) {
232
                    throw new ExceptionReadException(c);
233
                }
234
                return c;
235
            default:
236
                throw new IOException("can't process type: " + f.toString());
237
        }
238
    }
239
    public jdeserialize(String filename) {
240
        this.filename = filename;
241
    }
242
    private int newHandle() {
243
        return curhandle++;
244
    }
245

    
246
    public static String resolveJavaType(fieldtype type, String classname, boolean convertSlashes, boolean fixname)  throws IOException {
247
        if(type == fieldtype.ARRAY) {
248
            StringBuffer asb = new StringBuffer("");
249
            for(int i = 0; i < classname.length(); i++) {
250
                char ch = classname.charAt(i);
251
                switch(ch) {
252
                    case '[':
253
                        asb.append("[]");
254
                        continue;
255
                    case 'L':
256
                        String cn = decodeClassName(classname.substring(i), convertSlashes);
257
                        if(fixname) {
258
                            cn = fixClassName(cn);
259
                        }
260
                        return cn + asb.toString();
261
                    default:
262
                        if(ch < 1 || ch > 127) {
263
                            throw new ValidityException("invalid array field type descriptor character: " + classname);
264
                        }
265
                        fieldtype ft = fieldtype.get((byte)ch);
266
                        if(i != (classname.length()-1)) {
267
                            throw new ValidityException("array field type descriptor is too long: " + classname);
268
                        }
269
                        String ftn = ft.getJavaType();
270
                        if(fixname) {
271
                            ftn = fixClassName(ftn);
272
                        }
273
                        return ftn + asb.toString();
274
                }
275
            }
276
            throw new ValidityException("array field type descriptor is too short: " + classname);
277
        } else if(type == fieldtype.OBJECT) {
278
            return decodeClassName(classname, convertSlashes);
279
        } else {
280
            return type.getJavaType();
281
        }
282
    }
283

    
284
    public List<content> read_classAnnotation(DataInputStream dis) throws IOException {
285
        List<content> list = new ArrayList<content>();
286
        while(true) {
287
            byte tc = dis.readByte();
288
            if(tc == ObjectStreamConstants.TC_ENDBLOCKDATA) {
289
                return list;
290
            }
291
            if(tc == ObjectStreamConstants.TC_RESET) {
292
                reset();
293
                continue;
294
            }
295
            content c = read_Content(tc, dis, true);
296
            if(c != null && c.isExceptionObject()) {
297
                throw new ExceptionReadException(c);
298
            }
299
            list.add(c);
300
        }
301
    }
302
    public static void dump_Instance(int indentlevel, instance inst, PrintStream ps) {
303
        StringBuffer sb = new StringBuffer();
304
        sb.append("[instance " + hex(inst.handle) + ": " + hex(inst.classdesc.handle) + "/" + inst.classdesc.name);
305
        if(inst.annotations != null && inst.annotations.size() > 0) {
306
            sb.append(linesep).append("  object annotations:").append(linesep);
307
            for(classdesc cd: inst.annotations.keySet()) {
308
                sb.append("    ").append(cd.name).append(linesep);
309
                for(content c: inst.annotations.get(cd)) {
310
                    sb.append("        ").append(c.toString()).append(linesep);
311
                }
312
            }
313
        }
314
        if(inst.fielddata != null && inst.fielddata.size() > 0) {
315
            sb.append(linesep).append("  field data:").append(linesep);
316
            for(classdesc cd: inst.fielddata.keySet()) {
317
                sb.append("    ").append(hex(cd.handle)).append("/").append(cd.name).append(":").append(linesep);
318
                for(field f: inst.fielddata.get(cd).keySet()) {
319
                    Object o = inst.fielddata.get(cd).get(f);
320
                    sb.append("        ").append(f.name).append(": ");
321
                    if(o instanceof content) {
322
                        content c = (content)o;
323
                        int h = c.getHandle();
324
                        if(h == inst.handle) {
325
                            sb.append("this");
326
                        } else {
327
                            sb.append("r" + hex(h));
328
                        }
329
                        sb.append(": ").append(c.toString());
330
                        sb.append(linesep);
331
                    } else {
332
                        sb.append("" + o).append(linesep);
333
                    }
334
                }
335
            }
336
        }
337
        sb.append("]");
338
        ps.println(sb);
339
    }
340

    
341
    /**
342
     * "Fix" the given name by transforming illegal characters, such that the end result
343
     * is a legal Java identifier that is not a keyword.  
344
     * If the string is modified at all, the result will be prepended with "$__".
345
     *
346
     * @param name the name to be transformed
347
     * @return the unmodified string if it is legal, otherwise a legal-identifier version
348
     */
349
    public static String fixClassName(String name) {
350
        if(name == null) {
351
            return "$__null";
352
        }
353
        if(keywordSet.contains(name)) {
354
            return "$__" + name;
355
        }
356
        StringBuffer sb = new StringBuffer();
357
        int cplen = name.codePointCount(0, name.length());
358
        if(cplen < 1) {
359
            return "$__zerolen";
360
        }
361
        boolean modified = false;
362
        int scp = name.codePointAt(0);
363
        if(!Character.isJavaIdentifierStart(scp)) {
364
            modified = true;
365
            if(!Character.isJavaIdentifierPart(scp) || Character.isIdentifierIgnorable(scp)) {
366
                sb.append("x");
367
            } else {
368
                sb.appendCodePoint(scp);
369
            }
370
        } else {
371
            sb.appendCodePoint(scp);
372
        }
373

    
374
        for(int i = 1; i < cplen; i++) {
375
            int cp = name.codePointAt(i);
376
            if(!Character.isJavaIdentifierPart(cp) || Character.isIdentifierIgnorable(cp)) {
377
                modified = true;
378
                sb.append("x");
379
            } else {
380
                sb.appendCodePoint(cp);
381
            }
382
        }
383
        if(modified) {
384
            return "$__" + sb.toString();
385
        } else {
386
            return name;
387
        }
388
    }
389

    
390
    public static void dump_ClassDesc(int indentlevel, classdesc cd, PrintStream ps, boolean fixname) throws IOException {
391
        String classname = cd.name;
392
        if(fixname) {
393
            classname = fixClassName(classname);
394
        }
395
        if(cd.annotations != null && cd.annotations.size() > 0) {
396
            ps.println(indent(indentlevel) + "// annotations: ");
397
            for(content c: cd.annotations) {
398
                ps.print(indent(indentlevel) + "// " + indent(1));
399
                ps.println(c.toString());
400
            }
401
        }
402
        if(cd.classtype == classdesctype.NORMALCLASS) {
403
            if((cd.descflags & ObjectStreamConstants.SC_ENUM) != 0) {
404
                ps.print(indent(indentlevel) + "enum " + classname + " {");
405
                boolean shouldindent = true;
406
                int len = indent(indentlevel+1).length();
407
                for(String econst: cd.enumconstants) {
408
                    if(shouldindent) {
409
                        ps.println("");
410
                        ps.print(indent(indentlevel+1));
411
                        shouldindent = false;
412
                    }
413
                    len += econst.length();
414
                    ps.print(econst + ", ");
415
                    if(len >= CODEWIDTH) {
416
                        len = indent(indentlevel+1).length();
417
                        shouldindent = true;
418
                    }
419
                }
420
                ps.println("");
421
                ps.println(indent(indentlevel) + "}");
422
                return;
423
            } 
424
            ps.print(indent(indentlevel));
425
            if(cd.isStaticMemberClass()) {
426
                ps.print("static ");
427
            }
428
            ps.print("class " + (classname.charAt(0) == '[' ? resolveJavaType(fieldtype.ARRAY, cd.name, false, fixname) : classname));
429
            if(cd.superclass != null) {
430
                ps.print(" extends " + cd.superclass.name);
431
            }
432
            ps.print(" implements ");
433
            if((cd.descflags & ObjectStreamConstants.SC_EXTERNALIZABLE) != 0) {
434
                ps.print("java.io.Externalizable");
435
            } else {
436
                ps.print("java.io.Serializable");
437
            }
438
            if(cd.interfaces != null) {
439
                for(String intf: cd.interfaces) {
440
                    ps.print(", " + intf);
441
                }
442
            }
443
            ps.println(" {");
444
            for(field f: cd.fields) {
445
                if(f.isInnerClassReference()) {
446
                    continue;
447
                }
448
                ps.print(indent(indentlevel+1) + f.getJavaType());
449
                ps.println(" " + f.name + ";");
450
            }
451
            for(classdesc icd: cd.innerclasses) {
452
                dump_ClassDesc(indentlevel+1, icd, ps, fixname);
453
            }
454
            ps.println(indent(indentlevel)+"}");
455
        } else if(cd.classtype == classdesctype.PROXYCLASS) {
456
            ps.print(indent(indentlevel) + "// proxy class " + hex(cd.handle));
457
            if(cd.superclass != null) {
458
                ps.print(" extends " + cd.superclass.name);
459
            }
460
            ps.println(" implements ");
461
            for(String intf: cd.interfaces) {
462
                ps.println(indent(indentlevel) + "//    " + intf + ", ");
463
            }
464
            if((cd.descflags & ObjectStreamConstants.SC_EXTERNALIZABLE) != 0) {
465
                ps.println(indent(indentlevel) + "//    java.io.Externalizable");
466
            } else {
467
                ps.println(indent(indentlevel) + "//    java.io.Serializable");
468
            }
469
        } else {
470
            throw new ValidityException("encountered invalid classdesc type!");
471
        }
472
    }
473

    
474
    public void setHandle(int handle, content c) throws IOException {
475
        if(handles.containsKey(handle)) {
476
            throw new IOException("trying to reset handle " + hex(handle));
477
        }
478
        handles.put(handle, c);
479
    }
480
    public void reset() {
481
        debug("reset ordered!");
482
        if(handles != null && handles.size() > 0) {
483
            HashMap<Integer,content> hm = new HashMap<Integer,content>();
484
            hm.putAll(handles);
485
            handlemaps.add(hm);
486
        }
487
        handles.clear();
488
        curhandle = ObjectStreamConstants.baseWireHandle;  // 0x7e0000
489
    }
490
    /**
491
     * Read the content of a thrown exception object.  According to the spec, this must be
492
     * an object of type Throwable.  Although the Sun JDK always appears to provide enough
493
     * information about the hierarchy to reach all the way back to java.lang.Throwable,
494
     * it's unclear whether this is actually a requirement.  From my reading, it's
495
     * possible that some other ObjectOutputStream implementations may leave some gaps in
496
     * the hierarchy, forcing this app to hit the classloader.  To avoid this, we merely
497
     * ensure that the written object is indeed an instance; ensuring that the object is
498
     * indeed a Throwable is an exercise left to the user.
499
     */
500
    public content read_Exception(DataInputStream dis) throws IOException {
501
        reset();
502
        byte tc = dis.readByte();
503
        if(tc == ObjectStreamConstants.TC_RESET) {
504
            throw new ValidityException("TC_RESET for object while reading exception: what should we do?");
505
        }
506
        content c = read_Content(tc, dis, false);
507
        if(c == null) {
508
            throw new ValidityException("stream signaled for an exception, but exception object was null!");
509
        }
510
        if(!(c instanceof instance)) { 
511
            throw new ValidityException("stream signaled for an exception, but content is not an object!");
512
        }
513
        if(c.isExceptionObject()) {
514
            throw new ExceptionReadException(c);
515
        }
516
        c.setIsExceptionObject(true);
517
        reset();
518
        return c;
519
    }
520

    
521
    public classdesc read_classDesc(DataInputStream dis) throws IOException {
522
        byte tc = dis.readByte();
523
        classdesc cd = handle_classDesc(tc, dis, false);
524
        return cd;
525
    }
526
    public classdesc read_newClassDesc(DataInputStream dis) throws IOException {
527
        byte tc = dis.readByte();
528
        classdesc cd = handle_newClassDesc(tc, dis);
529
        return cd;
530
    }
531
    public content read_prevObject(DataInputStream dis) throws IOException {
532
            int handle = dis.readInt();
533
            if(!handles.containsKey(Integer.valueOf(handle))) {
534
                throw new ValidityException("can't find an entry for handle " + hex(handle));
535
            }
536
            content c = handles.get(handle);
537
            debug("prevObject: handle " + hex(c.getHandle()) + " classdesc " + c.toString());
538
            return c;
539
    }
540

    
541
    public classdesc handle_newClassDesc(byte tc, DataInputStream dis) throws IOException {
542
        return handle_classDesc(tc, dis, true);
543
    }
544
    public classdesc handle_classDesc(byte tc, DataInputStream dis, boolean mustBeNew) throws IOException {
545
        if(tc == ObjectStreamConstants.TC_CLASSDESC) {
546
            String name = dis.readUTF();
547
            long serialVersionUID = dis.readLong();
548
            int handle = newHandle();
549
            byte descflags = dis.readByte();
550
            short nfields = dis.readShort();
551
            if(nfields < 0) {
552
                throw new IOException("invalid field count: " + nfields);
553
            }
554
            field[] fields = new field[nfields];
555
            for(short s = 0; s < nfields; s++) {
556
                byte ftype = dis.readByte();
557
                if(ftype == 'B' || ftype == 'C' || ftype == 'D' 
558
                        || ftype == 'F' || ftype == 'I' || ftype == 'J'
559
                        || ftype == 'S' || ftype == 'Z') {
560
                    String fieldname = dis.readUTF();
561
                    fields[s] = new field(fieldtype.get(ftype), fieldname);
562
                } else if(ftype == '[' || ftype == 'L') {
563
                    String fieldname = dis.readUTF();
564
                    byte stc = dis.readByte();
565
                    stringobj classname = read_newString(stc, dis);
566
                    //String classname = dis.readUTF();
567
                    fields[s] = new field(fieldtype.get(ftype), fieldname, classname);
568
                } else {
569
                    throw new IOException("invalid field type char: " + hex(ftype));
570
                }
571
            }
572
            classdesc cd = new classdesc(classdesctype.NORMALCLASS);
573
            cd.name = name;
574
            cd.serialVersionUID = serialVersionUID;
575
            cd.handle = handle;
576
            cd.descflags = descflags;
577
            cd.fields = fields;
578
            cd.annotations = read_classAnnotation(dis);
579
            cd.superclass = read_classDesc(dis);
580
            setHandle(handle, cd);
581
            debug("read new classdesc: handle " + hex(handle) + " name " + name);
582
            return cd;
583
        } else if(tc == ObjectStreamConstants.TC_NULL) {
584
            if(mustBeNew) {
585
                throw new ValidityException("expected new class description -- got null!");
586
            }
587
            debug("read null classdesc");
588
            return null;
589
        } else if(tc == ObjectStreamConstants.TC_REFERENCE) {
590
            if(mustBeNew) {
591
                throw new ValidityException("expected new class description -- got a reference!");
592
            }
593
            content c = read_prevObject(dis);
594
            if(!(c instanceof classdesc)) {
595
                throw new IOException("referenced object not a class description!");
596
            }
597
            classdesc cd = (classdesc)c;
598
            return cd;
599
        } else if(tc == ObjectStreamConstants.TC_PROXYCLASSDESC) {
600
            int handle = newHandle();
601
            int icount = dis.readInt();
602
            if(icount < 0) {
603
                throw new IOException("invalid proxy interface count: " + hex(icount));
604
            }
605
            String interfaces[] = new String[icount];
606
            for(int i = 0; i < icount; i++) {
607
                interfaces[i] = dis.readUTF();
608
            }
609
            classdesc cd = new classdesc(classdesctype.PROXYCLASS);
610
            cd.handle = handle;
611
            cd.interfaces = interfaces;
612
            cd.annotations = read_classAnnotation(dis);
613
            cd.superclass = read_classDesc(dis);
614
            setHandle(handle, cd);
615
            cd.name = "(proxy class; no name)";
616
            debug("read new proxy classdesc: handle " + hex(handle) + " names [" + Arrays.toString(interfaces) + "]");
617
            return cd;
618
        } else {
619
            throw new ValidityException("expected a valid class description starter got " + hex(tc));
620
        }
621
    }
622
    public arrayobj read_newArray(DataInputStream dis) throws IOException {
623
        classdesc cd = read_classDesc(dis);
624
        int handle = newHandle();
625
        debug("reading new array: handle " + hex(handle) + " classdesc " + cd.toString());
626
        if(cd.name.length() < 2) {
627
            throw new IOException("invalid name in array classdesc: " + cd.name);
628
        }
629
        arraycoll ac = read_arrayValues(cd.name.substring(1), dis);
630
        return new arrayobj(handle, cd, ac);
631
    }
632
    public arraycoll read_arrayValues(String str, DataInputStream dis) throws IOException {
633
        byte b = str.getBytes("UTF-8")[0];
634
        fieldtype ft = fieldtype.get(b);
635
        int size = dis.readInt();
636
        if(size < 0) {
637
            throw new IOException("invalid array size: " + size);
638
        }
639

    
640
        arraycoll ac = new arraycoll(ft);
641
        for(int i = 0; i < size; i++) {
642
            ac.add(read_FieldValue(ft, dis));
643
            continue;
644
        }
645
        return ac;
646
    }
647
    public classobj read_newClass(DataInputStream dis) throws IOException {
648
        classdesc cd = read_classDesc(dis);
649
        int handle = newHandle();
650
        debug("reading new class: handle " + hex(handle) + " classdesc " + cd.toString());
651
        classobj c = new classobj(handle, cd);
652
        setHandle(handle, c);
653
        return c;
654
    }
655
    public enumobj read_newEnum(DataInputStream dis) throws IOException {
656
        classdesc cd = read_classDesc(dis);
657
        if(cd == null) {
658
            throw new IOException("enum classdesc can't be null!");
659
        }
660
        int handle = newHandle();
661
        debug("reading new enum: handle " + hex(handle) + " classdesc " + cd.toString());
662
        byte tc = dis.readByte();
663
        stringobj so = read_newString(tc, dis);
664
        cd.addEnum(so.value);
665
        setHandle(handle, so);
666
        return new enumobj(handle, cd, so);
667
    }
668
    public stringobj read_newString(byte tc, DataInputStream dis) throws IOException {
669
        byte[] data;
670
        if(tc == ObjectStreamConstants.TC_REFERENCE) {
671
                content c = read_prevObject(dis);
672
                if(!(c instanceof stringobj)) {
673
                    throw new IOException("got reference for a string, but referenced value was something else!");
674
                }
675
                return (stringobj)c;
676
        }
677
        int handle = newHandle();
678
        if(tc == ObjectStreamConstants.TC_STRING) {
679
            int len = dis.readUnsignedShort();
680
            data = new byte[len];
681
        } else if(tc == ObjectStreamConstants.TC_LONGSTRING) {
682
            long len = dis.readLong();
683
            if(len < 0) {
684
                throw new IOException("invalid long string length: " + len);
685
            }
686
            if(len > 2147483647) {
687
                throw new IOException("long string is too long: " + len);
688
            }
689
            if(len < 65536) {
690
                debugerr("warning: small string length encoded as TC_LONGSTRING: " + len);
691
            }
692
            data = new byte[(int)len];
693
        } else if(tc == ObjectStreamConstants.TC_NULL) {
694
            throw new ValidityException("stream signaled TC_NULL when string type expected!");
695
        } else {
696
            throw new IOException("invalid tc byte in string: " + hex(tc));
697
        }
698
        dis.readFully(data);
699
        debug("reading new string: handle " + hex(handle) + " bufsz " + data.length);
700
        stringobj sobj = new stringobj(handle, data);
701
        setHandle(handle, sobj);
702
        return sobj;
703
    }
704
    public blockdata read_blockdata(byte tc, DataInputStream dis) throws IOException {
705
        int size;
706
        if(tc == ObjectStreamConstants.TC_BLOCKDATA) {
707
            size = dis.readUnsignedByte();
708
        } else if(tc == ObjectStreamConstants.TC_BLOCKDATALONG) {
709
            size = dis.readInt();
710
        } else {
711
            throw new IOException("invalid tc value for blockdata: " + hex(tc));
712
        }
713
        if(size < 0) {
714
            throw new IOException("invalid value for blockdata size: " + size);
715
        }
716
        byte[] b = new byte[size];
717
        dis.readFully(b);
718
        debug("read blockdata of size " + size);
719
        return new blockdata(b);
720
    }
721
    public instance read_newObject(DataInputStream dis) throws IOException {
722
        classdesc cd = read_classDesc(dis);
723
        int handle = newHandle();
724
        debug("reading new object: handle " + hex(handle) + " classdesc " + cd.toString());
725
        instance i = new instance();
726
        i.classdesc = cd;
727
        i.handle = handle;
728
        setHandle(handle, i);
729
        read_Classdata(dis, i);
730
        debug("done reading object for handle " + hex(handle));
731
        return i;
732
    }
733

    
734
    /**
735
     * <p>
736
     * Read the next object corresponding to the spec grammar rule "content", and return
737
     * an object of type content.
738
     * </p>
739
     *
740
     * <p>
741
     * Usually, there is a 1:1 mapping of content items and returned instances.  The
742
     * one case where this isn't true is when an exception is embedded inside another
743
     * object.  When this is encountered, only the serialized exception object is
744
     * returned; it's up to the caller to backtrack in order to gather any data from the
745
     * object that was being serialized when the exception was thrown.
746
     * </p>
747
     *
748
     * @param tc the last byte read from the stream; it must be one of the TC_* values
749
     * within ObjectStreamConstants.*
750
     * @param dis the DataInputStream to read from
751
     * @param blockdata whether or not to read TC_BLOCKDATA (this is the difference
752
     * between spec rules "object" and "content").
753
     * @return an object representing the last read item from the stream 
754
     * @throws IOException when a validity or I/O error occurs while reading
755
     */
756
    public content read_Content(byte tc, DataInputStream dis, boolean blockdata) throws IOException {
757
        try {
758
            switch(tc) {
759
                case ObjectStreamConstants.TC_OBJECT:
760
                    return read_newObject(dis);
761
                case ObjectStreamConstants.TC_CLASS:
762
                    return read_newClass(dis);
763
                case ObjectStreamConstants.TC_ARRAY:
764
                    return read_newArray(dis);
765
                case ObjectStreamConstants.TC_STRING:
766
                case ObjectStreamConstants.TC_LONGSTRING:
767
                    return read_newString(tc, dis);
768
                case ObjectStreamConstants.TC_ENUM:
769
                    return read_newEnum(dis);
770
                case ObjectStreamConstants.TC_CLASSDESC:
771
                case ObjectStreamConstants.TC_PROXYCLASSDESC:
772
                    return handle_newClassDesc(tc, dis);
773
                case ObjectStreamConstants.TC_REFERENCE:
774
                    return read_prevObject(dis);
775
                case ObjectStreamConstants.TC_NULL:
776
                    return null;
777
                case ObjectStreamConstants.TC_EXCEPTION:
778
                    return read_Exception(dis);
779
                case ObjectStreamConstants.TC_BLOCKDATA:
780
                case ObjectStreamConstants.TC_BLOCKDATALONG:
781
                    if(blockdata == false) {
782
                        throw new IOException("got a blockdata TC_*, but not allowed here: " + hex(tc));
783
                    }
784
                    return read_blockdata(tc, dis);
785
                default:
786
                    throw new IOException("unknown content tc byte in stream: " + hex(tc));
787
            }
788
        } catch (ExceptionReadException ere) {
789
            return ere.getExceptionObject();
790
        }
791
    }
792

    
793
    /**
794
     * <p>
795
     * Reads in an entire ObjectOutputStream output on the given stream, filing 
796
     * this object's content and handle maps with data about the objects in the stream.  
797
     * </p>
798
     *
799
     * <p>
800
     * If shouldConnect is true, then jdeserialize will attempt to identify member classes
801
     * by their names according to the details laid out in the Inner Classes
802
     * Specification.  If it finds one, it will set the classdesc's flag indicating that
803
     * it is an member class, and it will create a reference in its enclosing class.
804
     * </p>
805
     *
806
     * @param is an open InputStream on a serialized stream of data
807
     * @param shouldConnect true if jdeserialize should attempt to identify and connect
808
     * member classes with their enclosing classes
809
     * 
810
     * Also see the <pre>connectMemberClasses</pre> method for more information on the 
811
     * member-class-detection algorithm.
812
     */
813
    public void run(InputStream is, boolean shouldConnect) throws IOException {
814
        LoggerInputStream lis = null;
815
        DataInputStream dis = null;
816
        try {
817
            lis = new LoggerInputStream(is);
818
            dis = new DataInputStream(lis);
819

    
820
            short magic = dis.readShort();
821
            if(magic != ObjectStreamConstants.STREAM_MAGIC) {
822
                throw new ValidityException("file magic mismatch!  expected " + ObjectStreamConstants.STREAM_MAGIC + ", got " + magic);
823
            }
824
            short streamversion = dis.readShort();
825
            if(streamversion != ObjectStreamConstants.STREAM_VERSION) {
826
                throw new ValidityException("file version mismatch!  expected " + ObjectStreamConstants.STREAM_VERSION + ", got " + streamversion);
827
            }
828
            reset();
829
            content = new ArrayList<content>();
830
            while(true) {
831
                byte tc;
832
                try { 
833
                    lis.record();
834
                    tc = dis.readByte();
835
                    if(tc == ObjectStreamConstants.TC_RESET) {
836
                        reset();
837
                        continue;
838
                    }
839
                } catch (EOFException eoe) {
840
                    break;
841
                }
842
                content c = read_Content(tc, dis, true);
843
                System.out.println("read: " + c.toString());
844
                if(c != null && c.isExceptionObject()) {
845
                    c = new exceptionstate(c, lis.getRecordedData());
846
                }
847
                content.add(c);
848
            }
849
        } finally {
850
            if(dis != null) {
851
                try {
852
                    dis.close();
853
                } catch (Exception ignore) { }
854
            }
855
            if(lis != null) {
856
                try {
857
                    lis.close();
858
                } catch (Exception ignore) {}
859
            }
860
        }
861
        for(content c: handles.values()) {
862
            c.validate();
863
        }
864
        if(shouldConnect) {
865
            connectMemberClasses();
866
            for(content c: handles.values()) {
867
                c.validate();
868
            }
869
        }
870
        if(handles != null && handles.size() > 0) {
871
            HashMap<Integer,content> hm = new HashMap<Integer,content>();
872
            hm.putAll(handles);
873
            handlemaps.add(hm);
874
        }
875
    }
876
    public void dump(Getopt go) throws IOException {
877
        if(go.hasOption("-blockdata") || go.hasOption("-blockdatamanifest")) {
878
            List<String> bout = go.getArguments("-blockdata");
879
            List<String> mout = go.getArguments("-blockdatamanifest");
880
            FileOutputStream bos = null, mos = null;
881
            PrintWriter pw = null;
882
            try {
883
                if(bout != null && bout.size() > 0) {
884
                    bos = new FileOutputStream(bout.get(0));
885
                }
886
                if(mout != null && bout.size() > 0) {
887
                    mos = new FileOutputStream(mout.get(0));
888
                    pw = new PrintWriter(mos);
889
                    pw.println("# Each line in this file that doesn't begin with a '#' contains the size of");
890
                    pw.println("# an individual blockdata block written to the stream.");
891
                }
892
                for(content c: content) {
893
                    System.out.println(c.toString());
894
                    if(c instanceof blockdata) {
895
                        blockdata bd = (blockdata)c;
896
                        if(mos != null) {
897
                            pw.println(bd.buf.length);
898
                        }
899
                        if(bos != null) {
900
                            bos.write(bd.buf);
901
                        }
902
                    }
903
                }
904
            } finally {
905
                if(bos != null) {
906
                    try {
907
                        bos.close();
908
                    } catch (IOException ignore) { }
909
                }
910
                if(mos != null) {
911
                    try {
912
                        pw.close();
913
                        mos.close();
914
                    } catch (IOException ignore) { }
915
                }
916
            }
917
        }
918
        if(!go.hasOption("-nocontent")) {
919
            System.out.println("//// BEGIN stream content output");
920
            for(content c: content) {
921
                System.out.println(c.toString());
922
            }
923
            System.out.println("//// END stream content output");
924
            System.out.println("");
925
        }
926

    
927
        if(!go.hasOption("-noclasses")) {
928
            boolean showarray = go.hasOption("-showarrays");
929
            List<String> fpat = go.getArguments("-filter");
930
            System.out.println("//// BEGIN class declarations"
931
                    + (showarray? "" : " (excluding array classes)")
932
                    + ((fpat != null && fpat.size() > 0) 
933
                        ? " (exclusion filter " + fpat.get(0) + ")"
934
                        : ""));
935
            for(content c: handles.values()) {
936
                if(c instanceof classdesc) {
937
                    classdesc cl = (classdesc)c;
938
                    if(showarray == false && cl.isArrayClass()) {
939
                        continue;
940
                    }
941
                    // Member classes will be displayed as part of their enclosing
942
                    // classes.
943
                    if(cl.isStaticMemberClass() || cl.isInnerClass()) {
944
                        continue;
945
                    }
946
                    if(fpat != null && fpat.size() > 0 && cl.name.matches(fpat.get(0))) {
947
                        continue;
948
                    }
949
                    dump_ClassDesc(0, cl, System.out, go.hasOption("-fixnames"));
950
                    System.out.println("");
951
                }
952
            }
953
            System.out.println("//// END class declarations");
954
            System.out.println("");
955
        }
956
        if(!go.hasOption("-noinstances")) {
957
            System.out.println("//// BEGIN instance dump");
958
            for(content c: handles.values()) {
959
                if(c instanceof instance) {
960
                    instance i = (instance)c;
961
                    dump_Instance(0, i, System.out);
962
                }
963
            }
964
            System.out.println("//// END instance dump");
965
            System.out.println("");
966
        }
967
    }
968

    
969

    
970
    /**
971
     * <p>
972
     * Connects member classes according to the rules specified by the JDK 1.1 Inner
973
     * Classes Specification.  
974
     * </p>
975
     *
976
     * <pre>
977
     * Inner classes:
978
     * for each class C containing an object reference member R named this$N, do:
979
     *     if the name of C matches the pattern O$I
980
     *     AND the name O matches the name of an existing type T
981
     *     AND T is the exact type referred to by R, then:
982
     *         don't display the declaration of R in normal dumping,
983
     *         consider C to be an inner class of O named I
984
     *
985
     * Static member classes (after):
986
     * for each class C matching the pattern O$I, 
987
     * where O is the name of a class in the same package
988
     * AND C is not an inner class according to the above algorithm:
989
     *     consider C to be an inner class of O named I
990
     * </pre>
991
     *
992
     * <p>
993
     * This functions fills in the isInnerClass value in classdesc, the
994
     * isInnerClassReference value in field, the isLocalInnerClass value in 
995
     * classdesc, and the isStaticMemberClass value in classdesc where necessary.
996
     * </p>
997
     *
998
     * <p>
999
     * A word on static classes: serializing a static member class S doesn't inherently
1000
     * require serialization of its parent class P.  Unlike inner classes, S doesn't
1001
     * retain an instance of P, and therefore P's class description doesn't need to be
1002
     * written.  In these cases, if parent classes can be found, their static member
1003
     * classes will be connected; but if they can't be found, the names will not be
1004
     * changed and no ValidityException will be thrown.
1005
     * </p>
1006
     *
1007
     * @throws ValidityException if the found values don't correspond to spec
1008
     */
1009
    public void connectMemberClasses() throws IOException {
1010
        HashMap<classdesc, String> newnames = new HashMap<classdesc, String>();
1011
        HashMap<String, classdesc> classes = new HashMap<String, classdesc>();
1012
        HashSet<String> classnames = new HashSet<String>();
1013
        for(content c: handles.values()) {
1014
            if(!(c instanceof classdesc)) {
1015
                continue;
1016
            }
1017
            classdesc cd = (classdesc)c;
1018
            classes.put(cd.name, cd);
1019
            classnames.add(cd.name);
1020
        }
1021
        Pattern fpat = Pattern.compile("^this\\$(\\d+)$");
1022
        Pattern clpat = Pattern.compile("^((?:[^\\$]+\\$)*[^\\$]+)\\$([^\\$]+)$");
1023
        for(classdesc cd: classes.values()) {
1024
            if(cd.classtype == classdesctype.PROXYCLASS) {
1025
                continue;
1026
            }
1027
            for(field f: cd.fields) {
1028
                if(f.type != fieldtype.OBJECT) {
1029
                    continue;
1030
                }
1031
                Matcher m = fpat.matcher(f.name);
1032
                if(!m.matches()) {
1033
                    continue;
1034
                }
1035
                boolean islocal = false;
1036
                Matcher clmat = clpat.matcher(cd.name);
1037
                if(!clmat.matches()) {
1038
                    throw new ValidityException("inner class enclosing-class reference field exists, but class name doesn't match expected pattern: class " + cd.name + " field " + f.name);
1039
                }
1040
                String outer = clmat.group(1), inner = clmat.group(2);
1041
                classdesc outercd = classes.get(outer);
1042
                if(outercd == null) {
1043
                    throw new ValidityException("couldn't connect inner classes: outer class not found for field name " + f.name);
1044
                }
1045
                if(!outercd.name.equals(f.getJavaType())) {
1046
                    throw new ValidityException("outer class field type doesn't match field type name: " + f.classname.value + " outer class name " + outercd.name);
1047
                }
1048
                outercd.addInnerClass(cd);
1049
                cd.setIsLocalInnerClass(islocal);
1050
                cd.setIsInnerClass(true);
1051
                f.setIsInnerClassReference(true);
1052
                newnames.put(cd, inner);
1053
            }
1054
        }
1055
        for(classdesc cd: classes.values()) {
1056
            if(cd.classtype == classdesctype.PROXYCLASS) {
1057
                continue;
1058
            }
1059
            if(cd.isInnerClass()) {
1060
                continue;
1061
            }
1062
            Matcher clmat = clpat.matcher(cd.name);
1063
            if(!clmat.matches()) {
1064
                continue;
1065
            }
1066
            String outer = clmat.group(1), inner = clmat.group(2);
1067
            classdesc outercd = classes.get(outer);
1068
            if(outercd != null) {
1069
                outercd.addInnerClass(cd);
1070
                cd.setIsStaticMemberClass(true);
1071
                newnames.put(cd, inner);
1072
            }
1073
        }
1074
        for(classdesc ncd: newnames.keySet()) {
1075
            String newname = newnames.get(ncd);
1076
            if(classnames.contains(newname)) {
1077
                throw new ValidityException("can't rename class from " + ncd.name + " to " + newname + " -- class already exists!");
1078
            }
1079
            for(classdesc cd: classes.values()) {
1080
                if(cd.classtype == classdesctype.PROXYCLASS) {
1081
                    continue;
1082
                }
1083
                for(field f: cd.fields) {
1084
                    if(f.getJavaType().equals(ncd.name)) {
1085
                        f.setReferenceTypeName(newname);
1086
                    }
1087
                }
1088
            }
1089
            if(classnames.remove(ncd.name) == false) {
1090
                throw new ValidityException("tried to remove " + ncd.name + " from classnames cache, but couldn't find it!");
1091
            }
1092
            ncd.name = newname;
1093
            if(classnames.add(newname) == false) {
1094
                throw new ValidityException("can't rename class to " + newname + " -- class already exists!");
1095
            }
1096
        }
1097
    }
1098

    
1099
    /**
1100
     * Decodes a class name according to the field-descriptor format in the jvm spec,
1101
     * section 4.3.2.
1102
     * @param fdesc name in field-descriptor format (Lfoo/bar/baz;)
1103
     * @param convertSlashes true iff slashes should be replaced with periods (true for
1104
     * "real" field-descriptor format; false for names in classdesc)
1105
     * @return a fully-qualified class name
1106
     * @throws ValidityException if the name isn't valid
1107
     */
1108
    public static String decodeClassName(String fdesc, boolean convertSlashes) throws ValidityException {
1109
        if(fdesc.charAt(0) != 'L' || fdesc.charAt(fdesc.length()-1) != ';' || fdesc.length() < 3) {
1110
            throw new ValidityException("invalid name (not in field-descriptor format): " + fdesc);
1111
        }
1112
        String subs = fdesc.substring(1, fdesc.length()-1);
1113
        if(convertSlashes) {
1114
            return subs.replace('/', '.');
1115
        } 
1116
        return subs;
1117
    }
1118

    
1119
    public static String hexnoprefix(long value) {
1120
        return hexnoprefix(value, 2);
1121
    }
1122
    public static String hexnoprefix(long value, int len) {
1123
        if(value < 0) {
1124
            value = 256 + value;
1125
        }
1126
        String s = Long.toString(value, 16);
1127
        while(s.length() < len) {
1128
            s = "0" + s;
1129
        }
1130
        return s;
1131
    }
1132
    public static String hex(long value) {
1133
        return "0x" + hexnoprefix(value);
1134
    }
1135
    public static void debugerr(String message) {
1136
        System.err.println(message);
1137
    }
1138
    public void debug(String message) {
1139
        if(debugEnabled) {
1140
            System.out.println(message);
1141
        }
1142
    }
1143

    
1144
    public static void main(String[] args) {
1145
        HashMap<String, Integer> options = new HashMap<String, Integer>();
1146
        Getopt go = new Getopt();
1147
        go.addOption("-help", 0, "Show this list.");
1148
        go.addOption("-debug", 0, "Write debug info generated during parsing to stdout.");
1149
        go.addOption("-filter", 1, "Exclude classes that match the given String.matches() regex from class output.");
1150
        go.addOption("-nocontent", 0, "Don't output descriptions of the content in the stream.");
1151
        go.addOption("-noinstances", 0, "Don't output descriptions of every instance.");
1152
        go.addOption("-showarrays", 0, "Show array class declarations (e.g. int[]).");
1153
        go.addOption("-noconnect", 0, "Don't attempt to connect member classes to their enclosing classes.");
1154
        go.addOption("-fixnames", 0, "In class names, replace illegal Java identifier characters with legal ones.");
1155
        go.addOption("-noclasses", 0, "Don't output class declarations.");
1156
        go.addOption("-blockdata", 1, "Write raw blockdata out to the specified file.");
1157
        go.addOption("-blockdatamanifest", 1, "Write blockdata manifest out to the specified file.");
1158
        try {
1159
            go.parse(args);
1160
        } catch (Getopt.OptionParseException ope) {
1161
            System.err.println("argument error: " + ope.getMessage());
1162
            System.out.println(go.getDescriptionString());
1163
            System.exit(1);
1164
        }
1165
        if(go.hasOption("-help")) {
1166
            System.out.println(go.getDescriptionString());
1167
            System.exit(1);
1168
        }
1169
        List<String> fargs = go.getOtherArguments();
1170
        if(fargs.size() < 1) {
1171
            debugerr("args: [options] file1 [file2 .. fileN]");
1172
            System.err.println("");
1173
            System.err.println(go.getDescriptionString());
1174
            System.exit(1);
1175
        }
1176
        for(String filename: fargs) {
1177
            FileInputStream fis = null;
1178
            try {
1179
                fis = new FileInputStream(filename);
1180
                jdeserialize jd = new jdeserialize(filename);
1181
                if(go.hasOption("-debug")) {
1182
                    jd.debugEnabled = true;
1183
                } else {
1184
                    jd.debugEnabled = false;
1185
                }
1186
                jd.run(fis, !go.hasOption("-noconnect"));
1187
                jd.dump(go);
1188
            } catch(EOFException eoe) {
1189
                debugerr("EOF error while attempting to decode file " + filename + ": " + eoe.getMessage());
1190
                eoe.printStackTrace();
1191
            } catch(IOException ioe) {
1192
                debugerr("error while attempting to decode file " + filename + ": " + ioe.getMessage());
1193
                ioe.printStackTrace();
1194
            } finally {
1195
                if(fis != null) {
1196
                    try {
1197
                        fis.close();
1198
                    } catch (Exception ignore) { }
1199
                }
1200
            }
1201
        }
1202
    }
1203
}
(19-19/20)