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 HashMap<Integer,content> handles = new HashMap<Integer,content>();
77
    private ArrayList<Map<Integer,content>> handlemaps = new ArrayList<Map<Integer,content>>();
78
    private ArrayList<content> content;
79
    private int curhandle;
80
    private boolean debugEnabled;
81

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

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

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

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

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

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

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

    
239
    private int newHandle() {
240
        return curhandle++;
241
    }
242

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

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

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

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

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

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

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

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

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

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

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

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

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

    
966

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

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

    
1116
    public static String hexnoprefix(long value) {
1117
        return hexnoprefix(value, 2);
1118
    }
1119
    public static String hexnoprefix(long value, int len) {
1120
        if(value < 0) {
1121
            value = 256 + value;
1122
        }
1123
        String s = Long.toString(value, 16);
1124
        while(s.length() < len) {
1125
            s = "0" + s;
1126
        }
1127
        return s;
1128
    }
1129
    public static String hex(long value) {
1130
        return "0x" + hexnoprefix(value);
1131
    }
1132
    public static void debugerr(String message) {
1133
        System.err.println(message);
1134
    }
1135
    public void debug(String message) {
1136
        if(debugEnabled) {
1137
            System.out.println(message);
1138
        }
1139
    }
1140
    
1141
    /*
1142
    This main commented-in because of using the project as library. No need for main.
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(fis);
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
    */
1204
}
(19-19/20)