JDK-4679556 : XMLEncoder can be configured to produce invalid archives
  • Type: Bug
  • Component: client-libs
  • Sub-Component: java.beans
  • Affected Version: 1.4.0,1.4.1
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: linux,windows_nt,windows_2000
  • CPU: x86
  • Submitted: 2002-05-03
  • Updated: 2003-04-12
  • Resolved: 2002-08-12
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
1.4.2 mantisFixed
Related Reports
Duplicate :  
Duplicate :  
Duplicate :  
Duplicate :  
Relates :  
Description
;
        d.close();

        System.exit(0);
    }
}

---------- END SOURCE ----------

CUSTOMER WORKAROUND :
The only workaround I have found is occluding the
XMLEncoder class using -Xbootclasspath with the
fix provided.
(Review ID: 145688) 
======================================================================


Name: gm110360			Date: 05/03/2002


FULL PRODUCT VERSION :
java version "1.4.0"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.0-b92)
Java HotSpot(TM) Client VM (build 1.4.0-b92, mixed mode)

FULL OPERATING SYSTEM VERSION :

All platforms

A DESCRIPTION OF THE PROBLEM :
There is a bug in the XMLEncoder which causes the
encoder to create invalid archives for some graphs.
There does not seem to be a simple case in which
shows the error (I have spent all day looking
for one). The test case enclosed, while complicated,
is a useful case since it uses a set of persistence
delegates to encode the SpingLayout class.

I am the original author of the XMLEncoder (and made
the mistake!) so I have included the fix in this
report.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. javac Test
2. java Test
3.

EXPECTED VERSUS ACTUAL BEHAVIOR :
All XML files produced by the XMLEncoder should be
valid archives. The Test program uses the XMLDecoder
to load the "Test.xml" file it creates. This shows
the archive to be invalid.

ERROR MESSAGES/STACK TRACES THAT OCCUR :
java.lang.Exception: Unbound variable: SpringLayout$SpringProxy0
Continuing ...
java.lang.NullPointerException
Continuing ...
javax.swing.JPanel
[,0,0,0x0,invalid,layout=javax.swing.SpringLayout,alignmentX=null,alignmentY=nul
l,border=,flags=9,maximumSize=,minimumSize=,preferredSize=]


This bug can be reproduced always.

---------- BEGIN SOURCE ----------
/**
 * Demonstrates the XMLEncoder bug, where the XMLEncoder duplicates the
 * SpringLayout instance because it is required as the target
 * of a factory method (in proxy).
 *
 * TO FIX
 *
 * Move the first line of the XMLEncoder::mark(Statement method)
 * to the end of the method. I.e. replace the mark() method in
 * XMLEncoder with this:
 *
    private void mark(Statement stm) {
        Object[] args = stm.getArguments();
        for (int i = 0; i < args.length; i++) {
            Object arg = args[i];
            mark(arg, true);
        }
        mark(stm.getTarget(), false);
    }
 */


import javax.swing.*;
import java.awt.*;
import java.beans.*;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.lang.reflect.Field;
import java.util.Iterator;
import java.util.Map;

public class Test {
    private static Class compoundSpringClass;
    private static Class springProxyClass;

    private static boolean equals(Object o1, Object o2) {
        return (o1 == null) ? (o2 == null) : o1.equals(o2);
    }

    static {
        try {
            compoundSpringClass = Class.forName
("javax.swing.Spring$CompoundSpring");
            springProxyClass = Class.forName
("javax.swing.SpringLayout$SpringProxy");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

    private static Object getPrivateField(Object instance, Class
declaringClass, String name, ExceptionListener el) {
        try {
            Field f = declaringClass.getDeclaredField(name);
            f.setAccessible(true);
            return f.get(instance);
        } catch (Exception e) {
            el.exceptionThrown(e);
        }
        return null;
    }

    private static class CompoundSpringPersistenceDelegate extends
PersistenceDelegate {
        private String functionName;

        // Construct with the name of the function to be used.
        public CompoundSpringPersistenceDelegate(String functionName) {
            this.functionName = functionName;
        }

        protected Expression instantiate(Object oldInstance, Encoder out) {
            ExceptionListener el = out.getExceptionListener();
            Spring s1 = (Spring) getPrivateField(oldInstance,
compoundSpringClass, "s1", el);
            Spring s2 = (Spring) getPrivateField(oldInstance,
compoundSpringClass, "s2", el);
            return new Expression(oldInstance, Spring.class, functionName,
                    new Object[]{unproxy(s1), unproxy(s2)});
        }
    }

    private static JComponent getParent(Map constraints) {
        Iterator it = constraints.keySet().iterator();
        JComponent result = (JComponent) it.next();
        while (it.hasNext()) {
            JComponent c = (JComponent) it.next();
            if (c.getParent() == result) {
                // Fine, result is indeed the parent.
            } else if (result.getParent() == c) {
                // Have just hit the parent.
                result = c;
            } else {
                throw new RuntimeException("Cannot find parent for
SpringLayout.");
            }
        }
        return result;
    }

    private static Spring unproxy(Spring spring) {
        return spring;
    }

    public static void configure(Encoder e) throws Exception {
        final Class staticSpringClass = Class.forName
("javax.swing.Spring$StaticSpring");

        // SpringLayout
        e.setPersistenceDelegate(SpringLayout.class, new
DefaultPersistenceDelegate() {
            private void handleConstraints(Encoder out, Component oldComponent,
Object oldInstance, Object newInstance) {
                Component newComponent = (Component) out.get(oldComponent);
//                    SpringLayout.Constraints cts = (SpringLayout.Constraints)
e.getValue();
                Expression oldGetExp = new Expression
(oldInstance, "getConstraints", new Object[]{oldComponent});
                Expression newGetExp = new Expression
(newInstance, "getConstraints", new Object[]{newComponent});
                try {
                    SpringLayout.Constraints oldConstraints =
(SpringLayout.Constraints) oldGetExp.getValue();
                    SpringLayout.Constraints newConstraints =
(SpringLayout.Constraints) newGetExp.getValue();
                    out.writeExpression(oldGetExp);
                    if (!Test.equals(newConstraints, out.get(oldConstraints))) {
                        out.writeStatement(new Statement
(oldInstance, "addLayoutComponent", new Object[]{oldComponent,
oldConstraints}));
                    }
                } catch (Exception e1) {
                    System.out.println("Failed here ... ");
                    out.getExceptionListener().exceptionThrown(e1);
                }
            }

            protected void initialize(Class type, Object oldInstance, Object
newInstance, Encoder out) {
                SpringLayout oldSpringLayout = (SpringLayout) oldInstance;
                SpringLayout newSpringLayout = (SpringLayout) newInstance;

                Map oldConstraintsMap = (Map) getPrivateField(oldInstance,
SpringLayout.class, "componentConstraints", out.getExceptionListener());
                JComponent oldParent = getParent(oldConstraintsMap);
                JComponent newParent = (JComponent) out.get(oldParent);

                // The SpringLayout manager performs some initialization
                // the first time any of, get{Preferred, {Min, Max}imum}Size,
                // or layoutContainer is called. Call this here to make
                // sure any internal state is consistent.
                oldSpringLayout.layoutContainer(oldParent);
                if (newSpringLayout != null && newParent != null) {
                    newSpringLayout.layoutContainer(newParent);
                }
                // Do the parent component first.
//                handleConstraints(out, oldParent, oldInstance, newInstance);
                Iterator it = oldConstraintsMap.keySet().iterator();
                while (it.hasNext()) {
                    Component oldComponent = (Component) it.next();
                    if (oldComponent != oldParent) {
                        handleConstraints(out, oldComponent, oldInstance,
newInstance);
                    }
                }

            }
        });

        // Spring proxies.
        e.setPersistenceDelegate(springProxyClass, new PersistenceDelegate() {
            protected Expression instantiate(Object oldInstance, Encoder out) {
                ExceptionListener el = out.getExceptionListener();
                Object edgeName = getPrivateField(oldInstance,
springProxyClass, "edgeName", el);
                Object c = getPrivateField(oldInstance, springProxyClass, "c",
el);
                Object l = getPrivateField(oldInstance, springProxyClass, "l",
el);
                return new Expression(oldInstance, l, "getConstraint", new
Object[]{edgeName, c});
            }
            // The above delegate highlights a bug in the archiver.
            // It is not possible to initialise an instance with subgraph
            // of components that use a factory method on the instance
            // being initialised. The archiver incorrectly assumes that
            // such a factory method will be called in the context of
            // the enclosing instance (the one that is being used as
            // the factory) and therefore that it will not need an
            // id. It does in this case as the expression is part of
            // the initialisation of the factor oobject. Difficult to
            // explain.
            //
            // For now short cut all proxies when the archive is
            // being written.
        });

        // Spring.Constraints.

        // Width and Height Springs are installed automatically by the layout
manager when the
        // constraints object is underconstrained. Check this case and treat
instances
        // of the private Height and Width Spring classes like null.
        final Class heightSpringClass = Class.forName
("javax.swing.SpringLayout$HeightSpring");
        final Class widthSpringClass = Class.forName
("javax.swing.SpringLayout$WidthSpring");

        // Use private fields to initialise the SpringLayout as, for example,
the
        // public x, width and east properties of a constraints object depend
on each
        // other. Also, the east property, for example, is set using a non-
stnadard idiom.
        e.setPersistenceDelegate(SpringLayout.Constraints.class, new
DefaultPersistenceDelegate() {
            protected void initialize(Class type, Object oldInstance, Object
newInstance, Encoder out) {
                ExceptionListener el = out.getExceptionListener();
                String properties[] =
{"X", "Y", "Width", "Height", "East", "South"};
                for (int i = 0; i < properties.length; i++) {
                    String pName = properties[i];
                    Spring s = (Spring) getPrivateField(oldInstance,
SpringLayout.Constraints.class, pName.toLowerCase(), el);
                    if (s != null && s.getClass() != heightSpringClass &&
s.getClass() != widthSpringClass) {
                        Expression oldGetExp;
                        Expression newGetExp;
                        if (pName != "East" && pName != "South") {
                            oldGetExp = new Expression(oldInstance, "get" +
pName, new Object[]{});
                            newGetExp = new Expression(newInstance, "get" +
pName, new Object[]{});
                        } else {
                            oldGetExp = new Expression
(oldInstance, "getConstraint", new Object[]{pName});
                            newGetExp = new Expression
(newInstance, "getConstraint", new Object[]{pName});
                        }
                        try {
                            Spring oldSpring = (Spring) oldGetExp.getValue();
                            Spring newSpring = (Spring) newGetExp.getValue();
//                            out.writeExpression(oldGetExp);
                            if (!Test.equals(newSpring, out.get(oldSpring))) {
                                if (pName != "East" && pName != "South") {
                                    out.writeStatement(new Statement
(oldInstance, "set" + pName, new Object[]{s}));
                                } else {
                                    out.writeStatement(new Statement
(oldInstance, "setConstraint", new Object[]{pName, s}));
                                }
                            }
                        } catch (Exception e1) {
                            System.out.println("Failed here ... ");
                            out.getExceptionListener().exceptionThrown(e1);
                        }

                    }
                }
            }
        });

        // Spring constants.
        e.setPersistenceDelegate(staticSpringClass, new PersistenceDelegate() {
            protected Expression instantiate(Object oldInstance, Encoder out) {
                Spring s = (Spring) oldInstance;
                int min = s.getMinimumValue();
                int pref = s.getPreferredValue();
                int max = s.getMaximumValue();
                Object[] args = null;
                // Use the shorter static method if possible.
                if (min == pref && pref == max) {
                    args = new Object[]{new Integer(pref)};
                } else {
                    args = new Object[]{new Integer(min), new Integer(pref),
new Integer(max)};
                }
                return new Expression(oldInstance, Spring.class, "constant",
args);
            }
        });

        // Negative Springs.
        final Class negativeSpringClass = Class.forName
("javax.swing.Spring$NegativeSpring");
        e.setPersistenceDelegate(negativeSpringClass, new PersistenceDelegate()
{
            protected Expression instantiate(Object oldInstance, Encoder out) {
                ExceptionListener el = out.getExceptionListener();
                Spring s = (Spring) getPrivateField(oldInstance,
negativeSpringClass, "s", el);
                return new Expression(oldInstance, Spring.class, "minus", new
Object[]{unproxy(s)});
            }
        });

        // Spring sums.
        e.setPersistenceDelegate(Class.forName("javax.swing.Spring$SumSpring"),
                new CompoundSpringPersistenceDelegate("sum"));

        // Spring maxima.
        e.setPersistenceDelegate(Class.forName("javax.swing.Spring$MaxSpring"),
                new CompoundSpringPersistenceDelegate("max"));

    }


    public static void main(String[] args) throws Exception {
        JPanel panel = new JPanel(new SpringLayout());
        JButton button = new JButton();
        panel.add(button);
        SpringLayout l = (SpringLayout) panel.getLayout();
        // Force getParent() to be called in the layout manager.
        l.layoutContainer(panel);
        l.putConstraint("South", button, -20, "South", panel);
        XMLEncoder e = new XMLEncoder(
                new BufferedOutputStream(
                        new FileOutputStream("Test.xml")));
        configure(e);
        e.writeObject(panel);
        e.close();

        // Read file back in to demonstrate that it is invalid.
        XMLDecoder d = new XMLDecoder(
                           new BufferedInputStream(
                               new FileInputStream("Test.xml")));
        Object root = d.readObject();
        System.out.println(root)

Comments
CONVERTED DATA BugTraq+ Release Management Values COMMIT TO FIX: mantis mantis-b02 FIXED IN: mantis mantis-b02 INTEGRATED IN: mantis mantis-b02
14-06-2004

PUBLIC COMMENTS .
10-06-2004

SUGGESTED FIX Move the mark statement as described in the evaluation. Also, the submitter noticed that the exception handling in XMLEncoder.writeStatement will clobber the thown exception. The exception handler should be rewritten as follows: getExceptionListener().exceptionThrown(new Exception("discarding statement " + oldStm, e)); ###@###.### 2002-05-23
23-05-2002

EVALUATION Just a quick look at the bug and solution. I implemented the solution and I'm still getting the problem that was described. ###@###.### 2002-05-06 Submitter provided a simpler test case and the problem and recommended solution seems to fix it. Will examine more thoroughly later. /** * Demonstrates the archiver bug, where the XMLEncoder duplicates the * instance of class A because it is required as the target * of a factory method (to produce an instance of class C). See * the output in the file Test.xml for the results and note * the (invalid) forward reference to the instance of class C. * * TO FIX * * Move the first line of the XMLEncoder::mark(Statement method) * to the end of the method. I.e. replace the mark() method in * XMLEncoder with this: * private void mark(Statement stm) { Object[] args = stm.getArguments(); for (int i = 0; i < args.length; i++) { Object arg = args[i]; mark(arg, true); } mark(stm.getTarget(), false); } */ import java.beans.*; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; public class SimplerTest { public static class Base { private Object x; public Object getX() { return x; } public void setX(Object x) { this.x = x; } } public static class A extends Base { public A() { setX(new B()); } public C createC() { return new C(this); } } public static class B extends Base { } public static class C extends Base { private C(Object x) { setX(x); } } public static void main(String[] args) throws Exception { String filename = "Test" + System.getProperty("java.version") + ".xml"; A a = new A(); ((B) a.getX()).setX(a.createC()); XMLEncoder e = new XMLEncoder( new BufferedOutputStream( new FileOutputStream(filename))); e.setPersistenceDelegate(C.class, new DefaultPersistenceDelegate() { protected Expression instantiate(Object oldInstance, Encoder out) { C c = (C) oldInstance; return new Expression(c, c.getX(), "createC", new Object[]{}); } }); e.writeObject(a); e.close(); // Read file back in to demonstrate that it is invalid. XMLDecoder d = new XMLDecoder( new BufferedInputStream( new FileInputStream(filename))); Object root = d.readObject(); System.out.println(root); d.close(); System.exit(0); } } ###@###.### 2002-05-15
15-05-2002