/*
 * Copyright 2003-2006 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package javax.swing.plaf.synth;

import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Toolkit;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.PatternSyntaxException;

import javax.swing.ImageIcon;
import javax.swing.JSplitPane;
import javax.swing.SwingConstants;
import javax.swing.UIDefaults;
import javax.swing.plaf.ColorUIResource;
import javax.swing.plaf.DimensionUIResource;
import javax.swing.plaf.FontUIResource;
import javax.swing.plaf.InsetsUIResource;
import javax.swing.plaf.UIResource;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.AttributeList;
import org.xml.sax.HandlerBase;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import com.sun.beans.ObjectHandler;

/**
 */
class SynthParser extends HandlerBase {
    //
    // Known element names
    //
    private static final String ELEMENT_SYNTH = "synth";
    private static final String ELEMENT_STYLE = "style";
    private static final String ELEMENT_STATE = "state";
    private static final String ELEMENT_FONT = "font";
    private static final String ELEMENT_COLOR = "color";
    private static final String ELEMENT_IMAGE_PAINTER = "imagePainter";
    private static final String ELEMENT_PAINTER = "painter";
    private static final String ELEMENT_PROPERTY = "property";
    private static final String ELEMENT_SYNTH_GRAPHICS = "graphicsUtils";
    private static final String ELEMENT_IMAGE_ICON = "imageIcon";
    private static final String ELEMENT_BIND = "bind";
    private static final String ELEMENT_BIND_KEY = "bindKey";
    private static final String ELEMENT_INSETS = "insets";
    private static final String ELEMENT_OPAQUE = "opaque";
    private static final String ELEMENT_DEFAULTS_PROPERTY =
                                        "defaultsProperty";
    private static final String ELEMENT_INPUT_MAP = "inputMap";

    //
    // Known attribute names
    //
    private static final String ATTRIBUTE_ACTION = "action";
    private static final String ATTRIBUTE_ID = "id";
    private static final String ATTRIBUTE_IDREF = "idref";
    private static final String ATTRIBUTE_CLONE = "clone";
    private static final String ATTRIBUTE_VALUE = "value";
    private static final String ATTRIBUTE_NAME = "name";
    private static final String ATTRIBUTE_STYLE = "style";
    private static final String ATTRIBUTE_SIZE = "size";
    private static final String ATTRIBUTE_TYPE = "type";
    private static final String ATTRIBUTE_TOP = "top";
    private static final String ATTRIBUTE_LEFT = "left";
    private static final String ATTRIBUTE_BOTTOM = "bottom";
    private static final String ATTRIBUTE_RIGHT = "right";
    private static final String ATTRIBUTE_KEY = "key";
    private static final String ATTRIBUTE_SOURCE_INSETS = "sourceInsets";
    private static final String ATTRIBUTE_DEST_INSETS = "destinationInsets";
    private static final String ATTRIBUTE_PATH = "path";
    private static final String ATTRIBUTE_STRETCH = "stretch";
    private static final String ATTRIBUTE_PAINT_CENTER = "paintCenter";
    private static final String ATTRIBUTE_METHOD = "method";
    private static final String ATTRIBUTE_DIRECTION = "direction";
    private static final String ATTRIBUTE_CENTER = "center";

    /**
     * Lazily created, used for anything we don't understand.
     */
    private ObjectHandler _handler;

    /**
     * Indicates the depth of how many elements we've encountered but don't
     * understand. This is used when forwarding to beans persistance to know
     * when we hsould stop forwarding.
     */
    private int _depth;

    /**
     * Factory that new styles are added to.
     */
    private DefaultSynthStyleFactory _factory;

    /**
     * Array of state infos for the current style. These are pushed to the
     * style when </style> is received.
     */
    private java.util.List _stateInfos;

    /**
     * Current style.
     */
    private ParsedSynthStyle _style;

    /**
     * Current state info.
     */
    private ParsedSynthStyle.StateInfo _stateInfo;

    /**
     * Bindings for the current InputMap
     */
    private java.util.List _inputMapBindings;

    /**
     * ID for the input map. This is cached as
     * the InputMap is created AFTER the inputMapProperty has ended.
     */
    private String _inputMapID;

    /**
     * Object references outside the scope of persistance.
     */
    private Map<String,Object> _mapping;

    /**
     * Based URL used to resolve paths.
     */
    private URL _urlResourceBase;

    /**
     * Based class used to resolve paths.
     */
    private Class<?> _classResourceBase;

    /**
     * List of ColorTypes. This is populated in startColorType.
     */
    private java.util.List _colorTypes;

    /**
     * defaultsPropertys are placed here.
     */
    private Map _defaultsMap;

    /**
     * List of SynthStyle.Painters that will be applied to the current style.
     */
    private java.util.List _stylePainters;

    /**
     * List of SynthStyle.Painters that will be applied to the current state.
     */
    private java.util.List _statePainters;

    SynthParser() {
        _mapping = new HashMap<String,Object>();
        _stateInfos = new ArrayList();
        _colorTypes = new ArrayList();
        _inputMapBindings = new ArrayList();
        _stylePainters = new ArrayList();
        _statePainters = new ArrayList();
    }

    /**
     * Parses a set of styles from <code>inputStream</code>, adding the
     * resulting styles to the passed in DefaultSynthStyleFactory.
     * Resources are resolved either from a URL or from a Class. When calling
     * this method, one of the URL or the Class must be null but not both at
     * the same time.
     *
     * @param inputStream XML document containing the styles to read
     * @param factory DefaultSynthStyleFactory that new styles are added to
     * @param urlResourceBase the URL used to resolve any resources, such as Images
     * @param classResourceBase the Class used to resolve any resources, such as Images
     * @param defaultsMap Map that UIDefaults properties are placed in
     */
    public void parse(InputStream inputStream,
                      DefaultSynthStyleFactory factory,
                      URL urlResourceBase, Class<?> classResourceBase,
                      Map defaultsMap)
                      throws ParseException, IllegalArgumentException {
        if (inputStream == null || factory == null ||
            (urlResourceBase == null && classResourceBase == null)) {
            throw new IllegalArgumentException(
                "You must supply an InputStream, StyleFactory and Class or URL");
        }

        assert(!(urlResourceBase != null && classResourceBase != null));

        _factory = factory;
        _classResourceBase = classResourceBase;
        _urlResourceBase = urlResourceBase;
        _defaultsMap = defaultsMap;
        try {
            try {
                SAXParser saxParser = SAXParserFactory.newInstance().
                                                   newSAXParser();
                saxParser.parse(new BufferedInputStream(inputStream), this);
            } catch (ParserConfigurationException e) {
                throw new ParseException("Error parsing: " + e, 0);
            }
            catch (SAXException se) {
                throw new ParseException("Error parsing: " + se + " " +
                                         se.getException(), 0);
            }
            catch (IOException ioe) {
                throw new ParseException("Error parsing: " + ioe, 0);
            }
        } finally {
            reset();
        }
    }

    /**
     * Returns the path to a resource.
     */
    private URL getResource(String path) {
        if (_classResourceBase != null) {
            return _classResourceBase.getResource(path);
        } else {
            try {
                return new URL(_urlResourceBase, path);
            } catch (MalformedURLException mue) {
                return null;
            }
        }
    }

    /**
     * Clears our internal state.
     */
    private void reset() {
        _handler = null;
        _depth = 0;
        _mapping.clear();
        _stateInfos.clear();
        _colorTypes.clear();
        _statePainters.clear();
        _stylePainters.clear();
    }

    /**
     * Returns true if we are forwarding to persistance.
     */
    private boolean isForwarding() {
        return (_depth > 0);
    }

    /**
     * Handles beans persistance.
     */
    private ObjectHandler getHandler() {
        if (_handler == null) {
            if (_urlResourceBase != null) {
                // getHandler() is never called before parse() so it is safe
                // to create a URLClassLoader with _resourceBase.
                //
                // getResource(".") is called to ensure we have the directory
                // containing the resources in the case the resource base is a
                // .class file.
                URL[] urls = new URL[] { getResource(".") };
                ClassLoader parent = Thread.currentThread().getContextClassLoader();
                ClassLoader urlLoader = new URLClassLoader(urls, parent);
                _handler = new ObjectHandler(null, urlLoader);
            } else {
                _handler = new ObjectHandler(null,
                    _classResourceBase.getClassLoader());
            }

            for (String key : _mapping.keySet()) {
                _handler.register(key, _mapping.get(key));
            }
        }
        return _handler;
    }

    /**
     * If <code>value</code> is an instance of <code>type</code> it is
     * returned, otherwise a SAXException is thrown.
     */
    private Object checkCast(Object value, Class type) throws SAXException {
        if (!type.isInstance(value)) {
            throw new SAXException("Expected type " + type + " got " +
                                   value.getClass());
        }
        return value;
    }

    /**
     * Returns an object created with id=key. If the object is not of
     * type type, this will throw an exception.
     */
    private Object lookup(String key, Class type) throws SAXException {
        Object value = null;
        if (_handler != null) {
            if ((value = _handler.lookup(key)) != null) {
                return checkCast(value, type);
            }
        }
        value = _mapping.get(key);
        if (value == null) {
            throw new SAXException("ID " + key + " has not been defined");
        }
        return checkCast(value, type);
    }

    /**
     * Registers an object by name. This will throw an exception if an
     * object has already been registered under the given name.
     */
    private void register(String key, Object value) throws SAXException {
        if (key != null) {
            if (_mapping.get(key) != null ||
                     (_handler != null && _handler.lookup(key) != null)) {
                throw new SAXException("ID " + key + " is already defined");
            }
            if (_handler != null) {
                _handler.register(key, value);
            }
            else {
                _mapping.put(key, value);
            }
        }
    }

    /**
     * Convenience method to return the next int, or throw if there are no
     * more valid ints.
     */
    private int nextInt(StringTokenizer tok, String errorMsg) throws
                   SAXException {
        if (!tok.hasMoreTokens()) {
            throw new SAXException(errorMsg);
        }
        try {
            return Integer.parseInt(tok.nextToken());
        } catch (NumberFormatException nfe) {
            throw new SAXException(errorMsg);
        }
    }

    /**
     * Convenience method to return an Insets object.
     */
    private Insets parseInsets(String insets, String errorMsg) throws
                        SAXException {
        StringTokenizer tokenizer = new StringTokenizer(insets);
        return new Insets(nextInt(tokenizer, errorMsg),
                          nextInt(tokenizer, errorMsg),
                          nextInt(tokenizer, errorMsg),
                          nextInt(tokenizer, errorMsg));
    }



    //
    // The following methods are invoked from startElement/stopElement
    //

    private void startStyle(AttributeList attributes) throws SAXException {
        String id = null;

        _style = null;
        for(int i = attributes.getLength() - 1; i >= 0; i--) {
            String key = attributes.getName(i);
            if (key.equals(ATTRIBUTE_CLONE)) {
                _style = (ParsedSynthStyle)((ParsedSynthStyle)lookup(
                         attributes.getValue(i), ParsedSynthStyle.class)).
                         clone();
            }
            else if (key.equals(ATTRIBUTE_ID)) {
                id = attributes.getValue(i);
            }
        }
        if (_style == null) {
            _style = new ParsedSynthStyle();
        }
        register(id, _style);
    }

    private void endStyle() throws SAXException {
        int size = _stylePainters.size();
        if (size > 0) {
            _style.setPainters((ParsedSynthStyle.PainterInfo[])
                  _stylePainters.toArray(new ParsedSynthStyle.
                  PainterInfo[size]));
            _stylePainters.clear();
        }
        size = _stateInfos.size();
        if (size > 0) {
            _style.setStateInfo((ParsedSynthStyle.StateInfo[])_stateInfos.
                 toArray(new ParsedSynthStyle.StateInfo[size]));
            _stateInfos.clear();
        }
        _style = null;
    }

    private void startState(AttributeList attributes) throws SAXException {
        ParsedSynthStyle.StateInfo stateInfo = null;
        int state = 0;
        String id = null;

        _stateInfo = null;
        for(int i = attributes.getLength() - 1; i >= 0; i--) {
            String key = attributes.getName(i);
            if (key.equals(ATTRIBUTE_ID)) {
                id = attributes.getValue(i);
            }
            else if (key.equals(ATTRIBUTE_IDREF)) {
                _stateInfo = (ParsedSynthStyle.StateInfo)lookup(
                   attributes.getValue(i), ParsedSynthStyle.StateInfo.class);
            }
            else if (key.equals(ATTRIBUTE_CLONE)) {
                _stateInfo = (ParsedSynthStyle.StateInfo)((ParsedSynthStyle.
                             StateInfo)lookup(attributes.getValue(i),
                             ParsedSynthStyle.StateInfo.class)).clone();
            }
            else if (key.equals(ATTRIBUTE_VALUE)) {
                StringTokenizer tokenizer = new StringTokenizer(
                                   attributes.getValue(i));
                while (tokenizer.hasMoreTokens()) {
                    String stateString = tokenizer.nextToken().toUpperCase().
                                                   intern();
                    if (stateString == "ENABLED") {
                        state |= SynthConstants.ENABLED;
                    }
                    else if (stateString == "MOUSE_OVER") {
                        state |= SynthConstants.MOUSE_OVER;
                    }
                    else if (stateString == "PRESSED") {
                        state |= SynthConstants.PRESSED;
                    }
                    else if (stateString == "DISABLED") {
                        state |= SynthConstants.DISABLED;
                    }
                    else if (stateString == "FOCUSED") {
                        state |= SynthConstants.FOCUSED;
                    }
                    else if (stateString == "SELECTED") {
                        state |= SynthConstants.SELECTED;
                    }
                    else if (stateString == "DEFAULT") {
                        state |= SynthConstants.DEFAULT;
                    }
                    else if (stateString != "AND") {
                        throw new SAXException("Unknown state: " + state);
                    }
                }
            }
        }
        if (_stateInfo == null) {
            _stateInfo = new ParsedSynthStyle.StateInfo();
        }
        _stateInfo.setComponentState(state);
        register(id, _stateInfo);
        _stateInfos.add(_stateInfo);
    }

    private void endState() throws SAXException {
        int size = _statePainters.size();
        if (size > 0) {
            _stateInfo.setPainters((ParsedSynthStyle.PainterInfo[])
                  _statePainters.toArray(new ParsedSynthStyle.
                  PainterInfo[size]));
            _statePainters.clear();
        }
        _stateInfo = null;
    }

    private void startFont(AttributeList attributes) throws SAXException {
        Font font = null;
        int style = Font.PLAIN;
        int size = 0;
        String id = null;
        String name = null;

        for(int i = attributes.getLength() - 1; i >= 0; i--) {
            String key = attributes.getName(i);
            if (key.equals(ATTRIBUTE_ID)) {
                id = attributes.getValue(i);
            }
            else if (key.equals(ATTRIBUTE_IDREF)) {
                font = (Font)lookup(attributes.getValue(i), Font.class);
            }
            else if (key.equals(ATTRIBUTE_NAME)) {
                name = attributes.getValue(i);
            }
            else if (key.equals(ATTRIBUTE_SIZE)) {
                try {
                    size = Integer.parseInt(attributes.getValue(i));
                } catch (NumberFormatException nfe) {
                    throw new SAXException("Invalid font size: " +
                                           attributes.getValue(i));
                }
            }
            else if (key.equals(ATTRIBUTE_STYLE)) {
                StringTokenizer tok = new StringTokenizer(
                                                attributes.getValue(i));
                while (tok.hasMoreTokens()) {
                    String token = tok.nextToken().intern();
                    if (token == "BOLD") {
                        style = ((style | Font.PLAIN) ^ Font.PLAIN) |
                                Font.BOLD;
                    }
                    else if (token == "ITALIC") {
                        style |= Font.ITALIC;
                    }
                }
            }
        }
        if (font == null) {
            if (name == null) {
                throw new SAXException("You must define a name for the font");
            }
            if (size == 0) {
                throw new SAXException("You must define a size for the font");
            }
            font = new FontUIResource(name, style, size);
        }
        else if (name != null || size != 0 || style != Font.PLAIN) {
            throw new SAXException("Name, size and style are not for use " +
                                   "with idref");
        }
        register(id, font);
        if (_stateInfo != null) {
            _stateInfo.setFont(font);
        }
        else if (_style != null) {
            _style.setFont(font);
        }
    }

    private void startColor(AttributeList attributes) throws SAXException {
        Color color = null;
        String id = null;

        _colorTypes.clear();
        for(int i = attributes.getLength() - 1; i >= 0; i--) {
            String key = attributes.getName(i);
            if (key.equals(ATTRIBUTE_ID)) {
                id = attributes.getValue(i);
            }
            else if (key.equals(ATTRIBUTE_IDREF)) {
                color = (Color)lookup(attributes.getValue(i), Color.class);
            }
            else if (key.equals(ATTRIBUTE_NAME)) {
            }
            else if (key.equals(ATTRIBUTE_VALUE)) {
                String value = attributes.getValue(i);

                if (value.startsWith("#")) {
                    try {
                        int argb;
                        boolean hasAlpha;

                        int length = value.length();
                        if (length < 8) {
                            // Just RGB, or some portion of it.
                            argb = Integer.decode(value);
                            hasAlpha = false;
                        } else if (length == 8) {
                            // Single character alpha: #ARRGGBB.
                            argb = Integer.decode(value);
                            hasAlpha = true;
                        } else if (length == 9) {
                            // Color has alpha and is of the form
                            // #AARRGGBB.
                            // The following split decoding is mandatory due to
                            // Integer.decode() behavior which won't decode
                            // hexadecimal values higher than #7FFFFFFF.
                            // Thus, when an alpha channel is detected, it is
                            // decoded separately from the RGB channels.
                            int rgb = Integer.decode('#' +
                                                     value.substring(3, 9));
                            int a = Integer.decode(value.substring(0, 3));
                            argb = (a << 24) | rgb;
                            hasAlpha = true;
                        } else {
                            throw new SAXException("Invalid Color value: "
                                + value);
                        }

                        color = new ColorUIResource(new Color(argb, hasAlpha));
                    } catch (NumberFormatException nfe) {
                        throw new SAXException("Invalid Color value: " +value);
                    }
                }
                else {
                    try {
                        color = new ColorUIResource((Color)Color.class.
                              getField(value.toUpperCase()).get(Color.class));
                    } catch (NoSuchFieldException nsfe) {
                        throw new SAXException("Invalid color name: " + value);
                    } catch (IllegalAccessException iae) {
                        throw new SAXException("Invalid color name: " + value);
                    }
                }
            }
            else if (key.equals(ATTRIBUTE_TYPE)) {
                StringTokenizer tokenizer = new StringTokenizer(
                                   attributes.getValue(i));
                while (tokenizer.hasMoreTokens()) {
                    String typeName = tokenizer.nextToken();
                    int classIndex = typeName.lastIndexOf('.');
                    Class typeClass;

                    if (classIndex == -1) {
                        typeClass = ColorType.class;
                        classIndex = 0;
                    }
                    else {
                        try {
                            typeClass = Class.forName(typeName.substring(
                                                      0, classIndex));
                        } catch (ClassNotFoundException cnfe) {
                            throw new SAXException("Unknown class: " +
                                      typeName.substring(0, classIndex));
                        }
                        classIndex++;
                    }
                    try {
                        _colorTypes.add((ColorType)checkCast(typeClass.
                              getField(typeName.substring(classIndex,
                              typeName.length() - classIndex)).
                              get(typeClass), ColorType.class));
                    } catch (NoSuchFieldException nsfe) {
                        throw new SAXException("Unable to find color type: " +
                                               typeName);
                    } catch (IllegalAccessException iae) {
                        throw new SAXException("Unable to find color type: " +
                                               typeName);
                    }
                }
            }
        }
        if (color == null) {
            throw new SAXException("color: you must specificy a value");
        }
        register(id, color);
        if (_stateInfo != null && _colorTypes.size() > 0) {
            Color[] colors = _stateInfo.getColors();
            int max = 0;
            for (int counter = _colorTypes.size() - 1; counter >= 0;
                     counter--) {
                max = Math.max(max, ((ColorType)_colorTypes.get(counter)).
                               getID());
            }
            if (colors == null || colors.length <= max) {
                Color[] newColors = new Color[max + 1];
                if (colors != null) {
                    System.arraycopy(colors, 0, newColors, 0, colors.length);
                }
                colors = newColors;
            }
            for (int counter = _colorTypes.size() - 1; counter >= 0;
                     counter--) {
                colors[((ColorType)_colorTypes.get(counter)).getID()] = color;
            }
            _stateInfo.setColors(colors);
        }
    }

    private void startProperty(AttributeList attributes,
                               Object property) throws SAXException {
        Object value = null;
        Object key = null;
        // Type of the value: 0=idref, 1=boolean, 2=dimension, 3=insets,
        // 4=integer,5=string
        int iType = 0;
        String aValue = null;

        for(int i = attributes.getLength() - 1; i >= 0; i--) {
            String aName = attributes.getName(i);
            if (aName.equals(ATTRIBUTE_TYPE)) {
                String type = attributes.getValue(i).toUpperCase();
                if (type.equals("IDREF")) {
                    iType = 0;
                }
                else if (type.equals("BOOLEAN")) {
                    iType = 1;
                }
                else if (type.equals("DIMENSION")) {
                    iType = 2;
                }
                else if (type.equals("INSETS")) {
                    iType = 3;
                }
                else if (type.equals("INTEGER")) {
                    iType = 4;
                }
                else if (type.equals("STRING")) {
                    iType = 5;
                }
                else {
                    throw new SAXException(property + " unknown type, use" +
                        "idref, boolean, dimension, insets or integer");
                }
            }
            else if (aName.equals(ATTRIBUTE_VALUE)) {
                aValue = attributes.getValue(i);
            }
            else if (aName.equals(ATTRIBUTE_KEY)) {
                key = attributes.getValue(i);
            }
        }
        if (aValue != null) {
            switch (iType) {
            case 0: // idref
                value = lookup(aValue, Object.class);
                break;
            case 1: // boolean
                if (aValue.toUpperCase().equals("TRUE")) {
                    value = Boolean.TRUE;
                }
                else {
                    value = Boolean.FALSE;
                }
                break;
            case 2: // dimension
                StringTokenizer tok = new StringTokenizer(aValue);
                value = new DimensionUIResource(
                    nextInt(tok, "Invalid dimension"),
                    nextInt(tok, "Invalid dimension"));
                break;
            case 3: // insets
                value = parseInsets(aValue, property + " invalid insets");
                break;
            case 4: // integer
                try {
                    value = new Integer(Integer.parseInt(aValue));
                } catch (NumberFormatException nfe) {
                    throw new SAXException(property + " invalid value");
                }
                break;
            case 5: //string
                value = aValue;
                break;
            }
        }
        if (value == null || key == null) {
            throw new SAXException(property + ": you must supply a " +
                                   "key and value");
        }
        if (property == ELEMENT_DEFAULTS_PROPERTY) {
            _defaultsMap.put(key, value);
        }
        else if (_stateInfo != null) {
            if (_stateInfo.getData() == null) {
                _stateInfo.setData(new HashMap());
            }
            _stateInfo.getData().put(key, value);
        }
        else if (_style != null) {
            if (_style.getData() == null) {
                _style.setData(new HashMap());
            }
            _style.getData().put(key, value);
        }
    }

    private void startGraphics(AttributeList attributes) throws SAXException {
        SynthGraphicsUtils graphics = null;

        for(int i = attributes.getLength() - 1; i >= 0; i--) {
            String key = attributes.getName(i);
            if (key.equals(ATTRIBUTE_IDREF)) {
                graphics = (SynthGraphicsUtils)lookup(attributes.getValue(i),
                                                 SynthGraphicsUtils.class);
            }
        }
        if (graphics == null) {
            throw new SAXException("graphicsUtils: you must supply an idref");
        }
        if (_style != null) {
            _style.setGraphicsUtils(graphics);
        }
    }

    private void startInsets(AttributeList attributes) throws SAXException {
        int top = 0;
        int bottom = 0;
        int left = 0;
        int right = 0;
        Insets insets = null;
        String id = null;

        for(int i = attributes.getLength() - 1; i >= 0; i--) {
            String key = attributes.getName(i);

            try {
                if (key.equals(ATTRIBUTE_IDREF)) {
                    insets = (Insets)lookup(attributes.getValue(i),
                                                   Insets.class);
                }
                else if (key.equals(ATTRIBUTE_ID)) {
                    id = attributes.getValue(i);
                }
                else if (key.equals(ATTRIBUTE_TOP)) {
                    top = Integer.parseInt(attributes.getValue(i));
                }
                else if (key.equals(ATTRIBUTE_LEFT)) {
                    left = Integer.parseInt(attributes.getValue(i));
                }
                else if (key.equals(ATTRIBUTE_BOTTOM)) {
                    bottom = Integer.parseInt(attributes.getValue(i));
                }
                else if (key.equals(ATTRIBUTE_RIGHT)) {
                    right = Integer.parseInt(attributes.getValue(i));
                }
            } catch (NumberFormatException nfe) {
                throw new SAXException("insets: bad integer value for " +
                                       attributes.getValue(i));
            }
        }
        if (insets == null) {
            insets = new InsetsUIResource(top, left, bottom, right);
        }
        register(id, insets);
        if (_style != null) {
            _style.setInsets(insets);
        }
    }

    private void startBind(AttributeList attributes) throws SAXException {
        ParsedSynthStyle style = null;
        String path = null;
        int type = -1;

        for(int i = attributes.getLength() - 1; i >= 0; i--) {
            String key = attributes.getName(i);

            if (key.equals(ATTRIBUTE_STYLE)) {
                style = (ParsedSynthStyle)lookup(attributes.getValue(i),
                                                  ParsedSynthStyle.class);
            }
            else if (key.equals(ATTRIBUTE_TYPE)) {
                String typeS = attributes.getValue(i).toUpperCase();

                if (typeS.equals("NAME")) {
                    type = DefaultSynthStyleFactory.NAME;
                }
                else if (typeS.equals("REGION")) {
                    type = DefaultSynthStyleFactory.REGION;
                }
                else {
                    throw new SAXException("bind: unknown type " + typeS);
                }
            }
            else if (key.equals(ATTRIBUTE_KEY)) {
                path = attributes.getValue(i);
            }
        }
        if (style == null || path == null || type == -1) {
            throw new SAXException("bind: you must specify a style, type " +
                                   "and key");
        }
        try {
            _factory.addStyle(style, path, type);
        } catch (PatternSyntaxException pse) {
            throw new SAXException("bind: " + path + " is not a valid " +
                                   "regular expression");
        }
    }

    private void startPainter(AttributeList attributes, String type) throws SAXException {
        Insets sourceInsets = null;
        Insets destInsets = null;
        String path = null;
        boolean paintCenter = true;
        boolean stretch = true;
        SynthPainter painter = null;
        String method = null;
        String id = null;
        int direction = -1;
        boolean center = false;

        boolean stretchSpecified = false;
        boolean paintCenterSpecified = false;

        for(int i = attributes.getLength() - 1; i >= 0; i--) {
            String key = attributes.getName(i);
            String value = attributes.getValue(i);

            if (key.equals(ATTRIBUTE_ID)) {
                id = value;
            }
            else if (key.equals(ATTRIBUTE_METHOD)) {
                method = value.toLowerCase(Locale.ENGLISH);
            }
            else if (key.equals(ATTRIBUTE_IDREF)) {
                painter = (SynthPainter)lookup(value, SynthPainter.class);
            }
            else if (key.equals(ATTRIBUTE_PATH)) {
                path = value;
            }
            else if (key.equals(ATTRIBUTE_SOURCE_INSETS)) {
                sourceInsets = parseInsets(value, type +
                   ": sourceInsets must be top left bottom right");
            }
            else if (key.equals(ATTRIBUTE_DEST_INSETS)) {
                destInsets = parseInsets(value, type +
                  ": destinationInsets must be top left bottom right");
            }
            else if (key.equals(ATTRIBUTE_PAINT_CENTER)) {
                paintCenter = value.toLowerCase().equals("true");
                paintCenterSpecified = true;
            }
            else if (key.equals(ATTRIBUTE_STRETCH)) {
                stretch = value.toLowerCase().equals("true");
                stretchSpecified = true;
            }
            else if (key.equals(ATTRIBUTE_DIRECTION)) {
                value = value.toUpperCase().intern();
                if (value == "EAST") {
                    direction = SwingConstants.EAST;
                }
                else if (value == "NORTH") {
                    direction = SwingConstants.NORTH;
                }
                else if (value == "SOUTH") {
                    direction = SwingConstants.SOUTH;
                }
                else if (value == "WEST") {
                    direction = SwingConstants.WEST;
                }
                else if (value == "TOP") {
                    direction = SwingConstants.TOP;
                }
                else if (value == "LEFT") {
                    direction = SwingConstants.LEFT;
                }
                else if (value == "BOTTOM") {
                    direction = SwingConstants.BOTTOM;
                }
                else if (value == "RIGHT") {
                    direction = SwingConstants.RIGHT;
                }
                else if (value == "HORIZONTAL") {
                    direction = SwingConstants.HORIZONTAL;
                }
                else if (value == "VERTICAL") {
                    direction = SwingConstants.VERTICAL;
                }
                else if (value == "HORIZONTAL_SPLIT") {
                    direction = JSplitPane.HORIZONTAL_SPLIT;
                }
                else if (value == "VERTICAL_SPLIT") {
                    direction = JSplitPane.VERTICAL_SPLIT;
                }
                else {
                    throw new SAXException(type + ": unknown direction");
                }
            }
            else if (key.equals(ATTRIBUTE_CENTER)) {
                center = value.toLowerCase().equals("true");
            }
        }
        if (painter == null) {
            if (type == ELEMENT_PAINTER) {
                throw new SAXException(type +
                             ": you must specify an idref");
            }
            if (sourceInsets == null && !center) {
                throw new SAXException(
                             "property: you must specify sourceInsets");
            }
            if (path == null) {
                throw new SAXException("property: you must specify a path");
            }
            if (center && (sourceInsets != null || destInsets != null ||
                           paintCenterSpecified || stretchSpecified)) {
                throw new SAXException("The attributes: sourceInsets, " +
                                       "destinationInsets, paintCenter and stretch " +
                                       " are not legal when center is true");
            }
            painter = new ImagePainter(!stretch, paintCenter,
                     sourceInsets, destInsets, getResource(path), center);
        }
        register(id, painter);
        if (_stateInfo != null) {
            addPainterOrMerge(_statePainters, method, painter, direction);
        }
        else if (_style != null) {
            addPainterOrMerge(_stylePainters, method, painter, direction);
        }
    }

    private void addPainterOrMerge(java.util.List painters, String method,
                                   SynthPainter painter, int direction) {
        ParsedSynthStyle.PainterInfo painterInfo;
        painterInfo = new ParsedSynthStyle.PainterInfo(method,
                                                       painter,
                                                       direction);

        for (Object infoObject: painters) {
            ParsedSynthStyle.PainterInfo info;
            info = (ParsedSynthStyle.PainterInfo) infoObject;

            if (painterInfo.equalsPainter(info)) {
                info.addPainter(painter);
                return;
            }
        }

        painters.add(painterInfo);
    }

    private void startImageIcon(AttributeList attributes) throws SAXException {
        String path = null;
        String id = null;

        for(int i = attributes.getLength() - 1; i >= 0; i--) {
            String key = attributes.getName(i);

            if (key.equals(ATTRIBUTE_ID)) {
                id = attributes.getValue(i);
            }
            else if (key.equals(ATTRIBUTE_PATH)) {
                path = attributes.getValue(i);
            }
        }
        if (path == null) {
            throw new SAXException("imageIcon: you must specify a path");
        }
        register(id, new LazyImageIcon(getResource(path)));
       }

    private void startOpaque(AttributeList attributes) throws
                      SAXException {
        if (_style != null) {
            _style.setOpaque(true);
            for(int i = attributes.getLength() - 1; i >= 0; i--) {
                String key = attributes.getName(i);

                if (key.equals(ATTRIBUTE_VALUE)) {
                    _style.setOpaque("true".equals(attributes.getValue(i).
                                                   toLowerCase()));
                }
            }
        }
    }

    private void startInputMap(AttributeList attributes) throws SAXException {
        _inputMapBindings.clear();
        _inputMapID = null;
        if (_style != null) {
            for(int i = attributes.getLength() - 1; i >= 0; i--) {
                String key = attributes.getName(i);

                if (key.equals(ATTRIBUTE_ID)) {
                    _inputMapID = attributes.getValue(i);
                }
            }
        }
    }

    private void endInputMap() throws SAXException {
        if (_inputMapID != null) {
            register(_inputMapID, new UIDefaults.LazyInputMap(
                     _inputMapBindings.toArray(new Object[_inputMapBindings.
                     size()])));
        }
        _inputMapBindings.clear();
        _inputMapID = null;
    }

    private void startBindKey(AttributeList attributes) throws SAXException {
        if (_inputMapID == null) {
            // Not in an inputmap, bail.
            return;
        }
        if (_style != null) {
            String key = null;
            String value = null;
            for(int i = attributes.getLength() - 1; i >= 0; i--) {
                String aKey = attributes.getName(i);

                if (aKey.equals(ATTRIBUTE_KEY)) {
                    key = attributes.getValue(i);
                }
                else if (aKey.equals(ATTRIBUTE_ACTION)) {
                    value = attributes.getValue(i);
                }
            }
            if (key == null || value == null) {
                throw new SAXException(
                    "bindKey: you must supply a key and action");
            }
            _inputMapBindings.add(key);
            _inputMapBindings.add(value);
        }
    }

    //
    // SAX methods, these forward to the ObjectHandler if we don't know
    // the element name.
    //

    public InputSource resolveEntity(String publicId, String systemId)
                              throws SAXException {
        if (isForwarding()) {
            return getHandler().resolveEntity(publicId, systemId);
        }
        return null;
    }

    public void notationDecl(String name, String publicId, String systemId) {
        if (isForwarding()) {
            getHandler().notationDecl(name, publicId, systemId);
        }
    }

    public void unparsedEntityDecl(String name, String publicId,
                                   String systemId, String notationName) {
        if (isForwarding()) {
            getHandler().unparsedEntityDecl(name, publicId, systemId,
                                            notationName);
        }
    }

    public void setDocumentLocator(Locator locator) {
        if (isForwarding()) {
            getHandler().setDocumentLocator(locator);
        }
    }

    public void startDocument() throws SAXException {
        if (isForwarding()) {
            getHandler().startDocument();
        }
    }

    public void endDocument() throws SAXException {
        if (isForwarding()) {
            getHandler().endDocument();
        }
    }

    public void startElement(String name, AttributeList attributes)
                     throws SAXException {
        name = name.intern();
        if (name == ELEMENT_STYLE) {
            startStyle(attributes);
        }
        else if (name == ELEMENT_STATE) {
            startState(attributes);
        }
        else if (name == ELEMENT_FONT) {
            startFont(attributes);
        }
        else if (name == ELEMENT_COLOR) {
            startColor(attributes);
        }
        else if (name == ELEMENT_PAINTER) {
            startPainter(attributes, name);
        }
        else if (name == ELEMENT_IMAGE_PAINTER) {
            startPainter(attributes, name);
        }
        else if (name == ELEMENT_PROPERTY) {
            startProperty(attributes, ELEMENT_PROPERTY);
        }
        else if (name == ELEMENT_DEFAULTS_PROPERTY) {
            startProperty(attributes, ELEMENT_DEFAULTS_PROPERTY);
        }
        else if (name == ELEMENT_SYNTH_GRAPHICS) {
            startGraphics(attributes);
        }
        else if (name == ELEMENT_INSETS) {
            startInsets(attributes);
        }
        else if (name == ELEMENT_BIND) {
            startBind(attributes);
        }
        else if (name == ELEMENT_BIND_KEY) {
            startBindKey(attributes);
        }
        else if (name == ELEMENT_IMAGE_ICON) {
            startImageIcon(attributes);
        }
        else if (name == ELEMENT_OPAQUE) {
            startOpaque(attributes);
        }
        else if (name == ELEMENT_INPUT_MAP) {
            startInputMap(attributes);
        }
        else if (name != ELEMENT_SYNTH) {
            if (_depth++ == 0) {
                getHandler().reset();
            }
            getHandler().startElement(name, attributes);
        }
    }

    public void endElement(String name) throws SAXException {
        if (isForwarding()) {
            getHandler().endElement(name);
            _depth--;
            if (!isForwarding()) {
                getHandler().reset();
            }
        }
        else {
            name = name.intern();
            if (name == ELEMENT_STYLE) {
                endStyle();
            }
            else if (name == ELEMENT_STATE) {
                endState();
            }
            else if (name == ELEMENT_INPUT_MAP) {
                endInputMap();
            }
        }
    }

    public void characters(char ch[], int start, int length)
                           throws SAXException {
        if (isForwarding()) {
            getHandler().characters(ch, start, length);
        }
    }

    public void ignorableWhitespace (char ch[], int start, int length)
        throws SAXException {
        if (isForwarding()) {
            getHandler().ignorableWhitespace(ch, start, length);
        }
    }

    public void processingInstruction(String target, String data)
                                     throws SAXException {
        if (isForwarding()) {
            getHandler().processingInstruction(target, data);
        }
    }

    public void warning(SAXParseException e) throws SAXException {
        if (isForwarding()) {
            getHandler().warning(e);
        }
    }

    public void error(SAXParseException e) throws SAXException {
        if (isForwarding()) {
            getHandler().error(e);
        }
    }


    public void fatalError(SAXParseException e) throws SAXException {
        if (isForwarding()) {
            getHandler().fatalError(e);
        }
        throw e;
    }


    /**
     * ImageIcon that lazily loads the image until needed.
     */
    private static class LazyImageIcon extends ImageIcon implements UIResource {
        private URL location;

        public LazyImageIcon(URL location) {
            super();
            this.location = location;
        }

        public void paintIcon(Component c, Graphics g, int x, int y) {
            if (getImage() != null) {
                super.paintIcon(c, g, x, y);
            }
        }

        public int getIconWidth() {
            if (getImage() != null) {
                return super.getIconWidth();
            }
            return 0;
        }

        public int getIconHeight() {
            if (getImage() != null) {
                return super.getIconHeight();
            }
            return 0;
        }

        public Image getImage() {
            if (location != null) {
                setImage(Toolkit.getDefaultToolkit().getImage(location));
                location = null;
            }
            return super.getImage();
        }
    }
}
