ca.sqlpower.wabit.dao.WorkspaceSAXHandler.java Source code

Java tutorial

Introduction

Here is the source code for ca.sqlpower.wabit.dao.WorkspaceSAXHandler.java

Source

/*
 * Copyright (c) 2008, SQL Power Group Inc.
 *
 * This file is part of Wabit.
 *
 * Wabit is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Wabit 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 */

package ca.sqlpower.wabit.dao;

import java.awt.Color;
import java.awt.Font;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.CancellationException;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.imageio.ImageIO;

import org.apache.commons.codec.binary.Base64;
import org.apache.log4j.Logger;
import org.olap4j.OlapException;
import org.olap4j.metadata.Cube;
import org.olap4j.metadata.Dimension;
import org.olap4j.metadata.Hierarchy;
import org.olap4j.metadata.Level;
import org.olap4j.metadata.Member;
import org.olap4j.query.Selection.Operator;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.DefaultHandler;

import ca.sqlpower.dao.session.DateConverter;
import ca.sqlpower.object.HorizontalAlignment;
import ca.sqlpower.object.VerticalAlignment;
import ca.sqlpower.query.Container;
import ca.sqlpower.query.Item;
import ca.sqlpower.query.SQLGroupFunction;
import ca.sqlpower.query.SQLJoin;
import ca.sqlpower.query.SQLObjectItem;
import ca.sqlpower.query.StringItem;
import ca.sqlpower.query.TableContainer;
import ca.sqlpower.query.QueryImpl.OrderByArgument;
import ca.sqlpower.sql.DataSourceCollection;
import ca.sqlpower.sql.JDBCDataSource;
import ca.sqlpower.sql.Olap4jDataSource;
import ca.sqlpower.sql.SPDataSource;
import ca.sqlpower.util.UserPrompter;
import ca.sqlpower.util.UserPrompterFactory;
import ca.sqlpower.util.Version;
import ca.sqlpower.util.UserPrompter.UserPromptOptions;
import ca.sqlpower.util.UserPrompter.UserPromptResponse;
import ca.sqlpower.util.UserPrompterFactory.UserPromptType;
import ca.sqlpower.wabit.WabitDataSource;
import ca.sqlpower.wabit.WabitObject;
import ca.sqlpower.wabit.WabitSession;
import ca.sqlpower.wabit.WabitSessionContext;
import ca.sqlpower.wabit.image.WabitImage;
import ca.sqlpower.wabit.report.CellSetRenderer;
import ca.sqlpower.wabit.report.ChartRenderer;
import ca.sqlpower.wabit.report.ColumnInfo;
import ca.sqlpower.wabit.report.ContentBox;
import ca.sqlpower.wabit.report.DataType;
import ca.sqlpower.wabit.report.Guide;
import ca.sqlpower.wabit.report.ImageRenderer;
import ca.sqlpower.wabit.report.WabitLabel;
import ca.sqlpower.wabit.report.Layout;
import ca.sqlpower.wabit.report.Page;
import ca.sqlpower.wabit.report.Report;
import ca.sqlpower.wabit.report.ResultSetRenderer;
import ca.sqlpower.wabit.report.Template;
import ca.sqlpower.wabit.report.ColumnInfo.GroupAndBreak;
import ca.sqlpower.wabit.report.Guide.Axis;
import ca.sqlpower.wabit.report.Page.PageOrientation;
import ca.sqlpower.wabit.report.ResultSetRenderer.BorderStyles;
import ca.sqlpower.wabit.report.chart.Chart;
import ca.sqlpower.wabit.report.chart.ChartColumn;
import ca.sqlpower.wabit.report.chart.ChartType;
import ca.sqlpower.wabit.report.chart.ColumnRole;
import ca.sqlpower.wabit.report.chart.LegendPosition;
import ca.sqlpower.wabit.report.selectors.ComboBoxSelector;
import ca.sqlpower.wabit.report.selectors.DateSelector;
import ca.sqlpower.wabit.report.selectors.Selector;
import ca.sqlpower.wabit.report.selectors.TextBoxSelector;
import ca.sqlpower.wabit.rs.WabitResultSetProducer;
import ca.sqlpower.wabit.rs.olap.OlapQuery;
import ca.sqlpower.wabit.rs.olap.WabitOlapAxis;
import ca.sqlpower.wabit.rs.olap.WabitOlapDimension;
import ca.sqlpower.wabit.rs.olap.WabitOlapExclusion;
import ca.sqlpower.wabit.rs.olap.WabitOlapInclusion;
import ca.sqlpower.wabit.rs.query.QueryCache;

/**
 * This will be used with a parser to load a saved workspace from a file.
 */
public class WorkspaceSAXHandler extends DefaultHandler {

    private static final Logger logger = Logger.getLogger(WorkspaceSAXHandler.class);

    /**
     * This will track the tags and depth of the XML tags.
     */
    private final Stack<String> xmlContext = new Stack<String>();

    /**
     * This is the session that the workspace will be loaded into.
     */
    private final WabitSession session;

    /**
     * This is the current query being loaded in from the file.
     */
    private QueryCache cache;

    /**
     * This maps all of the currently loaded Items with their UUIDs. This
     * will let the loaded elements of a query be able to hook up to the correct
     * items. This map will keep the item values throughout loading to allow access
     * to all items throughout the file.
     */
    private final Map<String, Item> uuidToItemMap = new HashMap<String, Item>();

    /**
     * This is the current container being loaded in by this SAX handler.
     */
    private Container container;

    /**
     * This list of items are the current items being loaded into the current
     * container being loaded.
     */
    private List<SQLObjectItem> containerItems;

    /**
     * This context will store the session created by this SAX handler.
     */
    private final WabitSessionContext context;

    /**
     * This layout stores the current layout being loaded by this SAX handler.
     */
    private Layout layout;

    /**
     * This is the current content box being loaded by this SAX handler.
     */
    private ContentBox contentBox;

    /**
     * This is the current result set renderer being loaded by this SAX handler.
     */
    private ResultSetRenderer rsRenderer;

    private ColumnInfo colInfo;

    /**
     * This stores the currently loading Image renderer. This will be null if no image
     * renderer is being loaded.
     */
    private ImageRenderer imageRenderer;

    private ByteArrayOutputStream byteStream;

    /**
     * Describes if the loading of the workspace has been cancelled.
     */
    private AtomicBoolean cancelled = new AtomicBoolean();

    /**
     * This message describes where the parser is in the file.
     */
    private String progressMessage = "";

    /**
     * This map stores old DS names used in the loaded workspace to the new DS specified
     * by the user. Only the data sources that have a new data source specified will
     * appear in this map.
     */
    private final Map<String, String> oldToNewDSNames;

    /**
     * This is an {@link OlapQuery} that is currently being loaded from the file. This
     * may be null if no OlapQuery is currently being loaded. This variable is also used
     * when loading up the olapQuery in a report.
     */
    private OlapQuery olapQuery;

    /**
     * This is an Olap4j {@link org.olap4j.query.Query} which is currently being
     * loaded. This may be null.
     */
    private CellSetRenderer cellSetRenderer;

    private final UserPrompterFactory promptFactory;

    /**
     * This is the current WabitImage being loaded.
     */
    private WabitImage currentWabitImage;

    /**
     * The chart currently being read from the XML stream. This will be null
     * unless we are within a &lt;chart&gt; element.
     */
    private Chart chart;

    /**
     * Gets set to true when inside a missing-columns element.
     */
    private boolean readingMissingChartCols;

    private boolean nameMandatory;

    private boolean uuidMandatory;

    private WabitOlapAxis olapAxis;

    private WabitOlapDimension olapDimension;

    private String catalogName;

    private String schemaName;

    private String cubeName;

    private Olap4jDataSource olapDataSource;

    private String olapName;

    private String olapID;

    private WabitObject selectorContainer;

    /**
     * The data sources in this collection will be used to verify if
     * {@link WabitDataSource}s being loaded can find valid JDBC or OLAP data
     * sources. If the data sources do not exist the user will be prompted.
     */
    private final DataSourceCollection<SPDataSource> dsCollection;

    private boolean isInLayout = false;

    private Selector selector;

    /**
     * Creates a new SAX handler which is capable of reading in a series of
     * workspace descriptions from an XML stream. The list of workspaces
     * encountered in the stream become available as a Wabit Session.
     * <p>
     * This constructor must be called from the foreground thread
     * 
     * @param context The context that will create sessions for loading and
     * creates user prompters if input is required.
     */
    public WorkspaceSAXHandler(WabitSessionContext context) {
        this(context, null);
    }

    /**
     * Creates a new SAX handler which is capable of reading in a series of
     * workspace descriptions from an XML stream. The list of workspaces
     * encountered in the stream become available as a Wabit Session.
     * <p>
     * This constructor must be called from the foreground thread
     * 
     * @param context
     *            The context that will create sessions for loading and creates
     *            user prompters if input is required.
     * @param serverInfo
     *            The collection of data sources available to the objects being
     *            loaded into Wabit. If this is null the local collection will
     *            be used.
     */
    public WorkspaceSAXHandler(WabitSessionContext context, DataSourceCollection<SPDataSource> dsCollection) {
        this.context = context;
        this.promptFactory = context;
        oldToNewDSNames = new HashMap<String, String>();
        setCancelled(false);
        session = context.createSession();
        if (dsCollection != null) {
            this.dsCollection = dsCollection;
        } else {
            this.dsCollection = context.getDataSources();
        }
    }

    @Override
    public void startElement(final String uri, final String localName, final String name, final Attributes attr)
            throws SAXException {
        if (isCancelled()) {
            throw new CancellationException();
        }
        byteStream = new ByteArrayOutputStream();
        final Attributes attributes = new AttributesImpl(attr);
        Runnable runner = new Runnable() {
            public void run() {
                try {
                    context.startLoading();
                    startElementImpl(uri, localName, name, attributes);
                } catch (SAXException e) {
                    setCancelled(true);
                    throw new RuntimeException(e);
                } finally {
                    context.endLoading();
                }
            }
        };
        session.runInForeground(runner);
    }

    /**
     * Throws a {@link CancellationException} if either the loading of the file
     * was cancelled by a method call or cancelled internally due to a problem
     * in the file. If there was a problem with the file this method will notify
     * the user and another notification does not need to be sent.
     * <p>
     * 
     * @see WorkspaceXMLDAO#FILE_VERSION
     */
    private void startElementImpl(final String uri, final String localName, final String name,
            Attributes attributes) throws SAXException {
        if (isCancelled()) {
            return;
        }

        xmlContext.push(name);

        final WabitObject createdObject;

        if (name.equals("wabit")) {
            createdObject = null;

            String versionString = attributes.getValue("export-format");

            //NOTE: For correct versioning behaviour see WorkspaceXMLDAO.FILE_VERSION.
            if (versionString == null) {
                UserPrompter up = promptFactory.createUserPrompter(
                        "This Wabit workspace file is very old. It may not read correctly, but I will try my best.",
                        UserPromptType.MESSAGE, UserPromptOptions.OK, UserPromptResponse.OK, null, "OK");
                up.promptUser();
            } else {
                Version fileVersion = new Version(versionString);
                Version fileMajorMinorVersion = new Version(fileVersion, 2);
                Version currentMajorMinorVersion = new Version(WorkspaceXMLDAO.FILE_VERSION, 2);
                Version fileMajorVersion = new Version(fileVersion, 1);
                Version currentMajorVersion = new Version(WorkspaceXMLDAO.FILE_VERSION, 1);

                String message = null;
                boolean displayMessage = true;
                if (fileMajorVersion.compareTo(currentMajorVersion) < 0) {
                    message = "The Wabit workspace you are opening is too old to be successfully loaded.\n"
                            + "An older version of Wabit is required to view the saved workspace.";
                    setCancelled(true);
                } else if (fileVersion.equals(new Version("1.0.0"))) {
                    message = "The Wabit workspace you are opening is an old version that does not record\n"
                            + "information about page orientation. All pages will default to portrait orientation.";
                } else if (fileVersion.compareTo(new Version("1.1.0")) < 0) {
                    message = "The Wabit workspace you are opening contains OLAP and/or reports from an.\n"
                            + "old version of the wabit. These items cannot be loaded and need to be updated\n"
                            + "to the latest version.";
                } else if (fileVersion.compareTo(new Version("1.2.0")) < 0) {
                    message = "The Wabit workspace you are opening was created in an older version of Wabit\n"
                            + "which stored charts within reports rather than sharing them within the Workspace.\n"
                            + "Your charts will appear as empty boxes; you will have to re-create them.";
                } else if (fileMajorMinorVersion.compareTo(currentMajorMinorVersion) > 0) {
                    message = "The Wabit workspace you are opening was created in a newer version of Wabit.\n"
                            + "Due to large changes in the file format this file cannot be loaded without updating "
                            + "Wabit.";
                    setCancelled(true);
                } else if (fileVersion.compareTo(WorkspaceXMLDAO.FILE_VERSION) > 0) {
                    message = "The Wabit workspace you are opening was created in a newer version of Wabit.\n"
                            + "I will attempt to load this workspace but it is recommended to update Wabit\n"
                            + "to the latest version.";
                } else {
                    displayMessage = false;
                }

                if (fileVersion.compareTo(new Version("1.2.5")) >= 0) {
                    nameMandatory = true;
                    uuidMandatory = true;
                } else {
                    nameMandatory = false;
                    uuidMandatory = false;
                }

                if (displayMessage) {
                    UserPrompter up = promptFactory.createUserPrompter(message, UserPromptType.MESSAGE,
                            UserPromptOptions.OK, UserPromptResponse.OK, null, "OK");
                    up.promptUser();
                }
                if (isCancelled())
                    throw new CancellationException();
            }

        } else if (name.equals("project")) {
            createdObject = session.getWorkspace();
        } else if (name.equals("data-source")) {
            String dsName = attributes.getValue("name");
            checkMandatory("name", dsName);

            progressMessage = session.getWorkspace().getName() + ": loading data source " + dsName;

            SPDataSource ds = dsCollection.getDataSource(dsName);
            if (ds == null) {
                List<Class<? extends SPDataSource>> dsTypes = new ArrayList<Class<? extends SPDataSource>>();
                dsTypes.add(JDBCDataSource.class);
                dsTypes.add(Olap4jDataSource.class);
                //Note: the new prompt here is so that on the server side the user still has the
                //option of creating a new datasource
                UserPrompter prompter = promptFactory.createDatabaseUserPrompter(
                        "The data source \"" + dsName + "\" does not exist. Please select a replacement.", dsTypes,
                        UserPromptOptions.OK_NEW_NOTOK_CANCEL, UserPromptResponse.NOT_OK, null, dsCollection,
                        "Select Data Source", "New...", "Skip Data Source", "Cancel Load");

                UserPromptResponse response = prompter.promptUser();
                if (response == UserPromptResponse.OK || response == UserPromptResponse.NEW) {
                    ds = (SPDataSource) prompter.getUserSelectedResponse();
                    createdObject = new WabitDataSource(ds);
                    if (!session.getWorkspace().dsAlreadyAdded(ds)) {
                        session.getWorkspace().addDataSource(ds);
                    }
                    oldToNewDSNames.put(dsName, ds.getName());
                } else if (response == UserPromptResponse.NOT_OK) {
                    ds = null;
                    createdObject = null;
                } else {
                    setCancelled(true);
                    createdObject = null;
                }
            } else if (!session.getWorkspace().dsAlreadyAdded(ds)) {
                session.getWorkspace().addDataSource(ds);
                createdObject = new WabitDataSource(ds);
            } else {
                createdObject = null;
            }
        } else if (name.equals("query")) {
            cache = new QueryCache(session.getContext(), false);
            createdObject = cache;

            String queryName = attributes.getValue("name");
            cache.setName(queryName);
            progressMessage = session.getWorkspace().getName() + " : loading query " + queryName;

            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("uuid")) {
                    // already loaded
                } else if (aname.equals("name")) {
                    // already loaded
                } else if (aname.equals("data-source")) {
                    JDBCDataSource ds = session.getWorkspace().getDataSource(aval, JDBCDataSource.class);
                    if (ds == null) {
                        String newDSName = oldToNewDSNames.get(aval);
                        if (newDSName != null) {
                            ds = session.getWorkspace().getDataSource(newDSName, JDBCDataSource.class);
                            if (ds == null) {
                                logger.debug("Data source " + aval
                                        + " is not in the workspace. Attempted to replace with new data source "
                                        + newDSName + ". Query " + aname + " was connected to it previously.");
                                throw new NullPointerException(
                                        "Data source " + newDSName + " was not found in the workspace.");
                            }
                        }
                        logger.debug("Workspace has data sources " + session.getWorkspace().getDataSources());
                    }
                    cache.setDataSourceWithoutSideEffects(ds);
                } else if (aname.equals("zoom")) {
                    cache.setZoomLevel(Integer.parseInt(aval));
                } else if (aname.equals("streaming-row-limit")) {
                    cache.setStreamingRowLimit(Integer.parseInt(aval));
                } else if (aname.equals("row-limit")) {
                    cache.setRowLimit(Integer.parseInt(aval));
                } else if (aname.equals("grouping-enabled")) {
                    cache.setGroupingEnabled(Boolean.parseBoolean(aval));
                } else if (aname.equals("prompt-for-cross-joins")) {
                    cache.setPromptForCrossJoins(Boolean.parseBoolean(aval));
                } else if (aname.equals("execute-queries-with-cross-joins")) {
                    cache.setExecuteQueriesWithCrossJoins(Boolean.parseBoolean(aval));
                } else if (aname.equals("automatically-executing")) {
                    cache.setAutomaticallyExecuting(Boolean.parseBoolean(aval));
                } else if (aname.equals("streaming")) {
                    cache.setStreaming(Boolean.parseBoolean(aval));
                } else {
                    logger.warn("Unexpected attribute of <query>: " + aname + "=" + aval);
                }
            }
            session.getWorkspace().addQuery(cache, session);
        } else if (name.equals("constants")) {
            createdObject = null;
            String uuid = attributes.getValue("uuid");
            checkMandatory("uuid", uuid);
            cache.getConstantsContainer().setUUID(uuid);
            Container constants = cache.getConstantsContainer();
            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("uuid")) {
                    // already loaded
                } else if (aname.equals("xpos")) {
                    constants.setPosition(
                            new Point2D.Double(Double.parseDouble(aval), constants.getPosition().getY()));
                    logger.debug("Constants container is at position " + constants.getPosition());
                } else if (aname.equals("ypos")) {
                    constants.setPosition(
                            new Point2D.Double(constants.getPosition().getX(), Double.parseDouble(aval)));
                } else {
                    logger.warn("Unexpected attribute of <constants>: " + aname + "=" + aval);
                }
            }
        } else if (name.equals("table")) {
            createdObject = null;
            String tableName = attributes.getValue("name");
            String schema = attributes.getValue("schema");
            String catalog = attributes.getValue("catalog");
            String uuid = attributes.getValue("uuid");
            checkMandatory("uuid", uuid);
            checkMandatory("name", tableName);
            TableContainer table = new TableContainer(uuid, cache.getDatabase(), tableName, schema, catalog,
                    new ArrayList<SQLObjectItem>());
            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("name") || aname.equals("schema") || aname.equals("catalog")
                        || aname.equals("uuid")) {
                    // already loaded.
                } else if (aname.equals("xpos")) {
                    table.setPosition(new Point2D.Double(Double.parseDouble(aval), table.getPosition().getY()));
                } else if (aname.equals("ypos")) {
                    table.setPosition(new Point2D.Double(table.getPosition().getX(), Double.parseDouble(aval)));
                } else if (aname.equals("alias")) {
                    table.setAlias(aval);
                } else {
                    logger.warn("Unexpected attribute of <table>: " + aname + "=" + aval);
                }
            }
            container = table;
            containerItems = new ArrayList<SQLObjectItem>();
        } else if (name.equals("column")) {
            createdObject = null;
            if (parentIs("constants")) {
                String itemName = attributes.getValue("name");
                String uuid = attributes.getValue("id");
                checkMandatory("name", itemName);
                checkMandatory("id", uuid);
                Item item = new StringItem(itemName, uuid);
                for (int i = 0; i < attributes.getLength(); i++) {
                    String aname = attributes.getQName(i);
                    String aval = attributes.getValue(i);
                    if (aname.equals("name") || aname.equals("id")) {
                        //already loaded.
                    } else if (aname.equals("alias")) {
                        item.setAlias(aval);
                    } else if (aname.equals("where-text")) {
                        item.setWhere(aval);
                    } else if (aname.equals("group-by")) {
                        item.setGroupBy(SQLGroupFunction.valueOf(aval));
                    } else if (aname.equals("having")) {
                        item.setHaving(aval);
                    } else if (aname.equals("order-by")) {
                        item.setOrderBy(OrderByArgument.valueOf(aval));
                    } else {
                        logger.warn("Unexpected attribute of <constant-column>: " + aname + "=" + aval);
                    }
                }
                cache.getConstantsContainer().addItem(item);
                uuidToItemMap.put(uuid, item);
            } else if (parentIs("table")) {
                String itemName = attributes.getValue("name");
                String uuid = attributes.getValue("id");
                checkMandatory("name", itemName);
                checkMandatory("id", uuid);
                SQLObjectItem item = new SQLObjectItem(itemName, uuid);
                for (int i = 0; i < attributes.getLength(); i++) {
                    String aname = attributes.getQName(i);
                    String aval = attributes.getValue(i);
                    if (aname.equals("name") || aname.equals("id")) {
                        //already loaded.
                    } else if (aname.equals("alias")) {
                        item.setAlias(aval);
                    } else if (aname.equals("where-text")) {
                        item.setWhere(aval);
                    } else if (aname.equals("group-by")) {
                        item.setGroupBy(SQLGroupFunction.valueOf(aval));
                    } else if (aname.equals("having")) {
                        item.setHaving(aval);
                    } else if (aname.equals("order-by")) {
                        item.setOrderBy(OrderByArgument.valueOf(aval));
                    } else {
                        logger.warn("Unexpected attribute of <constant-column>: " + aname + "=" + aval);
                    }
                }
                containerItems.add(item);
                uuidToItemMap.put(uuid, item);
            } else if (parentIs("select")) {
                String uuid = attributes.getValue("id");
                checkMandatory("id", uuid);
                if (uuidToItemMap.get(uuid) == null) {
                    throw new IllegalStateException(
                            "Cannot find a column with id " + uuid + " to add to the select statement.");
                }
                cache.selectItem(uuidToItemMap.get(uuid));
            } else {
                throw new IllegalStateException(
                        "A column is being loaded that is not contained by any tables. Parent is "
                                + xmlContext.get(xmlContext.size() - 2));
            }
        } else if (name.equals("join")) {
            createdObject = null;
            String leftUUID = attributes.getValue("left-item-id");
            String rightUUID = attributes.getValue("right-item-id");
            checkMandatory("left-item-id", leftUUID);
            checkMandatory("right-item-id", rightUUID);
            Item leftItem = uuidToItemMap.get(leftUUID);
            Item rightItem = uuidToItemMap.get(rightUUID);
            if (leftItem == null) {
                throw new IllegalStateException(
                        "The left side of a join was not found. Trying to match UUID " + leftUUID);
            }
            if (rightItem == null) {
                throw new IllegalStateException(
                        "The right side of a join was not found. Trying to match UUID " + rightUUID);
            }
            SQLJoin join = new SQLJoin(leftItem, rightItem);
            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("left-item-id") || aname.equals("right-item-id")) {
                    // already loaded
                } else if (aname.equals("left-is-outer")) {
                    join.setLeftColumnOuterJoin(Boolean.parseBoolean(aval));
                } else if (aname.equals("right-is-outer")) {
                    join.setRightColumnOuterJoin(Boolean.parseBoolean(aval));
                } else if (aname.equals("comparator")) {
                    join.setComparator(aval);
                } else {
                    logger.warn("Unexpected attribute of <join>: " + aname + "=" + aval);
                }
            }
            cache.addJoin(join);
        } else if (name.equals("select")) {
            createdObject = null;
            // Select portion loaded in the "column" part above.
        } else if (name.equals("global-where")) {
            createdObject = null;
            cache.setGlobalWhereClause(attributes.getValue("text"));
        } else if (name.equals("group-by-aggregate")) { // For backwards compatibility to Wabit 0.9.6 and older
            createdObject = null;
            String uuid = attributes.getValue("column-id");
            String aggregate = attributes.getValue("aggregate");
            checkMandatory("column-id", uuid);
            checkMandatory("aggregate", aggregate);
            Item item = uuidToItemMap.get(uuid);
            if (item == null) {
                throw new IllegalStateException(
                        "Could not get a column for grouping. Trying to match UUID " + uuid);
            }
            cache.setGroupingEnabled(true);
            item.setGroupBy(SQLGroupFunction.getGroupType(aggregate));
        } else if (name.equals("having")) { // For backwards compatibility to Wabit 0.9.6 and older
            createdObject = null;
            String uuid = attributes.getValue("column-id");
            String text = attributes.getValue("text");
            checkMandatory("column-id", uuid);
            checkMandatory("text", text);
            Item item = uuidToItemMap.get(uuid);
            if (item == null) {
                throw new IllegalStateException(
                        "Could not get a column to add a having filter. Trying to match UUID " + uuid);
            }
            cache.setGroupingEnabled(true);
            item.setHaving(text);
        } else if (name.equals("order-by")) {
            createdObject = null;
            String uuid = attributes.getValue("column-id");
            checkMandatory("column-id", uuid);
            Item item = uuidToItemMap.get(uuid);
            if (item == null) {
                throw new IllegalStateException(
                        "Could not get a column to add order by to the select statement. Trying to match UUID "
                                + uuid);
            }
            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("column-id")) {
                    //already loaded.
                }
                if (aname.equals("direction")) {// For backwards compatibility to Wabit 0.9.6 and older
                    item.setOrderBy(OrderByArgument.valueOf(aval));
                } else {
                    logger.warn("Unexpected attribute of <order-by>: " + aname + " = " + aval);
                }
            }
            //Reinserting the items for cases where when the items were first created they defined a sort
            //order and were placed in the query in an incorrect order to sort the columns in.
            cache.moveOrderByItemToEnd(item);
        } else if (name.equals("query-string")) {
            createdObject = null;
            String queryString = attributes.getValue("string");
            checkMandatory("string", queryString);
            cache.setUserModifiedQuery(queryString);
        } else if (name.equals("text") && parentIs("query")) {
            createdObject = null;
        } else if (name.equals("olap-query")) {
            olapName = attributes.getValue("name");
            olapID = attributes.getValue("uuid");
            String dsName = attributes.getValue("data-source");
            olapDataSource = session.getWorkspace().getDataSource(dsName, Olap4jDataSource.class);
            if (olapDataSource == null) {
                String newDSName = oldToNewDSNames.get(dsName);
                if (newDSName != null) {
                    olapDataSource = session.getWorkspace().getDataSource(newDSName, Olap4jDataSource.class);
                    if (olapDataSource == null) {
                        logger.debug("Data source " + dsName
                                + " is not in the workspace or was not of the correct type. Attempted to replace with new data source "
                                + newDSName + ". Query " + "data-source" + " was connected to it previously.");
                        throw new NullPointerException("Data source " + newDSName
                                + " was not found in the workspace or was not an Olap4j Datasource.");
                    }
                }
                logger.debug("Workspace has data sources " + session.getWorkspace().getDataSources());
            }
            createdObject = null;
        } else if (name.equals("olap-cube")) {
            catalogName = attributes.getValue("catalog");
            schemaName = attributes.getValue("schema");
            cubeName = attributes.getValue("cube-name");
            createdObject = null;
        } else if (name.equals("olap4j-query")) {
            olapQuery = new OlapQuery(olapID, session.getContext(), attributes.getValue("name"),
                    attributes.getValue("name"), catalogName, schemaName, cubeName,
                    attributes.getValue("modifiedOlapQuery"), !isInLayout);
            olapQuery.setName(olapName);
            olapQuery.setOlapDataSource(olapDataSource);
            if (cellSetRenderer == null) {
                session.getWorkspace().addOlapQuery(olapQuery);
            } else {
                cellSetRenderer.setModifiedOlapQuery(olapQuery);
            }
            createdObject = null;
        } else if (name.equals("olap4j-axis")) {
            olapAxis = new WabitOlapAxis(
                    org.olap4j.Axis.Factory.forOrdinal(Integer.parseInt(attributes.getValue("ordinal"))));
            olapQuery.addAxis(olapAxis);
            createdObject = olapAxis;
        } else if (name.equals("olap4j-dimension")) {
            olapDimension = new WabitOlapDimension(attributes.getValue("dimension-name"));
            olapAxis.addDimension(olapDimension);
            createdObject = olapDimension;
        } else if (name.equals("olap4j-selection")) {
            WabitOlapInclusion olapInclusion = new WabitOlapInclusion(
                    Operator.valueOf(attributes.getValue("operator")), attributes.getValue("unique-member-name"));
            olapDimension.addInclusion(olapInclusion);
            createdObject = olapInclusion;
        } else if (name.equals("olap4j-exclusion")) {
            WabitOlapExclusion olapExclusion = new WabitOlapExclusion(
                    Operator.valueOf(attributes.getValue("operator")), attributes.getValue("unique-member-name"));
            olapDimension.addExclusion(olapExclusion);
            createdObject = olapExclusion;
        } else if (name.equals("wabit-image")) {
            currentWabitImage = new WabitImage();
            createdObject = currentWabitImage;
            session.getWorkspace().addImage(currentWabitImage);

            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("name")) {
                    //already loaded
                } else {
                    logger.warn("Unexpected attribute of <wabit-image>: " + aname + "=" + aval);
                }
            }

        } else if (name.equals("chart")) {
            chart = new Chart();
            createdObject = chart;
            session.getWorkspace().addChart(chart);
            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("uuid")) {
                    //already loaded
                } else if (aname.equals("y-axis-name")) {
                    chart.setYaxisName(aval);
                } else if (aname.equals("x-axis-name")) {
                    chart.setXaxisName(aval);
                } else if (aname.equals("x-axis-label-rotation")) {
                    chart.setXAxisLabelRotation(Double.parseDouble(aval));
                } else if (aname.equals("type")) {
                    chart.setType(ChartType.valueOf(aval));
                } else if (aname.equals("legend-position")) {
                    chart.setLegendPosition(LegendPosition.valueOf(aval));
                } else if (aname.equals("gratuitous-animation")) {
                    chart.setGratuitouslyAnimated(Boolean.parseBoolean(aval));

                } else if (aname.equals("auto-x-axis")) {
                    chart.setAutoXAxisRange(Boolean.parseBoolean(aval));
                } else if (aname.equals("auto-y-axis")) {
                    chart.setAutoXAxisRange(Boolean.parseBoolean(aval));

                } else if (aname.equals("x-axis-max")) {
                    chart.setXAxisMaxRange(Double.parseDouble(aval));
                } else if (aname.equals("y-axis-max")) {
                    chart.setYAxisMaxRange(Double.parseDouble(aval));
                } else if (aname.equals("x-axis-min")) {
                    chart.setXAxisMinRange(Double.parseDouble(aval));
                } else if (aname.equals("y-axis-min")) {
                    chart.setYAxisMinRange(Double.parseDouble(aval));

                } else if (aname.equals("query-id")) {
                    QueryCache query = null;
                    for (QueryCache q : session.getWorkspace().getQueries()) {
                        if (q.getUUID().equals(aval)) {
                            query = q;
                            break;
                        }
                    }
                    if (query != null) {
                        chart.setQuery(query);
                    }
                    OlapQuery olapQuery = null;
                    for (OlapQuery q : session.getWorkspace().getOlapQueries()) {
                        if (q.getUUID().equals(aval)) {
                            olapQuery = q;
                            break;
                        }
                    }
                    if (olapQuery != null) {
                        try {
                            chart.setMagicEnabled(false);
                            chart.setQuery(olapQuery);
                        } finally {
                            chart.setMagicEnabled(true);
                        }
                    }
                    if (query == null && olapQuery == null) {
                        throw new IllegalArgumentException(
                                "The query with UUID " + aval + " is missing from this project.");
                    }
                } else {
                    logger.warn("Unexpected attribute of <chart>: " + aname + "=" + aval);
                }
            }

        } else if (name.equals("chart-column")) {
            ChartColumn colIdentifier = loadColumnIdentifier(attributes, "");
            createdObject = colIdentifier;
            if (colIdentifier == null) {
                throw new IllegalStateException("The chart " + chart.getName() + " with uuid " + chart.getUUID()
                        + " has a missing column identifier when ordering columns and cannot be loaded.");
            }

            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("name")) {
                    //already handled
                } else if (aname.equals("role")) {
                    colIdentifier.setRoleInChart(ColumnRole.valueOf(aval));
                } else if (aname.matches("x-axis-.*")) {
                    ChartColumn xAxisIdentifier = loadColumnIdentifier(attributes, "x-axis-");
                    colIdentifier.setXAxisIdentifier(xAxisIdentifier);
                }
            }

            if (readingMissingChartCols) {
                chart.addMissingIdentifier(colIdentifier);
            } else {
                chart.addChartColumn(colIdentifier);
            }

        } else if (name.equals("missing-columns")) {
            readingMissingChartCols = true;
            createdObject = null;

        } else if (name.equals("layout")) {
            this.isInLayout = true;
            String layoutName = attributes.getValue("name");
            checkMandatory("name", layoutName);
            if (attributes.getValue("template") == null || !Boolean.parseBoolean(attributes.getValue("template"))) {
                layout = new Report(layoutName);
                session.getWorkspace().addReport((Report) layout);
            } else {
                layout = new Template(layoutName);
                session.getWorkspace().addTemplate((Template) layout);
            }
            createdObject = layout;
            selectorContainer = layout;

            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("name")) {
                    //already loaded
                } else if (aname.equals("zoom")) {
                    layout.setZoomLevel(Integer.parseInt(aval));
                } else {
                    logger.warn("Unexpected attribute of <layout>: " + aname + "=" + aval);
                }
            }

        } else if (name.equals("layout-page")) {
            Page page = layout.getPage();
            createdObject = page;
            //Remove all guides from the page as they will be loaded in a later
            //part of this handler.
            for (WabitObject object : page.getChildren()) {
                if (object instanceof Guide) {
                    page.removeGuide((Guide) object);
                }
            }

            //This sets the orientation before setting the width and height to prevent
            //a change in the orientation from switching the width and height. If the
            //orientation changes between portrait and landscape the width and height
            //values are swapped.
            String orientation = attributes.getValue("orientation");
            if (orientation != null) {
                // XXX the null check is for compatibility with export-format 1.0.0
                page.setOrientation(PageOrientation.valueOf(orientation));
            }

            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("name")) {
                    // already loaded
                } else if (aname.equals("height")) {
                    page.setHeight(Integer.parseInt(aval));
                } else if (aname.equals("width")) {
                    page.setWidth(Integer.parseInt(aval));
                } else if (aname.equals("orientation")) {
                    //already loaded
                } else {
                    logger.warn("Unexpected attribute of <layout-page>: " + aname + "=" + aval);
                }
            }
        } else if (name.equals("content-box")) {
            contentBox = new ContentBox();
            createdObject = contentBox;
            selectorContainer = contentBox;
            layout.getPage().addContentBox(contentBox);
            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("name")) {
                    // already loaded
                } else if (aname.equals("width")) {
                    contentBox.setWidth(Double.parseDouble(aval));
                } else if (aname.equals("height")) {
                    contentBox.setHeight(Double.parseDouble(aval));
                } else if (aname.equals("xpos")) {
                    contentBox.setX(Double.parseDouble(aval));
                } else if (aname.equals("ypos")) {
                    contentBox.setY(Double.parseDouble(aval));
                } else {
                    logger.warn("Unexpected attribute of <content-box>: " + aname + "=" + aval);
                }
            }
        } else if (name.equals("content-label")) {
            WabitLabel label = new WabitLabel();
            createdObject = label;
            contentBox.setContentRenderer(label);
            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("name")) {
                    //handled elsewhere
                } else if (aname.equals("text")) {
                    label.setText(aval);
                } else if (aname.equals("horizontal-align")) {
                    label.setHorizontalAlignment(HorizontalAlignment.valueOf(aval));
                } else if (aname.equals("vertical-align")) {
                    label.setVerticalAlignment(VerticalAlignment.valueOf(aval));
                } else if (aname.equals("bg-colour")) {
                    label.setBackgroundColour(new Color(Integer.parseInt(aval)));
                } else {
                    logger.warn("Unexpected attribute of <content-label>: " + aname + "=" + aval);
                }
            }
        } else if (name.equals("text") && parentIs("content-label")) {
            createdObject = null;
        } else if (name.equals("image-renderer")) {
            imageRenderer = new ImageRenderer();
            createdObject = imageRenderer;
            //Old image renderers always had the image in the top left. If
            //the file is new it will have the horizontal and vertical alignments
            //set.
            imageRenderer.setHAlign(HorizontalAlignment.LEFT);
            imageRenderer.setVAlign(VerticalAlignment.TOP);

            contentBox.setContentRenderer(imageRenderer);
            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("name")) {
                    //Handled elsewhere
                } else if (aname.equals("wabit-image-uuid")) {
                    for (WabitImage image : session.getWorkspace().getImages()) {
                        if (image.getUUID().equals(aval)) {
                            imageRenderer.setImage(image);
                            break;
                        }
                    }
                    if (imageRenderer.getImage() == null) {
                        throw new IllegalStateException("Could not load the workspace as the report "
                                + layout.getName() + " is missing the image " + aval);
                    }
                } else if (aname.equals("preserving-aspect-ratio")) {
                    imageRenderer.setPreservingAspectRatio(Boolean.valueOf(aval));
                } else if (aname.equals("h-align")) {
                    imageRenderer.setHAlign(HorizontalAlignment.valueOf(aval));
                } else if (aname.equals("v-align")) {
                    imageRenderer.setVAlign(VerticalAlignment.valueOf(aval));
                } else {
                    logger.warn("Unexpected attribute of <image-renderer>: " + aname + "=" + aval);
                }
            }

        } else if (name.equals("chart-renderer")) {
            String chartUuid = attributes.getValue("chart-uuid");
            Chart chart = session.getWorkspace().findByUuid(chartUuid, Chart.class);
            if (chart == null) {
                throw new IllegalStateException("Missing chart with UUID " + chartUuid + ", which is supposed"
                        + " to be attached to a chart renderer");
            }

            final ChartRenderer chartRenderer = new ChartRenderer(chart);
            createdObject = chartRenderer;
            contentBox.setContentRenderer(chartRenderer);
            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("uuid")) {
                    // handled elsewhere
                } else if (aname.equals("chart-uuid")) {
                    // already handled
                } else if (aname.equals("name")) {
                    // handled elsewhere
                } else {
                    logger.warn("Unexpected attribute of <chart-renderer>: " + aname + "=" + aval);
                }
            }

        } else if (name.equals("content-result-set")) {
            String queryID = attributes.getValue("query-id");
            checkMandatory("query-id", queryID);
            WabitResultSetProducer query = session.getWorkspace().findByUuid(queryID, WabitResultSetProducer.class);
            rsRenderer = new ResultSetRenderer(query);
            createdObject = rsRenderer;
            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("query-id")) {
                    // handled elsewhere
                } else if (aname.equals("name")) {
                    // handled elsewhere
                } else if (aname.equals("null-string")) {
                    rsRenderer.setNullString(aval);
                } else if (aname.equals("bg-colour")) {
                    Color color = new Color(Integer.parseInt(aval));
                    logger.debug("Renderer has background " + color.getRed() + ", " + color.getBlue() + ", "
                            + color.getGreen());
                    rsRenderer.setBackgroundColour(color);
                } else if (aname.equals("data-colour")) {
                    Color color = new Color(Integer.parseInt(aval));
                    rsRenderer.setDataColour(color);
                } else if (aname.equals("header-colour")) {
                    Color color = new Color(Integer.parseInt(aval));
                    rsRenderer.setHeaderColour(color);
                } else if (aname.equals("border")) {
                    rsRenderer.setBorderType(BorderStyles.valueOf(aval));
                } else if (aname.equals("grand-totals")) {
                    rsRenderer.setPrintingGrandTotals(Boolean.parseBoolean(aval));
                } else {
                    logger.warn("Unexpected attribute of <content-result-set>: " + aname + "=" + aval);
                }
            }
            //columnInfoList.clear();
            contentBox.setContentRenderer(rsRenderer);
        } else if (name.equals("header-font")) {
            if (parentIs("content-result-set")) {
                rsRenderer.setHeaderFont(loadFont(attributes));
                createdObject = null;
            } else {
                throw new IllegalStateException("There are no header fonts defined for the parent "
                        + xmlContext.get(xmlContext.size() - 2));
            }
        } else if (name.equals("body-font")) {
            if (parentIs("content-result-set")) {
                createdObject = null;
                rsRenderer.setBodyFont(loadFont(attributes));
            } else {
                throw new IllegalStateException(
                        "There are no body fonts defined for the parent " + xmlContext.get(xmlContext.size() - 2));
            }
        } else if (name.equals("column-info")) {
            colInfo = null;
            createdObject = null; //Not going to set name later, as this may break alias association
            String colInfoName = attributes.getValue("name");
            String colInfoItem = attributes.getValue("column-info-item-id");

            /*
             * XXX This retro compatibility fix forces us to violate
             * proper encapsulation and cannot be maintained if we want to
             * display OLAP queries in a RSRenderer. Imma comment it
             * out for now and evaluate later if we want to keep it.
             * It is for retrocompatibility with 0.9.1, which is a terribly
             * old version anyways. I'm not even sure we can load those files
             * now anyways.
             */
            //           //For backwards compatability with 0.9.1
            //           String colInfoKey = attributes.getValue("column-info-key");
            //           if (colInfoKey != null && colInfoItem == null) {
            //              Query q = rsRenderer.getContent();
            //              for (Map.Entry<String, Item> entry : uuidToItemMap.entrySet()) {
            //                 Item item = entry.getValue();
            //                 if (q.getSelectedColumns().contains(item) && (item.getAlias().equals(colInfoKey) || item.getName().equals(colInfoKey))) {
            //                    colInfoItem = entry.getKey();
            //                    break;
            //                 }
            //              }
            //              if (colInfoItem == null) {
            //                 colInfo = new ColumnInfo(colInfoKey, colInfoName);
            //              }
            //           }

            String colAlias = attributes.getValue("column-alias");
            if (colInfo == null && colAlias != null && colInfoItem == null) {
                colInfo = new ColumnInfo(colAlias, colInfoName);
            }

            checkMandatory("name", colInfoName);
            if (colInfo == null) {
                colInfo = new ColumnInfo(uuidToItemMap.get(colInfoItem), colInfoName);
            }
            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("column-info-key") || aname.equals("name")) {
                    //already loaded
                } else if (aname.equals("uuid")) {
                    colInfo.setUUID(aval);
                } else if (aname.equals("width")) {
                    colInfo.setWidth(Integer.parseInt(aval));
                } else if (aname.equals("horizontal-align")) {
                    colInfo.setHorizontalAlignment(HorizontalAlignment.valueOf(aval));
                } else if (aname.equals("data-type")) {
                    colInfo.setDataType(DataType.valueOf(aval));
                } else if (aname.equals("break-on-column")) {
                    if (Boolean.parseBoolean(aval)) {
                        colInfo.setWillGroupOrBreak(GroupAndBreak.GROUP);
                    } else {
                        colInfo.setWillGroupOrBreak(GroupAndBreak.NONE);
                    }
                } else if (aname.equals("group-or-break")) {
                    colInfo.setWillGroupOrBreak(GroupAndBreak.valueOf(aval));
                } else if (aname.equals("will-subtotal")) {
                    colInfo.setWillSubtotal(Boolean.parseBoolean(aval));
                } else {
                    logger.warn("Unexpected attribute of <column-info>: " + aname + "=" + aval);
                }
            }
            rsRenderer.addChild(colInfo, rsRenderer.getChildren().size());
        } else if (name.equals("date-format")) {
            createdObject = null;
            if (parentIs("column-info")) {
                String format = attributes.getValue("format");
                checkMandatory("format", format);
                colInfo.setFormat(new SimpleDateFormat(format));
            } else {
                throw new IllegalStateException(
                        "There is no date format defined for the parent " + xmlContext.get(xmlContext.size() - 2));
            }
        } else if (name.equals("decimal-format")) {
            createdObject = null;
            if (parentIs("column-info")) {
                String format = attributes.getValue("format");
                checkMandatory("format", format);
                colInfo.setFormat(new DecimalFormat(format));
            } else {
                throw new IllegalStateException(
                        "There is no date format defined for the parent " + xmlContext.get(xmlContext.size() - 2));
            }

        } else if (name.equals("selector")) {

            String type = attributes.getValue("type");

            if (type.equals(ComboBoxSelector.class.getSimpleName())) {
                this.selector = new ComboBoxSelector();
            } else if (type.equals(TextBoxSelector.class.getSimpleName())) {
                this.selector = new TextBoxSelector();
            } else if (type.equals(DateSelector.class.getSimpleName())) {
                this.selector = new DateSelector();
            } else {
                throw new IllegalStateException("Cannot create a selector of type " + type);
            }

            if (selectorContainer == null) {
                throw new AssertionError("Program error. 'selectorContainer' not set.");
            } else if (selectorContainer instanceof Report || selectorContainer instanceof ContentBox) {
                selectorContainer.addChild(selector, selectorContainer.getChildren(Selector.class).size());
            } else {
                throw new IllegalStateException("Selectors can only be added to reports and content boxes..");
            }

            createdObject = this.selector;

        } else if (name.equals("selector-config")) {

            if (selector instanceof ComboBoxSelector) {
                ((ComboBoxSelector) selector).setSourceKey(attributes.getValue("sourceKey"));
                ((ComboBoxSelector) selector).setStaticValues(attributes.getValue("staticValues"));
                ((ComboBoxSelector) selector).setDefaultValue(attributes.getValue("defaultValue"));
                ((ComboBoxSelector) selector).setAlwaysIncludeDefaultValue(
                        Boolean.valueOf(attributes.getValue("alwaysIncludeDefaultValue")));
            } else if (selector instanceof TextBoxSelector) {
                ((TextBoxSelector) selector).setDefaultValue(attributes.getValue("defaultValue"));
            } else if (selector instanceof DateSelector) {
                String defValueS = attributes.getValue("defaultValue");
                final Date defValue;
                if (defValueS == null) {
                    defValue = null;
                } else {
                    defValue = new DateConverter().convertToComplexType(defValueS);
                }
                ((DateSelector) selector).setDefaultValue(defValue);
            } else {
                throw new IllegalStateException("Cannot configure selector.");
            }

            createdObject = null;

        } else if (name.equals("cell-set-renderer")) {
            String queryUUID = attributes.getValue("olap-query-uuid");
            OlapQuery newQuery = null;
            for (OlapQuery query : session.getWorkspace().getOlapQueries()) {
                if (query.getUUID().equals(queryUUID)) {
                    newQuery = query;
                    break;
                }
            }
            if (newQuery == null) {
                throw new NullPointerException("Cannot load workspace due to missing olap query in report.");
            }
            cellSetRenderer = new CellSetRenderer(newQuery);
            createdObject = cellSetRenderer;
            contentBox.setContentRenderer(cellSetRenderer);
            for (int i = 0; i < attributes.getLength(); i++) {
                String aname = attributes.getQName(i);
                String aval = attributes.getValue(i);
                if (aname.equals("uuid") || aname.equals("olap-query-uuid")) {
                    // handled elsewhere
                } else if (aname.equals("name")) {
                    // handled elsewhere
                } else if (aname.equals("body-alignment")) {
                    cellSetRenderer.setBodyAlignment(HorizontalAlignment.valueOf(aval));
                } else if (aname.equals("body-format-pattern")) {
                    cellSetRenderer.setBodyFormat(new DecimalFormat(aval));
                } else {
                    logger.warn("Unexpected attribute of <cell-set-renderer>: " + aname + "=" + aval);
                }
            }

        } else if (name.equals("olap-header-font")) {
            createdObject = null;
            cellSetRenderer.setHeaderFont(loadFont(attributes));
        } else if (name.equals("olap-body-font")) {
            createdObject = null;
            cellSetRenderer.setBodyFont(loadFont(attributes));
        } else if (name.equals("guide")) {
            String axisName = attributes.getValue("axis");
            String offsetAmount = attributes.getValue("offset");
            checkMandatory("axis", axisName);
            checkMandatory("offset", offsetAmount);
            Guide guide = new Guide(Axis.valueOf(axisName), Double.parseDouble(offsetAmount));
            createdObject = guide;
            layout.getPage().addGuide(guide);
        } else if (name.equals("font")) {
            createdObject = null;
            Font font = loadFont(attributes);
            if (parentIs("layout-page")) {
                layout.getPage().setDefaultFont(font);
            } else if (parentIs("content-box")) {
                contentBox.setFont(font);
            } else if (parentIs("content-label")) {
                ((WabitLabel) contentBox.getContentRenderer()).setFont(font);
            }
        } else {
            createdObject = null;
            logger.warn("Unknown object type: " + name);
        }

        if (createdObject != null) {
            String valName = attributes.getValue("name");
            String valUUID = attributes.getValue("uuid");
            if (nameMandatory) {
                checkMandatory("name", valName);
            }
            if (uuidMandatory) {
                checkMandatory("uuid", valUUID);
            }
            if (valName != null) {
                createdObject.setName(valName);
            }
            if (valUUID != null) {
                createdObject.setUUID(valUUID);
            }

            progressMessage = session.getWorkspace().getName() + ": reading " + valName;
        }

    }

    /**
     * This is a helper method for loading {@link ChartColumn}s introduced
     * in the save version 1.0.2 (updated in 1.2.0). Column identifiers are
     * defined by either a column name in a relational query, a row hierarchy
     * for OLAP queries, or a Position of the column axis of an OLAP query.
     * 
     * @param attributes
     *            the attributes of the start tag that introduces this column
     *            identifier
     * @param prefix
     *            the prefix string to apply to every attribute name when
     *            looking up its value. This is intended for use when more than
     *            one column identifier is defined in attributes of a single XML element.
     * @throws SAXException if any mandatory attributes are missing
     */
    private ChartColumn loadColumnIdentifier(Attributes attributes, String prefix) throws SAXException {

        String colName = attributes.getValue(prefix + "name");
        String dataTypeName = attributes.getValue(prefix + "data-type");
        checkMandatory("name", colName);
        checkMandatory("data-type", dataTypeName);
        ChartColumn.DataType dataType = ChartColumn.DataType.valueOf(dataTypeName);
        ChartColumn colIdentifier = new ChartColumn(colName, dataType);
        return colIdentifier;
    }

    /**
     * This method finds a member from a cube based on given attributes.
     */
    public Member findMember(Attributes attributes, Cube cube) {
        String uniqueMemberName = attributes.getValue("unique-member-name");
        if (uniqueMemberName != null) {
            String[] uniqueMemberNameList = uniqueMemberName.split("\\]\\.\\[");
            uniqueMemberNameList[0] = uniqueMemberNameList[0].substring(1); //remove starting [ bracket
            final int lastMemberNamePosition = uniqueMemberNameList.length - 1;
            uniqueMemberNameList[lastMemberNamePosition] = uniqueMemberNameList[lastMemberNamePosition].substring(0,
                    uniqueMemberNameList[lastMemberNamePosition].length() - 1); //remove ending ] bracket
            try {
                return cube.lookupMember(uniqueMemberNameList);
            } catch (OlapException e) {
                throw new RuntimeException(e);
            }

        } else {
            String dimensionName = attributes.getValue("dimension-name");
            String hierarchyName = attributes.getValue("hierarchy-name");
            String levelName = attributes.getValue("member-level");
            String memberName = attributes.getValue("member-name");
            Dimension dimension = cube.getDimensions().get(dimensionName);
            Member actualMember = null;
            final Hierarchy hierarchy = dimension.getHierarchies().get(hierarchyName);
            final Level level = hierarchy.getLevels().get(levelName);
            try {
                for (Member member : level.getMembers()) {
                    if (member.getName().equals(memberName)) {
                        actualMember = member;
                        break;
                    }
                }
            } catch (OlapException e) {
                throw new RuntimeException(e);
            }
            if (actualMember == null) {
                throw new NullPointerException("Cannot find member " + memberName + " in hierarchy " + hierarchyName
                        + " in dimension " + dimensionName);
            }
            return actualMember;
        }
    }

    /**
     * This loads a font based on the given attributes.
     */
    private Font loadFont(Attributes attributes) throws SAXException {
        String fontName = attributes.getValue("name");
        String fontSize = attributes.getValue("size");
        String fontStyle = attributes.getValue("style");
        checkMandatory("name", fontName);
        checkMandatory("style", fontStyle);
        checkMandatory("size", fontSize);
        Font font = new Font(fontName, Integer.parseInt(fontStyle), Integer.parseInt(fontSize));
        return font;
    }

    /**
     * Throws an informative exception if the given value is null.
     * 
     * @param attName Name of the attribute that's supposed to contain the value
     * @param value The value actually recovered from the attribute (if any)
     * @throws SAXException If value is null.
     */
    private void checkMandatory(String attName, Object value) throws SAXException {
        if (value == null) {
            throw new SAXException(
                    "Missing mandatory attribute \"" + attName + "\" of element \"" + xmlContext.peek() + "\"");
        }
    }

    /**
     * Returns true if the name of the parent element in the XML context
     * (the one just below the top of the stack) is the given name.
     * 
     * @param qName The name to check for equality with the parent element name.
     * @return If qName == parent element name
     */
    private boolean parentIs(String qName) {
        return xmlContext.get(xmlContext.size() - 2).equals(qName);
    }

    @Override
    public void endElement(final String uri, final String localName, final String name) throws SAXException {
        if (isCancelled())
            throw new CancellationException();

        final ByteArrayOutputStream copyStream;
        if (byteStream != null) {
            logger.debug("Byte stream contains " + byteStream.size() + " bytes.");
            copyStream = new ByteArrayOutputStream(byteStream.size());
            try {
                byteStream.writeTo(copyStream);
            } catch (IOException ex) {
                logger.error(ex);
                throw new CancellationException("Could not copy the stream. See the logs for more details.");
            }
        } else {
            copyStream = null;
        }
        Runnable runner = new Runnable() {
            public void run() {
                try {
                    context.startLoading();
                    endElementImpl(uri, localName, name, copyStream);
                } catch (SAXException e) {
                    setCancelled(true);
                    throw new RuntimeException(e);
                } finally {
                    context.endLoading();
                }

            }
        };
        session.runInForeground(runner);
    }

    private void endElementImpl(final String uri, final String localName, final String name,
            ByteArrayOutputStream stream) throws SAXException {
        if (isCancelled())
            return;

        if (name.equals("project")) {
            //XXX uncomment the code below when we are confident that Wabit runs all
            //of its queries and other intensive operations on a separate thread.
            //See bug 2040.
            session.getWorkspace().setEditorPanelModel(session.getWorkspace());
            //          session.getWorkspace().setEditorPanelModel(initialView);

        } else if (name.equals("layout")) {
            isInLayout = false;
        } else if (name.equals("table")) {
            TableContainer table = new TableContainer(container.getUUID(), cache.getDatabase(), container.getName(),
                    ((TableContainer) container).getSchema(), ((TableContainer) container).getCatalog(),
                    containerItems);
            table.setPosition(container.getPosition());
            table.setAlias(container.getAlias());
            cache.addTable(table);
        } else if (name.equals("image-renderer")) {
            //This was loading an image for 1.1.2 and older.
            byte[] byteArray = new Base64().decode(stream.toByteArray());
            if (byteArray.length > 0) {
                logger.debug("Decoding byte stream: Stream has " + stream.toString().length() + " and array has "
                        + Arrays.toString(byteArray));
                try {
                    BufferedImage img = ImageIO.read(new ByteArrayInputStream(byteArray));
                    WabitImage wabitImage = new WabitImage();
                    wabitImage.setImage(img);
                    wabitImage.setName(imageRenderer.getName());
                    session.getWorkspace().addImage(wabitImage);
                    imageRenderer.setImage(wabitImage);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            imageRenderer = null;

        } else if (name.equals("wabit-image")) {
            byte[] byteArray = new Base64().decode(stream.toByteArray());
            try {
                BufferedImage img = ImageIO.read(new ByteArrayInputStream(byteArray));
                currentWabitImage.setImage(img);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            currentWabitImage = null;

        } else if (name.equals("missing-columns")) {
            readingMissingChartCols = false;

        } else if (name.equals("chart")) {
            //XXX Fix for broken references in chart columns. The chart columns x axis references
            //are actual new chart columns and therefore have the wrong uuid. The chart column
            //uuids should be stored and used to reference each other instead of name.
            for (ChartColumn column : chart.getColumns()) {
                if (column.getXAxisIdentifier() != null) {
                    for (ChartColumn otherColumn : chart.getColumns()) {
                        if (column.getXAxisIdentifier().getColumnName().equals(otherColumn.getColumnName())) {
                            column.setXAxisIdentifier(otherColumn);
                            break;
                        }
                    }
                }
            }

        } else if (name.equals("text") && parentIs("content-label")) {
            ((WabitLabel) contentBox.getContentRenderer()).setText(stream.toString());

        } else if (name.equals("text") && parentIs("query")) {
            cache.setUserModifiedQuery(stream.toString());
        }

        xmlContext.pop();
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        if (isCancelled())
            throw new CancellationException();

        for (int i = start; i < start + length; i++) {
            byteStream.write((byte) ch[i]);
        }
        if (logger.isDebugEnabled()) {
            logger.debug("Starting characters at " + start + " and ending at " + length);
            logger.debug("Byte stream has " + byteStream.toString());
        }
    }

    public WabitSession getSession() {
        return session;
    }

    public String getMessage() {
        return progressMessage;
    }

    public boolean isCancelled() {
        return cancelled.get();
    }

    public void setCancelled(boolean cancelled) {
        this.cancelled.set(cancelled);
    }

}