org.metawidget.gwt.client.ui.GwtMetawidget.java Source code

Java tutorial

Introduction

Here is the source code for org.metawidget.gwt.client.ui.GwtMetawidget.java

Source

// Metawidget
//
// This file is dual licensed under both the LGPL
// (http://www.gnu.org/licenses/lgpl-2.1.html) and the EPL
// (http://www.eclipse.org/org/documents/epl-v10.php). As a
// recipient of Metawidget, you may choose to receive it under either
// the LGPL or the EPL.
//
// Commercial licenses are also available. See http://metawidget.org
// for details.

package org.metawidget.gwt.client.ui;

import static org.metawidget.inspector.InspectionResultConstants.*;
import static org.metawidget.util.simple.StringUtils.*;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;

import org.metawidget.gwt.client.ui.layout.FlexTableLayout;
import org.metawidget.gwt.client.ui.layout.LabelLayoutDecorator;
import org.metawidget.gwt.client.ui.layout.LabelLayoutDecoratorConfig;
import org.metawidget.gwt.client.widgetbuilder.GwtWidgetBuilder;
import org.metawidget.gwt.client.widgetbuilder.OverriddenWidgetBuilder;
import org.metawidget.gwt.client.widgetbuilder.ReadOnlyWidgetBuilder;
import org.metawidget.gwt.client.widgetprocessor.StyleNameProcessor;
import org.metawidget.inspectionresultprocessor.iface.InspectionResultProcessor;
import org.metawidget.inspector.gwt.remote.client.GwtRemoteInspectorProxy;
import org.metawidget.inspector.iface.Inspector;
import org.metawidget.layout.iface.Layout;
import org.metawidget.util.simple.PathUtils;
import org.metawidget.util.simple.PathUtils.TypeAndNames;
import org.metawidget.util.simple.StringUtils;
import org.metawidget.widgetbuilder.composite.CompositeWidgetBuilder;
import org.metawidget.widgetbuilder.composite.CompositeWidgetBuilderConfig;
import org.metawidget.widgetbuilder.iface.WidgetBuilder;
import org.metawidget.widgetprocessor.iface.WidgetProcessor;

import com.google.gwt.i18n.client.Dictionary;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HasName;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.xml.client.Element;

/**
 * Metawidget for GWT environments.
 * <p>
 * GWT compiles Java to JavaScript, and JavaScript lacks Java's comprehensive reflection support.
 * The only viable Inspector the JavaScript could run would be XmlInspector, and even that would
 * have to be considerably rewritten as GWT supplies its own variant of <code>org.w3c.dom</code>.
 * <p>
 * A more interesting solution is to have the JavaScript client send its objects (via AJAX) to the
 * Java server for inspection. The full power of Java Inspectors can then be brought to bear,
 * including inspecting annotations and server-side configuration files (such as
 * <code>hibernate.cfg.xml</code>).
 * 
 * @author <a href="http://kennardconsulting.com">Richard Kennard</a>
 */

public class GwtMetawidget extends FlowPanel implements HasName {

    //
    // Private statics
    //

    private static final int BUILDING_COMPLETE = 0;

    private static final int BUILDING_IN_PROGRESS = 1;

    private static final int BUILDING_NEEDED = 2;

    /**
     * Delay before rebuilding widgets (in milliseconds).
     * <p>
     * GWT does not define a good 'paint' method to override, so we must call
     * <code>buildWidgets</code> after every <code>invalidateWidgets</code>. Many methods (eg. most
     * setters) trigger <code>invalidateWidgets</code>, however, so we impose a short delay to try
     * and 'batch' multiple <code>buildWidgets</code> requests (and their associated AJAX calls)
     * into one.
     */

    private static final int BUILD_DELAY = 50;

    /**
     * Static cache of the default Inspector.
     * <p>
     * Note this needn't be <code>synchronized</code> because JavaScript is not multi-threaded.
     * <p>
     * <code>GWTMetawidget</code> cannot use our <code>ConfigReader</code>, because that relies
     * heavily on reflection which is not available client-side. Note
     * <code>GwtRemoteInspectorProxy</code> <em>does</em> use <code>ConfigReader</code>.
     */

    private static Inspector DEFAULT_INSPECTOR;

    private static WidgetBuilder<Widget, GwtMetawidget> DEFAULT_WIDGETBUILDER;

    private static WidgetProcessor<Widget, GwtMetawidget> DEFAULT_WIDGETPROCESSOR;

    private static Layout<Widget, Panel, GwtMetawidget> DEFAULT_LAYOUT;

    //
    // Private members
    //

    private Object mToInspect;

    private String mDictionaryName;

    private Dictionary mDictionary;

    private Map<String, Facet> mFacets = new HashMap<String, Facet>();

    private Set<Widget> mExistingWidgets = new HashSet<Widget>();

    private Set<Widget> mExistingUnusedWidgets = new HashSet<Widget>();

    /**
     * Map of widgets added to this Metawidget.
     * <p>
     * Searching for Widgets by traversing children is complicated in GWT, because not all Widgets
     * that contain child Widgets extend a common base class. For example, both ComplexPanel and
     * FlexTable can contain child Widgets but have very different APIs. It is easier to keep a
     * separate Map of the widgets we have encountered.
     */

    private Map<String, Widget> mAddedWidgets = new HashMap<String, Widget>();

    private Timer mBuildWidgets;

    private Map<Object, Object> mClientProperties;

    //
    // Package-private members
    //

    /* package private */String mPath;

    /**
     * Name used to implement <code>HasName</code>. Subtly different from <code>mPath</code> and
     * <code>mNamesPrefix</code>.
     */

    /* package private */String mName;

    /* package private */int mNeedToBuildWidgets;

    /* package private */Element mLastInspection;

    /* package private */boolean mIgnoreAddRemove;

    /**
     * For unit tests.
     */

    /* package private */Timer mExecuteAfterBuildWidgets;

    /* package private */Pipeline mPipeline;

    //
    // Constructor
    //

    public GwtMetawidget() {

        mPipeline = newPipeline();
    }

    //
    // Public methods
    //

    /**
     * Gets the object being inspected.
     * <p>
     * Exposed for binding implementations.
     * 
     * @return the object. Note this return type uses generics, so as to not require a cast by the
     *         caller (eg. <code>Person p = getToInspect()</code>)
     */

    @SuppressWarnings("unchecked")
    public <T> T getToInspect() {

        return (T) mToInspect;
    }

    /**
     * Sets the Object to inspect.
     * <p>
     * If <code>setPath</code> has not been set, or points to a previous <code>setToInspect</code>,
     * sets it to point to the given Object.
     */

    public void setToInspect(Object toInspect) {

        updateToInspectWithoutInvalidate(toInspect);
        invalidateInspection();
    }

    /**
     * Updates the Object to inspect, without invalidating the previous inspection results.
     * <p>
     * <strong>This is an internal API exposed for WidgetProcessor rebinding support. Clients should
     * not call it directly.</strong>
     */

    public void updateToInspectWithoutInvalidate(Object toInspect) {

        if (mToInspect == null) {
            if (mPath == null && toInspect != null) {
                mPath = toInspect.getClass().getName();
            }
        } else if (mToInspect.getClass().getName().equals(mPath)) {
            if (toInspect == null) {
                mPath = null;
            } else {
                mPath = toInspect.getClass().getName();
            }
        }

        mToInspect = toInspect;
    }

    /**
     * Sets the path to be inspected.
     * <p>
     * Note <code>setPath</code> is quite different to
     * <code>com.google.gwt.user.client.ui.HasName.setName</code>. <code>setPath</code> is always in
     * relation to <code>setToInspect</code>, so must include the type name and any subsequent
     * sub-names (eg. type/name/name). Conversely, <code>setName</code> is a single name relative to
     * our immediate parent. <code>setName</code> is only implemented so that
     * <code>GwtMetawidget</code>s can be used directly as overridden widgets (i.e. without needing
     * to be wrapped in a <code>Stub</code>).
     */

    public void setPath(String path) {

        mPath = path;
        invalidateInspection();
    }

    public String getPath() {

        return mPath;
    }

    /**
     * Implements the <code>com.google.gwt.user.client.ui.HasName</code> interface.
     * <p>
     * Useful so that <code>GwtMetawidget</code>s can be used directly as overridden widgets (i.e.
     * without needing to be wrapped in a <code>Stub</code>). Unlike <code>setPath</code>, has no
     * effect on inspection.
     */

    public void setName(String name) {

        mName = name;
    }

    /**
     * Implements the <code>com.google.gwt.user.client.ui.HasName</code> interface.
     * <p>
     * Useful so that <code>GwtMetawidget</code>s can be used directly as overridden widgets (i.e.
     * without needing to be wrapped in a <code>Stub</code>). Unlike <code>setPath</code>, has no
     * effect on inspection.
     */

    public String getName() {

        return mName;
    }

    public void setInspector(Inspector inspector) {

        mPipeline.setInspector(inspector);
        invalidateInspection();
    }

    /**
     * Useful for WidgetBuilders to perform nested inspections (eg. for Collections).
     */

    public String inspect(Object toInspect, String type, String... names) {

        return mPipeline.inspect(toInspect, type, names);
    }

    public void addInspectionResultProcessor(InspectionResultProcessor<GwtMetawidget> inspectionResultProcessor) {

        mPipeline.addInspectionResultProcessor(inspectionResultProcessor);
        invalidateInspection();
    }

    public void setWidgetBuilder(WidgetBuilder<Widget, GwtMetawidget> widgetBuilder) {

        mPipeline.setWidgetBuilder(widgetBuilder);
        invalidateWidgets();
    }

    public void addWidgetProcessor(WidgetProcessor<Widget, GwtMetawidget> widgetProcessor) {

        mPipeline.addWidgetProcessor(widgetProcessor);
        invalidateWidgets();
    }

    public <T> T getWidgetProcessor(Class<T> widgetProcessorClass) {

        return mPipeline.getWidgetProcessor(widgetProcessorClass);
    }

    public void setLayout(Layout<Widget, Panel, GwtMetawidget> layout) {

        mPipeline.setLayout(layout);
        invalidateWidgets();
    }

    /**
     * Set the Dictionary name for localization.
     * <p>
     * The Dictionary name must be a JavaScript variable declared in the host HTML page.
     */

    public void setDictionaryName(String dictionaryName) {

        mDictionaryName = dictionaryName;
        mDictionary = null;
        invalidateWidgets();
    }

    /**
     * Returns a label for the given set of attributes.
     * <p>
     * The label is determined using the following algorithm:
     * <p>
     * <ul>
     * <li>if <tt>attributes.get( "label" )</tt> exists...
     * <ul>
     * <li><tt>attributes.get( "label" )</tt> is camel-cased and used as a lookup into
     * <tt>getLocalizedKey( camelCasedLabel )</tt>. This means developers can initially build their
     * UIs without worrying about localization, then turn it on later</li>
     * <li>if no such lookup exists, return <tt>attributes.get( "label" )</tt>
     * </ul>
     * </li>
     * <li>if <tt>attributes.get( "label" )</tt> does not exist...
     * <ul>
     * <li><tt>attributes.get( "name" )</tt> is used as a lookup into
     * <tt>getLocalizedKey( name )</tt></li>
     * <li>if no such lookup exists, return <tt>attributes.get( "name" )</tt>
     * </ul>
     * </li>
     * </ul>
     */

    public String getLabelString(Map<String, String> attributes) {

        if (attributes == null) {
            return "";
        }

        // Explicit label

        String label = attributes.get(LABEL);

        if (label != null) {
            // (may be forced blank)

            if ("".equals(label)) {
                return null;
            }

            // (localize if possible)

            String localized = getLocalizedKey(StringUtils.camelCase(label));

            if (localized != null) {
                return localized.trim();
            }

            return label.trim();
        }

        // Default name

        String name = attributes.get(NAME);

        if (name != null) {
            // (localize if possible)

            String localized = getLocalizedKey(name);

            if (localized != null) {
                return localized.trim();
            }

            return StringUtils.uncamelCase(name);
        }

        return "";
    }

    /**
     * @return null if no bundle, ???key??? if bundle is missing a key
     */

    public String getLocalizedKey(String key) {

        if (mDictionaryName == null) {
            return null;
        }

        try {
            if (mDictionary == null) {
                mDictionary = Dictionary.getDictionary(mDictionaryName);
            }

            return mDictionary.get(key);
        } catch (MissingResourceException e) {
            return StringUtils.RESOURCE_KEY_NOT_FOUND_PREFIX + key + StringUtils.RESOURCE_KEY_NOT_FOUND_SUFFIX;
        }
    }

    public void setReadOnly(boolean readOnly) {

        if (mPipeline.isReadOnly() == readOnly) {
            return;
        }

        mPipeline.setReadOnly(readOnly);
        invalidateWidgets();
    }

    public boolean isReadOnly() {

        return mPipeline.isReadOnly();
    }

    public int getMaximumInspectionDepth() {

        return mPipeline.getMaximumInspectionDepth();
    }

    public void setMaximumInspectionDepth(int maximumInspectionDepth) {

        mPipeline.setMaximumInspectionDepth(maximumInspectionDepth);
        invalidateWidgets();
    }

    /**
     * Gets the widget with the given name.
     */

    @SuppressWarnings("unchecked")
    public <T extends Widget> T getWidget(String... names) {

        if (names == null) {
            return null;
        }

        if (mNeedToBuildWidgets != BUILDING_COMPLETE) {
            throw new RuntimeException(
                    "Widgets still building asynchronously: need to complete before calling getWidget( \""
                            + GwtUtils.toString(names, SEPARATOR_DOT_CHAR) + "\" )");
        }

        Map<String, Widget> children = mAddedWidgets;

        for (int loop = 0, length = names.length; loop < length; loop++) {
            if (children == null) {
                return null;
            }

            String name = names[loop];
            Widget widget = children.get(name);

            if (widget == null) {
                return null;
            }

            if (loop == length - 1) {
                return (T) widget;
            }

            if (!(widget instanceof GwtMetawidget)) {
                return null;
            }

            children = ((GwtMetawidget) widget).mAddedWidgets;
        }

        return null;
    }

    /**
     * Gets the value from the Widget with the given name.
     * <p>
     * The value is returned as it is stored in the Widget (eg. String for TextBox) so may need some
     * conversion before being reapplied to the object being inspected. This obviously requires
     * knowledge of which Widget GwtMetawidget created, which is not ideal, so clients may prefer to
     * use binding instead.
     * 
     * @return the value. Note this return type uses generics, so as to not require a cast by the
     *         caller (eg. <code>String s = getValue(names)</code>)
     */

    @SuppressWarnings("unchecked")
    public <T> T getValue(String... names) {

        Widget widget = getWidget(names);

        if (widget == null) {
            throw new RuntimeException("No such widget " + GwtUtils.toString(names, SEPARATOR_DOT_CHAR));
        }

        return (T) getValue(widget);
    }

    /**
     * Gets the value from the given Widget.
     * 
     * @return the value. Note this return type uses generics, so as to not require a cast by the
     *         caller (eg. <code>String s = getValue(widget)</code>)
     */

    @SuppressWarnings("unchecked")
    public <T> T getValue(Widget widget) {

        return (T) getValue(widget, mPipeline.getWidgetBuilder());
    }

    /**
     * Sets the Widget with the given name to the specified value.
     * <p>
     * Clients must ensure the value is of the correct type to suit the Widget (eg. String for
     * TextBox). This obviously requires knowledge of which Widget GwtMetawidget created, which is
     * not ideal, so clients may prefer to use binding instead.
     */

    public void setValue(Object value, String... names) {

        Widget widget = getWidget(names);

        if (widget == null) {
            throw new RuntimeException("No such widget " + GwtUtils.toString(names, SEPARATOR_DOT_CHAR));
        }

        setValue(value, widget);
    }

    /**
     * Sets the given Widget to the specified value.
     */

    public void setValue(Object value, Widget widget) {

        if (!setValue(value, widget, mPipeline.getWidgetBuilder())) {
            throw new RuntimeException("Don't know how to setValue of a " + widget.getClass().getName());
        }
    }

    public Facet getFacet(String name) {

        return mFacets.get(name);
    }

    /**
     * Storage area for WidgetProcessors, Layouts, and other stateless clients.
     */

    public void putClientProperty(Object key, Object value) {

        if (mClientProperties == null) {
            mClientProperties = new HashMap<Object, Object>();
        }

        mClientProperties.put(key, value);
    }

    /**
     * Storage area for WidgetProcessors, Layouts, and other stateless clients.
     */

    @SuppressWarnings("unchecked")
    public <T> T getClientProperty(Object key) {

        if (mClientProperties == null) {
            return null;
        }

        return (T) mClientProperties.get(key);
    }

    @Override
    public boolean remove(int index) {

        if (!mIgnoreAddRemove) {
            invalidateWidgets();

            Widget widget = getChildren().get(index);

            if (widget instanceof Facet) {
                mFacets.remove(((Facet) widget).getName());
            } else {
                mExistingWidgets.remove(widget);
            }
        }

        return super.remove(index);
    }

    @Override
    public boolean remove(Widget widget) {

        if (!mIgnoreAddRemove) {
            invalidateWidgets();

            if (widget instanceof Facet) {
                mFacets.remove(((Facet) widget).getName());
            } else {
                mExistingWidgets.remove(widget);
            }
        }

        return super.remove(widget);
    }

    @Override
    public void clear() {

        super.clear();

        if (!mIgnoreAddRemove) {
            invalidateWidgets();

            mFacets.clear();
            mExistingWidgets.clear();
        }
    }

    /**
     * Fetch a list of <code>Widgets</code> that were added manually, and have so far not been used.
     * <p>
     * <strong>This is an internal API exposed for OverriddenWidgetBuilder. Clients should not call
     * it directly.</strong>
     */

    public Set<Widget> fetchExistingUnusedWidgets() {

        return mExistingUnusedWidgets;
    }

    //
    // Protected methods
    //

    /**
     * Instantiate the Pipeline used by this Metawidget.
     * <p>
     * Subclasses wishing to use their own Pipeline should override this method to instantiate their
     * version.
     */

    protected Pipeline newPipeline() {

        return new Pipeline();
    }

    protected void configure() {

        // Sensible defaults
        //
        // We cannot use ConfigReader, because GWT's client-side JavaScript is not up to it

        if (mPipeline.getInspector() == null) {
            if (DEFAULT_INSPECTOR == null) {
                DEFAULT_INSPECTOR = new GwtRemoteInspectorProxy();
            }

            mPipeline.setInspector(DEFAULT_INSPECTOR);
        }

        if (mPipeline.getWidgetBuilder() == null) {
            if (DEFAULT_WIDGETBUILDER == null) {
                @SuppressWarnings("unchecked")
                CompositeWidgetBuilderConfig<Widget, GwtMetawidget> config = new CompositeWidgetBuilderConfig<Widget, GwtMetawidget>()
                        .setWidgetBuilders(new OverriddenWidgetBuilder(), new ReadOnlyWidgetBuilder(),
                                new GwtWidgetBuilder());
                DEFAULT_WIDGETBUILDER = new CompositeWidgetBuilder<Widget, GwtMetawidget>(config);
            }

            mPipeline.setWidgetBuilder(DEFAULT_WIDGETBUILDER);
        }

        if (mPipeline.getWidgetProcessors() == null) {
            if (DEFAULT_WIDGETPROCESSOR == null) {
                DEFAULT_WIDGETPROCESSOR = new StyleNameProcessor();
            }

            mPipeline.addWidgetProcessor(DEFAULT_WIDGETPROCESSOR);
        }

        if (mPipeline.getLayout() == null) {
            if (DEFAULT_LAYOUT == null) {
                DEFAULT_LAYOUT = new LabelLayoutDecorator(
                        new LabelLayoutDecoratorConfig().setLayout(new FlexTableLayout()));
            }

            mPipeline.setLayout(DEFAULT_LAYOUT);
        }
    }

    @Override
    protected void add(Widget child, com.google.gwt.user.client.Element container) {

        if (!mIgnoreAddRemove) {
            invalidateWidgets();

            if (child instanceof Facet) {
                Facet facet = (Facet) child;
                mFacets.put(facet.getName(), facet);
            } else {
                mExistingWidgets.add(child);
            }

            // Because of the lag between invalidateWidgets() and buildWidgets(), and
            // because some CSS styles aren't applied until buildWidgets(), we
            // see a visual 'glitch' when adding new widgets (like buttons). To stop
            // this, we don't call super.add directly when !mIgnoreAddRemove

            return;
        }

        super.add(child, container);
    }

    @Override
    protected void insert(Widget child, com.google.gwt.user.client.Element container, int beforeIndex,
            boolean domInsert) {

        if (!mIgnoreAddRemove) {
            invalidateWidgets();

            if (child instanceof Facet) {
                Facet facet = (Facet) child;
                mFacets.put(facet.getName(), facet);
            } else {
                mExistingWidgets.add(child);
            }

            // Because of the lag between invalidateWidgets() and buildWidgets(), and
            // because some CSS styles aren't applied until buildWidgets(), we
            // see a visual 'glitch' when inserting new widgets (like buttons). To stop
            // this, we don't call super.insert directly when !mIgnoreAddRemove

            return;
        }

        super.insert(child, container, beforeIndex, domInsert);
    }

    /**
     * Invalidates the current inspection result (if any) <em>and</em> invalidates the widgets.
     * <p>
     * As an optimisation we only invalidate the widgets, not the entire inspection result, for some
     * operations (such as adding/removing stubs, changing read-only etc.)
     */

    protected void invalidateInspection() {

        mLastInspection = null;
        invalidateWidgets();
    }

    /**
     * Invalidates the widgets.
     * <p>
     * If the widgets are already invalidated, but rebuilding is not yet in progress, cancels the
     * pending rebuild and resets the timer. This tries to 'batch' multiple invalidate requests into
     * one.
     */

    protected void invalidateWidgets() {

        // If widgets are already invalidated...

        if (mNeedToBuildWidgets == BUILDING_NEEDED) {
            // ...cancel the pending rebuild...

            mBuildWidgets.cancel();
        } else {
            mNeedToBuildWidgets = BUILDING_NEEDED;

            // ...otherwise, clear the widgets

            super.clear();
            mAddedWidgets.clear();
        }

        // Schedule a new build

        mBuildWidgets = new Timer() {

            @Override
            public void run() {

                buildWidgets();
            }
        };

        mBuildWidgets.schedule(BUILD_DELAY);
    }

    /**
     * Builds the widgets.
     * <p>
     * Unlike <code>buildWidgets</code> in other Metawidget implementations, this method may be
     * asynchronous. If the <code>GwtMetawidget</code> is using an <code>GwtInspectorAsync</code>
     * Inspector (which it does by default), clients should not expect the widgets to be built by
     * the time this method returns.
     */

    protected void buildWidgets() {

        // No need to build?

        if (mNeedToBuildWidgets != BUILDING_NEEDED) {
            // For unit tests: if buildWidgets is already underway, rely on
            // mExecuteAfterBuildWidgets being injected into it. This is preferrable to running
            // buildWidgets() twice without calling invalidateWidgets()

            if (mNeedToBuildWidgets == BUILDING_COMPLETE && mExecuteAfterBuildWidgets != null) {
                Timer executeAfterBuildWidgets = mExecuteAfterBuildWidgets;
                mExecuteAfterBuildWidgets = null;

                executeAfterBuildWidgets.run();
            }

            return;
        }

        mNeedToBuildWidgets = BUILDING_IN_PROGRESS;

        // TODO: test configureOnce

        mPipeline.configureOnce();

        if (mToInspect != null) {
            Inspector inspector = mPipeline.getInspector();

            if (mLastInspection == null) {
                // Special support for GwtRemoteInspectorProxy

                if (inspector instanceof GwtRemoteInspectorProxy) {
                    TypeAndNames typeAndNames = PathUtils.parsePath(mPath);
                    ((GwtRemoteInspectorProxy) inspector).inspect(mToInspect, typeAndNames.getType(),
                            typeAndNames.getNamesAsArray(), new AsyncCallback<String>() {

                                public void onFailure(Throwable caught) {

                                    GwtUtils.alert(caught);

                                    mNeedToBuildWidgets = BUILDING_COMPLETE;
                                }

                                public void onSuccess(String inspectionResult) {

                                    mLastInspection = mPipeline.stringToElement(inspectionResult);

                                    try {
                                        mIgnoreAddRemove = true;
                                        mPipeline.buildWidgets(mLastInspection);
                                    } catch (Exception e) {
                                        GwtUtils.alert(e);
                                    } finally {
                                        mIgnoreAddRemove = false;
                                    }

                                    mNeedToBuildWidgets = BUILDING_COMPLETE;

                                    // For unit tests

                                    if (mExecuteAfterBuildWidgets != null) {
                                        Timer executeAfterBuildWidgets = mExecuteAfterBuildWidgets;
                                        mExecuteAfterBuildWidgets = null;

                                        executeAfterBuildWidgets.run();
                                    }
                                }
                            });

                    return;
                }
            }

            // Regular GwtInspectors

            try {
                mIgnoreAddRemove = true;

                if (mLastInspection == null) {
                    TypeAndNames typeAndNames = PathUtils.parsePath(mPath);
                    mLastInspection = mPipeline.inspectAsDom(mToInspect, typeAndNames.getType(),
                            typeAndNames.getNamesAsArray());
                }

                mPipeline.buildWidgets(mLastInspection);
            } catch (Exception e) {
                GwtUtils.alert(e);
            } finally {
                mIgnoreAddRemove = false;
            }

            mNeedToBuildWidgets = BUILDING_COMPLETE;

            // For unit tests

            if (mExecuteAfterBuildWidgets != null) {
                mExecuteAfterBuildWidgets.run();
                mExecuteAfterBuildWidgets = null;
            }
        }
    }

    protected void startBuild() {

        mExistingUnusedWidgets = new HashSet<Widget>(mExistingWidgets);
    }

    /**
     * @param elementName
     *            XML node name of the business field. Typically 'entity', 'property' or 'action'.
     *            Never null
     */

    protected void layoutWidget(Widget widget, String elementName, Map<String, String> attributes) {

        String name = attributes.get(NAME);
        mAddedWidgets.put(name, widget);
    }

    /**
     * Hook so subclasses can change which class gets created.
     */

    protected GwtMetawidget buildNestedMetawidget() {

        return new GwtMetawidget();
    }

    protected void initNestedMetawidget(GwtMetawidget nestedMetawidget, Map<String, String> attributes)
            throws Exception {

        mPipeline.initNestedPipeline(nestedMetawidget.mPipeline, attributes);
        nestedMetawidget.setPath(mPath + StringUtils.SEPARATOR_FORWARD_SLASH_CHAR + attributes.get(NAME));
        nestedMetawidget.setDictionaryName(mDictionaryName);
        nestedMetawidget.setToInspect(mToInspect);
    }

    protected void endBuild() {

        if (mExistingUnusedWidgets != null) {
            Layout<Widget, Panel, GwtMetawidget> layout = mPipeline.getLayout();
            for (Widget widgetExisting : mExistingUnusedWidgets) {
                Map<String, String> miscAttributes = new HashMap<String, String>();

                // Manually created components default to no section

                miscAttributes.put(SECTION, "");

                if (widgetExisting instanceof Stub) {
                    Map<String, String> stubAttributes = ((Stub) widgetExisting).getAttributes();

                    if (stubAttributes != null) {
                        miscAttributes.putAll(stubAttributes);
                    }
                }

                layout.layoutWidget(widgetExisting, PROPERTY, miscAttributes, this, this);
            }
        }
    }

    //
    // Private members
    //

    private Object getValue(Widget widget, WidgetBuilder<Widget, GwtMetawidget> widgetBuilder) {

        // Recurse into CompositeWidgetBuilders

        if (widgetBuilder instanceof CompositeWidgetBuilder<?, ?>) {
            for (WidgetBuilder<Widget, GwtMetawidget> widgetBuilderChild : ((CompositeWidgetBuilder<Widget, GwtMetawidget>) widgetBuilder)
                    .getWidgetBuilders()) {
                Object value = getValue(widget, widgetBuilderChild);

                if (value != null) {
                    return value;
                }
            }

            return null;
        }

        // Interrogate GwtValueAccessors

        if (widgetBuilder instanceof GwtValueAccessor) {
            return ((GwtValueAccessor) widgetBuilder).getValue(widget);
        }

        return null;
    }

    private boolean setValue(Object value, Widget widget, WidgetBuilder<Widget, GwtMetawidget> widgetBuilder) {

        // Recurse into CompositeWidgetBuilders

        if (widgetBuilder instanceof CompositeWidgetBuilder<?, ?>) {
            for (WidgetBuilder<Widget, GwtMetawidget> widgetBuilderChild : ((CompositeWidgetBuilder<Widget, GwtMetawidget>) widgetBuilder)
                    .getWidgetBuilders()) {
                if (setValue(value, widget, widgetBuilderChild)) {
                    return true;
                }
            }

            return false;
        }

        // Interrogate GwtValueAccessors

        if (widgetBuilder instanceof GwtValueAccessor) {
            return ((GwtValueAccessor) widgetBuilder).setValue(widget, value);
        }

        return false;
    }

    //
    // Inner class
    //

    protected class Pipeline extends GwtPipeline<Widget, Panel, GwtMetawidget> {

        //
        // Protected methods
        //

        @Override
        protected void configure() {

            GwtMetawidget.this.configure();
        }

        @Override
        protected void startBuild() {

            GwtMetawidget.this.startBuild();
            super.startBuild();
        }

        @Override
        protected GwtMetawidget buildNestedMetawidget(Map<String, String> attributes) throws Exception {

            GwtMetawidget nestedMetawidget = GwtMetawidget.this.buildNestedMetawidget();
            GwtMetawidget.this.initNestedMetawidget(nestedMetawidget, attributes);

            return nestedMetawidget;
        }

        @Override
        protected Map<String, String> getAdditionalAttributes(Widget widget) {

            if (widget instanceof Stub) {
                return ((Stub) widget).getAttributes();
            }

            return null;
        }

        @Override
        protected void layoutWidget(Widget widget, String elementName, Map<String, String> attributes) {

            GwtMetawidget.this.layoutWidget(widget, elementName, attributes);
            super.layoutWidget(widget, elementName, attributes);
        }

        @Override
        protected void endBuild() {

            GwtMetawidget.this.endBuild();
            super.endBuild();
        }

        @Override
        protected GwtMetawidget getPipelineOwner() {

            return GwtMetawidget.this;
        }
    }
}