
package uk.co.wingpath.xml;

import java.io.*;
import java.util.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
import javax.xml.parsers.*;

import uk.co.wingpath.util.*;

/**
* This class provides classes and methods for reading and writing
* XML-formatted files.
*<p>Note that namespaces are not supported.
*/
public class Xml
{
    private Xml () {}

    /**
    * This interface should be implemented by classes that want to receive
    * values read from an XML-formatted file.
    */
    public interface Receiver<T>
    {
        /**
        * Receives a value read from an XML-formatted file.
        * @param value the value that has been read.
        * @throws ValueException if the value is unacceptable.
        */
        void receive (T value)
            throws ValueException;
    }

    /**
    * This interface should be implemented by classes that want to load
    * elements from an XML-formatted file.
    */
    public interface Loader
    {
        /**
        * Called when a start tag for a nested XML element is encountered.
        * @param tag the start tag.
        * @return a loader to load the nested element. If {@code null} is
        * returned, the nested element will be skipped.
        * @throws ValueException if the tag is not allowed.
        */
        Loader startChild (String tag)
            throws ValueException;

        /**
        * Called when an attribute is encountered.
        * @param key the attribute key.
        * @param value the attribute value.
        * @throws ValueException if the attribute is unacceptable.
        */
        void attribute (String key, String value)
            throws ValueException;

        /**
        * Called when the end tag of the element is encountered.
        * @param value the trimmed concatenation of any characters
        * found between the start and end tags, excluding any nested
        * elements.
        * @throws ValueException if the value is unacceptable.
        */
        void end (String value)
            throws ValueException;

        /**
        * Called when loading of the element has finished.
        * <p>This method is called regardless of whether the termination is
        * normal or due to a checked exception (so it's used like a 
        * {@code finally} clause).
        * If the termination is normal, the {@code cleanup} method is called
        * immediately after the {@code end} method.
        */
        void cleanup ();
    }

    /**
    * This class provides a default implementation of the Loader interface.
    */
    public abstract static class AbstractLoader
        implements Loader
    {
        /**
        * Called when a start tag for a nested XML element is encountered.
        * <p>This implementation returns {@code null}, so the nested
        * element will be skipped.
        * @param tag the start tag.
        * @return {@code null}.
        */
        public Loader startChild (String tag)
            throws ValueException
        {
            return null;
        }

        /**
        * Called when an attribute is encountered.
        * <p>This implementation does nothing.
        * @param key the attribute key.
        * @param value the attribute value.
        */
        public void attribute (String key, String value)
            throws ValueException
        {
        }

        /**
        * Called when the end tag of the element is encountered.
        * <p>This implementation throws a {@code ValueException} if the value
        * is not an empty string.
        * @param value the trimmed concatenation of any characters
        * found between the start and end tags, excluding any nested
        * elements.
        * @throws ValueException if the value is unacceptable.
        */
        public void end (String value)
            throws ValueException
        {
            if (!value.equals (""))
                throw new ValueException (value + ": Value not allowed");
        }

        /**
        * Called when loading of the element has finished.
        * <p>This method is called regardless of whether the termination is
        * normal or due to a checked exception (so it's used like a 
        * {@code finally} clause).
        * If the termination is normal, the {@code cleanup} method is called
        * immediately after the {@code end} method.
        */
        public void cleanup ()
        {
        }
    }

    /**
    * This implementation of the {@code Loader} interface loads an element
    * that has a string value and no nested elements.
    */
    public static class StringLoader
        extends AbstractLoader
    {
        private Receiver<String> receiver;
        private String [] values;

        /**
        * Constructs a {@code StringLoader}.
        * <p>The loaded value is passed to the supplied receiver to be
        * processed/stored.
        * @param receiver the receiver that will receive the value.
        */
        public StringLoader (Receiver<String> receiver)
        {
            this (null, receiver);
        }

        /**
        * Constructs a {@code StringLoader} that will only accept values
        * from a specified list of values.
        * <p>If the value is in the list of acceptable values, it is passed
        * to the supplied receiver to be processed/stored.
        * @param values the acceptable values.
        * @param receiver the receiver that will receive the value.
        */
        public StringLoader (String [] values, Receiver<String> receiver)
        {
            this.values = values;
            this.receiver = receiver;
        }

        /**
        * Called when the end tag of the element is encountered.
        * <p>Checks that the value is in the list of acceptable values
        * (if supplied), and call the receiver to receive the value.
        * @param value the trimmed concatenation of any characters
        * found between the start and end tags, excluding any nested
        * elements.
        * @throws ValueException if the value is not in the list of acceptable
        * values (if supplied), or if the receiver throws a
        * {@code ValueException}..
        */
        public void end (String value)
            throws ValueException
        {
            if (values != null)
            {
                for (String v : values)
                {
                    if (v.equalsIgnoreCase (value))
                    {
                        receiver.receive (v);
                        return;
                    }
                }

                throw new ValueException (value + ": Invalid value");
            }
            else
            {
                receiver.receive (value);
            }
        }
    }

    /**
    * This implementation of the {@code Loader} interface loads an element
    * that has an enum value and no nested elements.
    */
    public static class EnumLoader<T extends Enum<T>>
        extends AbstractLoader
    {
        private Class<T> cls;
        private Receiver<T> receiver;

        /**
        * Constructs an {@code EnumLoader}.
        * @param cls the enum class.
        * @param receiver the receiver that will receive the value.
        */
        public EnumLoader (Class<T> cls, Receiver<T> receiver)
        {
            this.cls = cls;
            this.receiver = receiver;
        }

        /**
        * Called when the end tag of the element is encountered.
        * <p>Checks that the value is an acceptable value,
        * and calls the receiver with the value.
        * @param value the trimmed concatenation of any characters
        * found between the start and end tags, excluding any nested
        * elements.
        * @throws ValueException if the value is not acceptable,
        * or if the receiver throws a {@code ValueException}..
        */
        public void end (String value)
            throws ValueException
        {
            try
            {
                receiver.receive (Enum.valueOf (cls, value));
            }
            catch (IllegalArgumentException e)
            {
                throw new ValueException (value + ": Invalid value");
            }
        }
    }

    /**
    * This implementation of the {@code Loader} interface loads an element
    * that has an integer value and no nested elements.
    */
    public static class IntegerLoader
        extends AbstractLoader
    {
        private final Receiver<Integer> receiver;
        private final int minValue;
        private final int maxValue;
        private final int radix;

        /**
        * Constructs an {@code IntegerLoader} with the specified range of
        * acceptable values.
        * @param minValue the smallest acceptable value.
        * @param maxValue the largest acceptable value.
        * @param radix the radix to be used for converting the value.
        * @param receiver the receiver that will receive the value.
        */
        public IntegerLoader (int minValue, int maxValue, int radix,
            Receiver<Integer> receiver)
        {
            this.receiver = receiver;
            this.minValue = minValue;
            this.maxValue = maxValue;
            this.radix = radix;
        }

        /**
        * Constructs an {@code IntegerLoader} with the specified range of
        * acceptable values.
        * The value is converted using a radix of 10.
        * @param minValue the smallest acceptable value.
        * @param maxValue the largest acceptable value.
        * @param receiver the receiver that will receive the value.
        */
        public IntegerLoader (int minValue, int maxValue,
            Receiver<Integer> receiver)
        {
            this (minValue, maxValue, 10, receiver);
        }

        /**
        * Constructs an {@code IntegerLoader} that will accept any integer.
        * The value is converted using a radix of 10.
        * @param receiver the receiver that will receive the value.
        */
        public IntegerLoader (Receiver<Integer> receiver)
        {
            this (Integer.MIN_VALUE, Integer.MAX_VALUE, 10, receiver);
        }

        /**
        * Called when the end tag of the element is encountered.
        * Parses the value as a decimal integer, checks that it is in
        * the acceptable range (if supplied), and passes the integral value
        * to the receiver.
        * @param value the trimmed concatenation of any characters
        * found between the start and end tags, excluding any nested
        * elements.
        * @throws ValueException if the value cannot be parsed as an integer,
        * or the value is not in the acceptable range, or if the receiver
        * throws a {@code ValueException}..
        */
        public void end (String value)
            throws ValueException
        {
            try
            {
                long ival = Long.parseLong (value, radix);
                if (ival < minValue || ival > maxValue)
                {
                    throw new ValueException (value +
                        ": Value must be in the range " +
                        minValue + " to " + maxValue);
                }
                receiver.receive ((int) ival);
            }
            catch (NumberFormatException e)
            {
                throw new ValueException (value +
                    ": Value must be a number in the range "
                    + minValue + " to " + maxValue);
            }
        }
    }

    /**
    * This implementation of the {@code Loader} interface loads an element
    * that has a double value and no nested elements.
    */
    public static class DoubleLoader
        extends AbstractLoader
    {
        private final Receiver<Double> receiver;

        /**
        * Constructs an {@code DoubleLoader}.
        * @param receiver the receiver that will receive the value.
        */
        public DoubleLoader (Receiver<Double> receiver)
        {
            this.receiver = receiver;
        }

        /**
        * Called when the end tag of the element is encountered.
        * Parses the value as a double, and passes the double value
        * to the receiver.
        * @param value the trimmed concatenation of any characters
        * found between the start and end tags, excluding any nested
        * elements.
        * @throws ValueException if the value cannot be parsed as a double,
        * or if the receiver throws a {@code ValueException}..
        */
        public void end (String value)
            throws ValueException
        {
            try
            {
                Double dval = Double.parseDouble (value);
                receiver.receive (dval);
            }
            catch (NumberFormatException e)
            {
                throw new ValueException (value + ": Number required");
            }
        }
    }

    /**
    * This implementation of the {@code Loader} interface loads an element
    * that has a boolean value and no nested elements.
    */
    public static class BooleanLoader
        extends AbstractLoader
    {
        private final Receiver<Boolean> receiver;

        /**
        * Constructs an {@code BooleanLoader}.
        * @param receiver the receiver that will receive the value.
        */
        public BooleanLoader (Receiver<Boolean> receiver)
        {
            this.receiver = receiver;
        }

        /**
        * Called when the end tag of the element is encountered.
        * Parses the value as a boolean, and passes the boolean value
        * to the receiver.
        * @param value the trimmed concatenation of any characters
        * found between the start and end tags, excluding any nested
        * elements.
        * @throws ValueException if the value cannot be parsed as a boolean,
        * or if the receiver throws a {@code ValueException}..
        */
        public void end (String value)
            throws ValueException
        {
            Boolean bval = Boolean.parseBoolean (value);
            receiver.receive (bval);
        }
    }

    // Some legacy XML files use hex to save signed values. The current
    // Numeric implementation is strict about values being in range, so we may
    // have to convert to an unsigned type first, and then to the signed type.
    private static Numeric.Value fromString (Numeric.Type type, String str,
            int radix)
        throws ValueException
    {
        try
        {
            return type.fromString (str, radix);
        }
        catch (ValueException e)
        {
            Numeric.Value value = type.unsigned ().fromString (str, radix);
            return type.createValue (value);
        }
    }

    /**
    * This implementation of the {@code Loader} interface loads an element
    * that has a numeric value, and no nested elements.
    */
    public static class NumericLoader
        extends AbstractLoader
    {
        private final Receiver<Numeric.Value> receiver;
        private final Numeric.Type type;
        private final int radix;
        private final boolean mayBeEmpty;

        /**
        * Constructs an {@code NumericLoader}.
        * @param type the type to be used for converting the value.
        * @param radix the radix to be used for converting the value.
        * @param mayBeEmpty whether the value may be empty.
        * @param receiver the receiver that will receive the value.
        */
        public NumericLoader (Numeric.Type type, int radix, boolean mayBeEmpty,
            Receiver<Numeric.Value> receiver)
        {
            this.type = type;
            this.radix = radix;
            this.mayBeEmpty = mayBeEmpty;
            this.receiver = receiver;
        }

        /**
        * Called when the end tag of the element is encountered.
        * Parses the value as a numeric value, and passes the
        * parsed value to the receiver.
        * @param value the trimmed concatenation of any characters
        * found between the start and end tags, excluding any nested
        * elements.
        * @throws ValueException if the value cannot be parsed as an array
        * of hex bytes, or if the receiver throws a {@code ValueException}..
        */
        public void end (String value)
            throws ValueException
        {
            if (!mayBeEmpty && value.equals (""))
                throw new ValueException ("Value missing");
            receiver.receive (fromString (type, value, radix));
        }
    }

    /**
    * This implementation of the {@code Loader} interface loads an element
    * that has a numeric pattern value, and no nested elements.
    */
    public static class NumericPatternLoader
        extends AbstractLoader
    {
        private final Receiver<Numeric.Pattern> receiver;
        private final Numeric.Type type;
        private final int radix;

        /**
        * Constructs an {@code NumericPatternLoader}.
        * @param type the type to be used for converting the value.
        * @param radix the radix to be used for converting the value.
        * @param receiver the receiver that will receive the value.
        */
        public NumericPatternLoader (Numeric.Type type, int radix,
            Receiver<Numeric.Pattern> receiver)
        {
            this.type = type;
            this.radix = radix;
            this.receiver = receiver;
        }

        /**
        * Called when the end tag of the element is encountered.
        * Parses the value as a numeric value, and passes the
        * parsed value to the receiver.
        * @param value the trimmed concatenation of any characters
        * found between the start and end tags, excluding any nested
        * elements.
        * @throws ValueException if the value cannot be parsed as a pattern,
        * or if the receiver throws a {@code ValueException}..
        */
        public void end (String value)
            throws ValueException
        {
            receiver.receive (type.createPattern (value, radix));
        }
    }

    /**
    * This implementation of the {@code Loader} interface loads an element
    * that has a sequence of numeric values as its value, and no nested
    * elements.
    */
    public static class NumericArrayLoader
        extends AbstractLoader
    {
        private final Receiver<Numeric.Value []> receiver;
        private final Numeric.Type type;
        private final int radix;
        private final boolean mayBeEmpty;

        /**
        * Constructs an {@code NumericArrayLoader}.
        * @param type the type to be used for converting the values.
        * @param radix the radix to be used for converting the values.
        * @param mayBeEmpty whether values may be empty.
        * @param receiver the receiver that will receive the value.
        */
        public NumericArrayLoader (Numeric.Type type, int radix,
            boolean mayBeEmpty, Receiver<Numeric.Value []> receiver)
        {
            this.type = type;
            this.radix = radix;
            this.mayBeEmpty = mayBeEmpty;
            this.receiver = receiver;
        }

        /**
        * Called when the end tag of the element is encountered.
        * Parses the value as a sequence of numeric values, and passes the
        * parsed value to the receiver.
        * @param value the trimmed concatenation of any characters
        * found between the start and end tags, excluding any nested
        * elements.
        * @throws ValueException if the value cannot be parsed as an array
        * of hex bytes, or if the receiver throws a {@code ValueException}..
        */
        public void end (String value)
            throws ValueException
        {
            if (value.equals ("!"))
            {
                receiver.receive (new Numeric.Value [0]);
                return;
            }
            String [] a = value.split ("\\s+", 0);
            Numeric.Value [] result = new Numeric.Value [a.length];

            for (int i = 0 ; i < a.length ; i++)
            {
                if (a [i].equals ("*"))
                {
                    if (!mayBeEmpty)
                        throw new ValueException ("Value missing");
                    result [i] = type.undef;
                }
                else
                {
                    result [i] = fromString (type, a [i], radix);
                }
            }

            receiver.receive (result);
        }
    }

    /**
    * This implementation of the {@code Loader} interface loads an element
    * that has a sequence of numeric patterns as its value, and no nested
    * elements.
    */
    public static class NumericPatternArrayLoader
        extends AbstractLoader
    {
        private final Receiver<Numeric.Pattern []> receiver;
        private final Numeric.Type type;
        private final int radix;

        /**
        * Constructs an {@code NumericArrayLoader}.
        * @param type the type to be used for converting the values.
        * @param radix the radix to be used for converting the values.
        * @param receiver the receiver that will receive the value.
        */
        public NumericPatternArrayLoader (Numeric.Type type, int radix,
            Receiver<Numeric.Pattern []> receiver)
        {
            this.type = type;
            this.radix = radix;
            this.receiver = receiver;
        }

        /**
        * Called when the end tag of the element is encountered.
        * Parses the value as a sequence of numeric patterns, and passes the
        * parsed value to the receiver.
        * @param value the trimmed concatenation of any characters
        * found between the start and end tags, excluding any nested
        * elements.
        * @throws ValueException if the value cannot be parsed as an array
        * of patterns, or if the receiver throws a {@code ValueException}..
        */
        public void end (String value)
            throws ValueException
        {
            if (value.equals ("!"))
            {
                receiver.receive (new Numeric.Pattern [0]);
                return;
            }
            String [] a = value.split (",", -1);
            Numeric.Pattern [] result = new Numeric.Pattern [a.length];

            for (int i = 0 ; i < a.length ; i++)
            {
                result [i] = type.createPattern (a [i], radix);
            }

            receiver.receive (result);
        }
    }

    /**
    * This implementation of the {@code Loader} interface loads an element
    * that has a sequence of hex bytes as its value, and no nested elements.
    */
    public static class ByteArrayLoader
        extends AbstractLoader
    {
        private final Receiver<byte []> receiver;

        /**
        * Constructs an {@code ByteArrayLoader}.
        * @param receiver the receiver that will receive the value.
        */
        public ByteArrayLoader (Receiver<byte []> receiver)
        {
            this.receiver = receiver;
        }

        /**
        * Called when the end tag of the element is encountered.
        * Parses the value as a sequence of hex bytes, and passes the parsed
        *  value to the receiver.
        * @param value the trimmed concatenation of any characters
        * found between the start and end tags, excluding any nested
        * elements.
        * @throws ValueException if the value cannot be parsed as an array
        * of hex bytes, or if the receiver throws a {@code ValueException}..
        */
        public void end (String value)
            throws ValueException
        {
            receiver.receive (Bytes.fromHexString (value));
        }
    }

    /**
    * This implementation of the {@code Loader} interface loads an element
    * that has a sequence of bits as its value, and no nested elements.
    */
    public static class BitArrayLoader
        extends AbstractLoader
    {
        private final Receiver<Numeric.Value []> receiver;

        /**
        * Constructs an {@code BitArrayLoader}.
        * @param receiver the receiver that will receive the value.
        */
        public BitArrayLoader (Receiver<Numeric.Value []> receiver)
        {
            this.receiver = receiver;
        }

        /**
        * Called when the end tag of the element is encountered.
        * Parses the value as a sequence of bits, and passes the parsed
        *  value to the receiver.
        * @param value the trimmed concatenation of any characters
        * found between the start and end tags, excluding any nested
        * elements.
        * @throws ValueException if the value cannot be parsed as an array
        * of bits, or if the receiver throws a {@code ValueException}..
        */
        public void end (String value)
            throws ValueException
        {
            Numeric.Value [] bits =
                Numeric.Type.int1.createUndefArray (value.length ());

            for (int i = 0 ; i < bits.length ; i++)
            {
                switch (value.charAt (i))
                {
                case '0':
                    bits [i] = Numeric.Type.int1.zero;
                    break;
                case '1':
                    bits [i] = Numeric.Type.int1.one;
                    break;
                case '*':
                    bits [i] = Numeric.Type.int1.undef;
                    break;
                default:
                    throw new ValueException ("0 or 1 required");
                }
            }
            receiver.receive (bits);
        }
    }

    private static class State
    {
        final State parent;
        final Loader loader;
        final StringBuilder value;

        State (Loader loader, State parent)
        {
            this.loader = loader;
            this.parent = parent;
            value = new StringBuilder ();
        }
    }

    private static class Handler
        extends DefaultHandler
    {
        private final Loader rootLoader;
        private final String rootTag;
        private Locator locator;
        private State state;

        Handler (String rootTag, Loader rootLoader)
        {
            this.rootTag = rootTag;
            this.rootLoader = rootLoader;
            locator = null;
            state = null;
        }

        @Override
        public void characters (char [] chars, int start, int len)
        {
            state.value.append (chars, start, len);
        }

        @Override
        public void startElement (String uri, String localName,
                String qName, org.xml.sax.Attributes atts)
            throws SAXException
        {
            Loader loader = null;
            if (state == null)
            {
                if (!localName.equals (rootTag))
                {
                    throw new SAXNotRecognizedException (
                        localName + ": Invalid root element");
                }
                loader = rootLoader;
            }
            else if (state.loader != null)
            {
                try
                {
                    loader = state.loader.startChild (localName);
                }
                catch (ValueException e)
                {
                    parseError (state, e.getMessage ());
                }
            }
            state = new State (loader, state);

            if (loader != null)
            {
                try
                {
                    int length = atts.getLength ();

                    for (int i = 0 ; i < length ; i++)
                    {
                        String key = atts.getLocalName (i);
                        String value = atts.getValue (i);
                        loader.attribute (key, value);
                    }
                }
                catch (ValueException e)
                {
                    parseError (state, e.getMessage ());
                }
            }
        }

        @Override
        public void endElement (String uri, String localName, String qName)
            throws SAXException
        {
            try
            {
                String value = state.value.toString ().trim ();
                if (value.equals (""))
                    value = "";
                Loader child = state.loader;

                if (child != null)
                {
                    child.end (value);
                    child.cleanup ();
                }

                state = state.parent;
            }
            catch (ValueException e)
            {
                parseError (state, e.getMessage ());
            }
        }

        private void parseError (State state, String message)
            throws SAXException
        {
            while (state != null)
            {
                if (state.loader != null)
                    state.loader.cleanup ();
                state = state.parent;
            }

            fatalError (new SAXParseException (message, locator));
        }

        @Override
        public void setDocumentLocator (Locator locator)
        {
            this.locator = locator;
        }

        @Override
        public void error (SAXParseException e)
            throws SAXException
        {
            throw e;
        }

        @Override
        public void warning (SAXParseException e)
            throws SAXException
        {
            throw e;
        }
    }

    /**
    * Loads an XML-formatted file.
    * @param tag the tag of the root element.
    * @param rootLoader loader to load the root element.
    * @param file the XML-formatted file to load from.
    * @throws ValueException if a parse error occurs, or if a ValueException
    * is thrown by a loader.
    * @throws IOException if an I/O error occurs.
    */
    public static void load (String tag, Loader rootLoader, File file)
        throws ValueException, IOException
    {
        try
        {
            Handler handler = new Handler (tag, rootLoader);
            SAXParserFactory factory = SAXParserFactory.newInstance ();
            factory.setValidating (false);
            factory.setNamespaceAware (true);
            SAXParser parser = factory.newSAXParser ();
            parser.parse (file, handler);
        }
        catch (ParserConfigurationException e)
        {
            throw new ValueException ("SAX parser configuration error: " +
                Exceptions.getMessage (e));
        }
        catch (SAXParseException e)
        {
            throw new ValueException ("Line " + e.getLineNumber () + ": " +
                    Exceptions.getMessage (e));
        }
        catch (SAXException e)
        {
            throw new ValueException (Exceptions.getMessage (e));
        }
    }



    /**
    * This interface should be implemented by classes that want to save
    * elements to an XML-formatted file.
    */
    public interface Savable
    {
        /**
        * Saves to an XML-formatted file.
        * @param saver the saver that writes the XML-formatted file.
        */
        void save (Saver saver)
            throws IOException;
    }

    /**
    * This interface is implemented by classes that write XML-formatted files.
    */
    public interface Saver
    {
        /**
        * Saves a {@code Savable} value as an element.
        * @param tag the element tag.
        * @param value the element value.
        * @throws IOException if an I/O error occurs.
        */
        void saveValue (String tag, Savable value)
            throws IOException;

        /**
        * Saves a string value as an element.
        * @param tag the element tag.
        * @param value the element value.
        * @throws IOException if an I/O error occurs.
        */
        void saveValue (String tag, String value)
            throws IOException;

        /**
        * Saves a long value as an element.
        * @param tag the element tag.
        * @param value the element value.
        * @param radix the radix to be used to convert the value.
        * @throws IOException if an I/O error occurs.
        */
        void saveValue (String tag, long value, int radix)
            throws IOException;

        /**
        * Saves a long value as an element.
        * The value is saved in decimal.
        * @param tag the element tag.
        * @param value the element value.
        * @throws IOException if an I/O error occurs.
        */
        void saveValue (String tag, long value)
            throws IOException;

        /**
        * Saves a double value as an element.
        * @param tag the element tag.
        * @param value the element value.
        * @throws IOException if an I/O error occurs.
        */
        void saveValue (String tag, double value)
            throws IOException;

        /**
        * Saves a boolean value as an element.
        * @param tag the element tag.
        * @param value the element value.
        * @throws IOException if an I/O error occurs.
        */
        void saveValue (String tag, boolean value)
            throws IOException;

        /**
        * Saves an enum value as an element.
        * @param tag the element tag.
        * @param value the element value.
        * @throws IOException if an I/O error occurs.
        */
        void saveValue (String tag, Enum<?> value)
            throws IOException;

        /**
        * Saves an array of bytes as an element.
        * The array is saved as space-separated hex.
        * @param tag the element tag.
        * @param value the element value.
        * @throws IOException if an I/O error occurs.
        */
        public void saveValue (String tag, byte [] value)
            throws IOException;

        /**
        * Saves a numeric value as an element.
        * @param tag the element tag.
        * @param value the element value.
        * @param radix the radix to be used for converting the value.
        * @throws IOException if an I/O error occurs.
        */
        public void saveValue (String tag, Numeric.Value value, int radix)
            throws IOException;

        /**
        * Saves a numeric pattern as an element.
        * @param tag the element tag.
        * @param value the element value.
        * @param radix the radix to be used for converting the value.
        * @throws IOException if an I/O error occurs.
        */
        public void saveValue (String tag, Numeric.Pattern value, int radix)
            throws IOException;

        /**
        * Saves an array of bytes as an element.
        * Each element of the array is assumed to be 0 or 1.
        * The array is saved as a sequence of 0 and 1 characters, with
        * no spaces.
        * @param tag the element tag.
        * @param value the element value.
        * @throws IOException if an I/O error occurs.
        */
        public void saveBitArrayValue (String tag, Numeric.Value [] value)
            throws IOException;

        /**
        * Saves an array of numeric values as an element.
        * @param tag the element tag.
        * @param values the element values.
        * @param radix the radix to be used for converting the values.
        * @throws IOException if an I/O error occurs.
        */
        public void saveValue (String tag, Numeric.Value [] values, int radix)
            throws IOException;

        /**
        * Saves an array of numeric patterns as an element.
        * @param tag the element tag.
        * @param values the element values.
        * @param radix the radix to be used for converting the values.
        * @throws IOException if an I/O error occurs.
        */
        public void saveValue (String tag, Numeric.Pattern [] values, int radix)
            throws IOException;

        /**
        * Saves an attribute.
        * @param name the attribute name.
        * @param value the attribute value.
        * @throws IOException if an I/O error occurs.
        */
        void saveAttribute (String name, String value)
            throws IOException;

        /**
        * Outputs a start tag.
        * @param tag the tag name.
        * @throws IOException if an I/O error occurs.
        */
        void startTag (String tag)
            throws IOException;

        /**
        * Outputs an end tag.
        * @param tag the tag name.
        * @throws IOException if an I/O error occurs.
        */
        void endTag (String tag)
            throws IOException;
    }

    /**
    * Saves to an XML-formatted file.
    * @param tag the tag of the root element.
    * @param data the root of the data to be saved.
    * @param file the XML-formatted file to save to.
    * @throws IOException if an I/O error occurs.
    */
    public static void save (String tag, Savable data, File file)
        throws IOException
    {
        BufferedWriter out = new BufferedWriter (new OutputStreamWriter (
            new FileOutputStream (file), "UTF-8"));
        try
        {
            out.write ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            out.newLine ();
            PrintSaver saver = new PrintSaver (out, 0);
            saver.saveValue (tag, data);
        }
        finally
        {
            out.close ();
        }
    }

    private static class PrintSaver
        implements Saver
    {
        private final BufferedWriter out;
        private int lev;
        private boolean startTagEnded;
        private boolean attributesStarted;

        PrintSaver (BufferedWriter out, int lev)
        {
            this.out = out;
            this.lev = lev;
            startTagEnded = true;
            attributesStarted = false;
        }

        private void endStartTag ()
            throws IOException
        {
            if (!startTagEnded)
            {
                if (lev == 1 && attributesStarted)
                {
                    out.write (" >");
                    out.newLine ();
                    out.newLine ();
                }
                else
                {
                    out.write (">");
                    out.newLine ();
                }
                startTagEnded = true;
            }
        }

        private void indent ()
            throws IOException
        {
            for (int i = 0 ; i < lev ; i++)
                out.write ("  ");
        }

        /*
        * This method outputs a string, escaping XML syntax characters, and
        * ignoring any characters that are not allowed in XML - see
        * http://cse-mjmcl.cse.bris.ac.uk/blog/2007/02/14/1171465494443.html
        */
        private void saveString (String str)
            throws IOException
        {
            for (int i = 0 ; i < str.length () ; )
            {
                int c = str.codePointAt (i);

                if (c == 0x9 || c == 0xA || c == 0xD ||
                    (c >= 0x20 && c <= 0xD7FF) ||
                    (c >= 0xE000 && c <= 0xFFFD) ||
                    (c >= 0x10000 && c <= 0x10FFFF))
                {
                    switch (c)
                    {
                    case '>':
                        out.write ("&gt;");
                        break;
                    case '<':
                        out.write ("&lt;");
                        break;
                    case '&':
                        out.write ("&amp;");
                        break;
                    case '"':
                        out.write ("&quot;");
                        break;
                    case '\'':
                        out.write ("&apos;");
                        break;
                    default:
                        out.write (Character.toChars (c));
                        break;
                    }
                }

                i+= Character.charCount (c);
            }
        }

        public void startTag (String tag)
            throws IOException
        {
            endStartTag ();
            indent ();
            out.write ("<" + tag);
            startTagEnded = false;
            lev++;
        }

        public void endTag (String tag)
            throws IOException
        {
            lev--;
            if (startTagEnded)
            {
                indent ();
                out.write ("</" + tag + ">");
                out.newLine ();
            }
            else
            {
                out.write ("/>");
                out.newLine ();
                startTagEnded = true;
            }
        }

        public void saveValue (String tag, Savable value)
            throws IOException
        {
            startTag (tag);
            value.save (this);
            endStartTag ();
            endTag (tag);
        }

        public void saveValue (String tag, String value)
            throws IOException
        {
            startTag (tag);
            out.write (">");
            startTagEnded = true;
            saveString (value);
            lev--;
            out.write ("</" + tag + ">");
            out.newLine ();
        }

        public void saveValue (String tag, long value, int radix)
            throws IOException
        {
            saveValue (tag, Long.toString (value, radix));
        }

        public void saveValue (String tag, long value)
            throws IOException
        {
            saveValue (tag, Long.toString (value));
        }

        public void saveValue (String tag, double value)
            throws IOException
        {
            saveValue (tag, Double.toString (value));
        }

        public void saveValue (String tag, boolean value)
            throws IOException
        {
            saveValue (tag, Boolean.toString (value));
        }

        public void saveValue (String tag, Enum<?> value)
            throws IOException
        {
            saveValue (tag, value.name ());
        }

        public void saveValue (String tag, byte [] value)
            throws IOException
        {
            saveValue (tag, Bytes.toHexString (value));
        }

        public void saveValue (String tag, Numeric.Value value, int radix)
            throws IOException
        {
            saveValue (tag, value.toString (radix));
        }

        public void saveValue (String tag, Numeric.Pattern value, int radix)
            throws IOException
        {
            saveValue (tag, value.toString (radix));
        }

        public void saveValue (String tag, Numeric.Value [] values, int radix)
            throws IOException
        {
            if (values.length == 0)
            {
                saveValue (tag, "!");
                return;
            }

            StringBuilder str = new StringBuilder ();

            for (Numeric.Value value : values)
            {
                if (value.isDefined ())
                    str.append (value.toString (radix));
                else
                    str.append ('*');
                str.append (' ');
            }

            saveValue (tag, str.toString ());
        }

        public void saveValue (String tag, Numeric.Pattern [] values, int radix)
            throws IOException
        {
            if (values.length == 0)
            {
                saveValue (tag, "!");
                return;
            }

            StringBuilder str = new StringBuilder ();

            for (int i = 0 ; i < values.length ; ++i)
            {
                if (i != 0)
                    str.append (',');
                str.append (values [i].toString (radix));
            }

            saveValue (tag, str.toString ());
        }

        public void saveBitArrayValue (String tag, Numeric.Value [] value)
            throws IOException
        {
            StringBuilder str = new StringBuilder ();

            for (Numeric.Value b : value)
            {
                str.append (
                    !b.isDefined () ? '*' :
                    b.equals (Numeric.Type.int1.zero) ? '0' : '1');
            }

            saveValue (tag, str.toString ());
        }

        public void saveAttribute (String name, String value)
            throws IOException
        {
            if (startTagEnded)
            {
                throw new IllegalStateException (
                    "saveAttribute called outside of start tag");
            }
            if (lev == 0 && !attributesStarted)
                out.newLine ();
            attributesStarted = true;
            out.write (" " + name + "=\"");
            saveString (value);
            out.write ("\"");
            if (lev == 0)
                out.newLine ();
        }
    }
}


