1/*
2 *  Licensed to the Apache Software Foundation (ASF) under one or more
3 *  contributor license agreements.  See the NOTICE file distributed with
4 *  this work for additional information regarding copyright ownership.
5 *  The ASF licenses this file to You under the Apache License, Version 2.0
6 *  (the "License"); you may not use this file except in compliance with
7 *  the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 */
17
18/**
19* @author Alexey V. Varlamov
20* @version $Revision$
21*/
22
23package org.apache.harmony.testframework.serialization;
24
25import junit.framework.TestCase;
26import java.io.ByteArrayInputStream;
27import java.io.ByteArrayOutputStream;
28import java.io.File;
29import java.io.FileOutputStream;
30import java.io.IOException;
31import java.io.InputStream;
32import java.io.ObjectInputStream;
33import java.io.ObjectOutputStream;
34import java.io.OutputStream;
35import java.io.Serializable;
36import java.lang.reflect.Method;
37import java.security.Permission;
38import java.security.PermissionCollection;
39import java.util.Collection;
40import java.util.Collections;
41import java.util.HashSet;
42
43/**
44 * Framework for serialization testing. Subclasses only need to override
45 * getData() method and, optionally, assertDeserialized() method. The first one
46 * returns array of objects to be de/serialized in tests, and the second
47 * compares reference and deserialized objects (needed only if tested objects do
48 * not provide specific method equals()). <br>
49 * There are two modes of test run: <b>reference generation mode </b> and
50 * <b>testing mode </b>. The actual mode is selected via
51 * <b>&quot;test.mode&quot; </b> system property. The <b>testing mode </b> is
52 * the default mode. <br>
53 * To turn on the <b>reference generation mode </b>, the test.mode property
54 * should be set to value &quot;serial.reference&quot;. In this mode, no testing
55 * is performed but golden files are produced, which contain reference
56 * serialized objects. This mode should be run on a pure
57 * Implementation classes, which are targeted for compartibility. <br>
58 * The location of golden files (in both modes) is controlled via
59 * <b>&quot;RESOURCE_DIR&quot; </b> system property.
60 *
61 */
62public abstract class SerializationTest extends TestCase {
63
64    /**
65     * Key to a system property defining root location of golden files.
66     */
67    public static final String GOLDEN_PATH = "RESOURCE_DIR";
68
69    private static final String outputPath = System.getProperty(GOLDEN_PATH,
70            "src/test/resources/serialization");
71
72    /**
73     * This is the main working method of this framework. Subclasses must
74     * override it to provide actual objects for testing.
75     *
76     * @return array of objects to be de/serialized in tests.
77     */
78    protected abstract Object[] getData();
79
80    /**
81     * Tests that data objects can be serialized and deserialized without
82     * exceptions, and that deserialization really produces deeply cloned
83     * objects.
84     */
85    public void testSelf() throws Throwable {
86
87        if (this instanceof SerializableAssert) {
88            verifySelf(getData(), (SerializableAssert) this);
89        } else {
90            verifySelf(getData());
91
92        }
93    }
94
95    /**
96     * Tests that data objects can be deserialized from golden files, to verify
97     * compatibility with Reference Implementation.
98     */
99
100    public void testGolden() throws Throwable {
101        verifyGolden(this, getData());
102    }
103
104    /**
105     * Returns golden file for an object being tested.
106     *
107     * @param index array index of tested data (as returned by
108     *        {@link #getData() getData()})
109     * @return corresponding golden file
110     */
111    protected File getDataFile(int index) {
112        String name = this.getClass().getName();
113        int dot = name.lastIndexOf(".");
114        String path = name.substring(0, dot).replace('.', File.separatorChar);
115        if (outputPath != null && outputPath.length() != 0) {
116            path = outputPath + File.separator + path;
117        }
118
119        return new File(path, name.substring(dot + 1) + "." + index + ".dat");
120    }
121
122    /**
123     * Working method for files generation mode. Serializes test objects
124     * returned by {@link #getData() getData()}to golden files, each object to
125     * a separate file.
126     *
127     * @throws IOException
128     */
129    protected void produceGoldenFiles() throws IOException {
130
131        String goldenPath = outputPath + File.separatorChar
132                + getClass().getName().replace('.', File.separatorChar)
133                + ".golden.";
134
135        Object[] data = getData();
136        for (int i = 0; i < data.length; i++) {
137
138            File goldenFile = new File(goldenPath + i + ".ser");
139            goldenFile.getParentFile().mkdirs();
140            goldenFile.createNewFile();
141
142            putObjectToStream(data[i], new FileOutputStream(goldenFile));
143        }
144    }
145
146    /**
147     * Serializes specified object to an output stream.
148     */
149    public static void putObjectToStream(Object obj, OutputStream os)
150            throws IOException {
151        ObjectOutputStream oos = new ObjectOutputStream(os);
152        oos.writeObject(obj);
153        oos.flush();
154        oos.close();
155    }
156
157    /**
158     * Deserializes single object from an input stream.
159     */
160    public static Serializable getObjectFromStream(InputStream is)
161            throws IOException, ClassNotFoundException {
162        ObjectInputStream ois = new ObjectInputStream(is);
163        Object result = ois.readObject();
164        ois.close();
165        return (Serializable)result;
166    }
167
168    /**
169     * Interface to compare (de)serialized objects
170     *
171     * Should be implemented if a class under test does not provide specific
172     * equals() method and it's instances should to be compared manually.
173     */
174    public interface SerializableAssert {
175
176        /**
177         * Compares deserialized and reference objects.
178         *
179         * @param initial - initial object used for creating serialized form
180         * @param deserialized - deserialized object
181         */
182        void assertDeserialized(Serializable initial, Serializable deserialized);
183    }
184
185    // default comparator for a class that has equals(Object) method
186    private final static SerializableAssert DEFAULT_COMPARATOR = new SerializableAssert() {
187        public void assertDeserialized(Serializable initial, Serializable deserialized) {
188            assertEquals(initial, deserialized);
189        }
190    };
191
192    /**
193     * Comparator for verifying that deserialized object is the same as initial.
194     */
195    public final static SerializableAssert SAME_COMPARATOR = new SerializableAssert() {
196        public void assertDeserialized(Serializable initial, Serializable deserialized) {
197            assertSame(initial, deserialized);
198        }
199    };
200
201    /**
202     * Comparator for Throwable objects
203     */
204    public final static SerializableAssert THROWABLE_COMPARATOR = new SerializableAssert() {
205        public void assertDeserialized(Serializable initial, Serializable deserialized) {
206            Throwable initThr = (Throwable) initial;
207            Throwable dserThr = (Throwable) deserialized;
208
209            // verify class
210            assertEquals(initThr.getClass(), dserThr.getClass());
211
212            // verify message
213            assertEquals(initThr.getMessage(), dserThr.getMessage());
214
215            // verify cause
216            if (initThr.getCause() == null) {
217                assertNull(dserThr.getCause());
218            } else {
219                assertNotNull(dserThr.getCause());
220                THROWABLE_COMPARATOR.assertDeserialized(initThr.getCause(),
221                        dserThr.getCause());
222            }
223        }
224    };
225
226    /**
227     * Comparator for PermissionCollection objects
228     */
229    public final static SerializableAssert PERMISSION_COLLECTION_COMPARATOR = new SerializableAssert() {
230        public void assertDeserialized(Serializable initial, Serializable deserialized) {
231
232            PermissionCollection initPC = (PermissionCollection) initial;
233            PermissionCollection dserPC = (PermissionCollection) deserialized;
234
235            // verify class
236            assertEquals(initPC.getClass(), dserPC.getClass());
237
238            // verify 'readOnly' field
239            assertEquals(initPC.isReadOnly(), dserPC.isReadOnly());
240
241            // verify collection of permissions
242            Collection<Permission> refCollection = new HashSet<Permission>(
243                    Collections.list(initPC.elements()));
244            Collection<Permission> tstCollection = new HashSet<Permission>(
245                    Collections.list(dserPC.elements()));
246
247            assertEquals(refCollection, tstCollection);
248        }
249    };
250
251    /**
252     * Returns <code>comparator</code> for provided serializable
253     * <code>object</code>.
254     *
255     * The <code>comparator</code> is searched in the following order: <br>
256     * - if <code>test</code> implements SerializableAssert interface then it is
257     * selected as </code>comparator</code>.<br>- if passed <code>object</code>
258     * has class in its classes hierarchy that overrides <code>equals(Object)</code>
259     * method then <code>DEFAULT_COMPARATOR</code> is selected.<br> - the
260     * method tries to select one of known comparators basing on <code>object's</code>
261     * class,for example, if passed <code>object</code> is instance of
262     * Throwable then <code>THROWABLE_COMPARATOR</code> is used.<br>
263     * - otherwise RuntimeException is thrown
264     *
265     * @param test - test case
266     * @param object - object to be compared
267     * @return object's comparator
268     */
269    public static SerializableAssert defineComparator(Object test, Object object)
270            throws Exception {
271
272        if (test instanceof SerializableAssert) {
273            return (SerializableAssert) test;
274        }
275
276        Method m = object.getClass().getMethod("equals", new Class[] { Object.class });
277        if (m.getDeclaringClass() != Object.class) {
278            // one of classes overrides Object.equals(Object) method
279            // use default comparator
280            return DEFAULT_COMPARATOR;
281        }
282
283        // TODO use generics to detect comparator
284        // instead of 'instanceof' for the first element
285        if (object instanceof Throwable) {
286            return THROWABLE_COMPARATOR;
287        }
288        if (object instanceof PermissionCollection) {
289            return PERMISSION_COLLECTION_COMPARATOR;
290        }
291        throw new RuntimeException("Failed to detect comparator");
292    }
293
294    /**
295     * Verifies that object deserialized from golden file correctly.
296     *
297     * The method invokes <br>
298     * verifyGolden(test, object, defineComparator(test, object));
299     *
300     * @param test - test case
301     * @param object - to be compared
302     */
303    public static void verifyGolden(Object test, Object object) throws Exception {
304        verifyGolden(test, object, defineComparator(test, object));
305    }
306
307    /**
308     * Verifies that object deserialized from golden file correctly.
309     *
310     * The method loads "<code>testName</code>.golden.ser" resource file
311     * from "<module root>/src/test/resources/serialization/<code>testPackage</code>"
312     * folder, reads an object from the loaded file and compares it with
313     * <code>object</code> using specified <code>comparator</code>.
314     *
315     * @param test - test case
316     * @param object - to be compared
317     * @param comparator - for comparing (de)serialized objects
318     */
319    public static void verifyGolden(Object test, Object object, SerializableAssert comparator)
320            throws Exception {
321        assertNotNull("Null comparator", comparator);
322        Serializable deserialized = getObject(test, ".golden.ser");
323        comparator.assertDeserialized((Serializable) object, deserialized);
324    }
325
326    /**
327     * Verifies that objects from array deserialized from golden files
328     * correctly.
329     *
330     * The method invokes <br>
331     * verifyGolden(test, objects, defineComparator(test, object[0]));
332     *
333     * @param test - test case
334     * @param objects - array of objects to be compared
335     */
336    public static void verifyGolden(Object test, Object[] objects) throws Exception {
337        assertFalse("Empty array", objects.length == 0);
338        verifyGolden(test, objects, defineComparator(test, objects[0]));
339    }
340
341    /**
342     * Verifies that objects from array deserialized from golden files
343     * correctly.
344     *
345     * The method loads "<code>testName</code>.golden.<code>N</code>.ser"
346     * resource files from "<module root>/src/test/resources/serialization/<code>testPackage</code>"
347     * folder, from each loaded file it reads an object from and compares it
348     * with corresponding object in provided array (i.e. <code>objects[N]</code>)
349     * using specified <code>comparator</code>. (<code>N</code> is index
350     * in object's array.)
351     *
352     * @param test - test case
353     * @param objects - array of objects to be compared
354     * @param comparator - for comparing (de)serialized objects
355     */
356    public static void verifyGolden(Object test, Object[] objects, SerializableAssert comparator)
357            throws Exception {
358        assertFalse("Empty array", objects.length == 0);
359        for (int i = 0; i < objects.length; i++) {
360            Serializable deserialized = getObject(test, ".golden." + i + ".ser");
361            comparator.assertDeserialized((Serializable) objects[i], deserialized);
362        }
363    }
364
365    /**
366     * Verifies that object can be smoothly serialized/deserialized.
367     *
368     * The method invokes <br>
369     * verifySelf(object, defineComparator(null, object));
370     *
371     * @param object - to be serialized/deserialized
372     */
373    public static void verifySelf(Object object) throws Exception {
374        verifySelf(object, defineComparator(null, object));
375    }
376
377    /**
378     * Verifies that object can be smoothly serialized/deserialized.
379     *
380     * The method serialize/deserialize <code>object</code> and compare it
381     * with initial <code>object</code>.
382     *
383     * @param object - object to be serialized/deserialized
384     * @param comparator - for comparing serialized/deserialized object with initial object
385     */
386    public static void verifySelf(Object object, SerializableAssert comparator) throws Exception {
387        Serializable initial = (Serializable) object;
388        comparator.assertDeserialized(initial, copySerializable(initial));
389    }
390
391    /**
392     * Verifies that that objects from array can be smoothly
393     * serialized/deserialized.
394     *
395     * The method invokes <br>
396     * verifySelf(objects, defineComparator(null, object[0]));
397     *
398     * @param objects - array of objects to be serialized/deserialized
399     */
400    public static void verifySelf(Object[] objects) throws Exception {
401        assertFalse("Empty array", objects.length == 0);
402        verifySelf(objects, defineComparator(null, objects[0]));
403    }
404
405    /**
406     * Verifies that that objects from array can be smoothly
407     * serialized/deserialized.
408     *
409     * The method serialize/deserialize each object in <code>objects</code>
410     * array and compare it with initial object.
411     *
412     * @param objects - array of objects to be serialized/deserialized
413     * @param comparator - for comparing serialized/deserialized object with initial object
414     */
415    public static void verifySelf(Object[] objects, SerializableAssert comparator)
416            throws Exception {
417        assertFalse("Empty array", objects.length == 0);
418        for (Object entry: objects){
419            verifySelf(entry, comparator);
420        }
421    }
422
423    private static Serializable getObject(Object test, String toAppend) throws Exception {
424        StringBuilder path = new StringBuilder("/serialization");
425        path.append(File.separatorChar);
426        path.append(test.getClass().getName().replace('.', File.separatorChar));
427        path.append(toAppend);
428
429        String pathString = path.toString();
430
431        InputStream in = SerializationTest.class.getResourceAsStream(pathString);
432        assertNotNull("Failed to load serialization resource file: " + path, in);
433        return getObjectFromStream(in);
434    }
435
436    /**
437     * Creates golden file.
438     *
439     * The folder for created file is: <code>root + test's package name</code>.
440     * The file name is: <code>test's name + "golden.ser"</code>
441     *
442     * @param root - root directory for serialization resource files
443     * @param test - test case
444     * @param object - to be serialized
445     * @throws IOException - if I/O error
446     */
447    public static void createGoldenFile(String root, TestCase test, Object object)
448            throws IOException {
449        String goldenPath = (test.getClass().getName().replace('.', File.separatorChar)
450                             + ".golden.ser");
451        if (root != null) {
452            goldenPath = root + File.separatorChar + goldenPath;
453        }
454
455        File goldenFile = new File(goldenPath);
456        goldenFile.getParentFile().mkdirs();
457        assertTrue("Could not create " + goldenFile.getParentFile(),
458                   goldenFile.getParentFile().isDirectory());
459        goldenFile.createNewFile();
460        putObjectToStream(object, new FileOutputStream(goldenFile));
461
462        // don't forget to remove it from test case after using
463        fail("Generating golden file. Golden file name: " + goldenFile.getAbsolutePath());
464    }
465
466    /**
467     * Copies an object by serializing/deserializing it.
468     *
469     * @param initial - an object to be copied
470     * @return copy of provided object
471     */
472    public static Serializable copySerializable(Serializable initial)
473            throws IOException, ClassNotFoundException {
474        ByteArrayOutputStream out = new ByteArrayOutputStream();
475        putObjectToStream(initial, out);
476        ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
477        return getObjectFromStream(in);
478    }
479}
480