org.apache.cocoon.forms.util.WidgetFinder.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.cocoon.forms.util.WidgetFinder.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.cocoon.forms.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.cocoon.forms.event.RepeaterEvent;
import org.apache.cocoon.forms.event.RepeaterEventAction;
import org.apache.cocoon.forms.event.RepeaterListener;
import org.apache.cocoon.forms.event.WidgetEventMulticaster;
import org.apache.cocoon.forms.formmodel.Repeater;
import org.apache.cocoon.forms.formmodel.Widget;
import org.apache.commons.lang.StringUtils;

/**
 * An utility class to manage list of widgets.
 * 
 * <p>
 * The {@link org.apache.cocoon.forms.formmodel.Widget#lookupWidget(String)} method is able
 * to only return one widget, while this class returns a list of widgets. It uses a path syntax containing a /./,
 * <code>repeater/./foo</code>, which repreesents all the instances of the foo widget inside the repeater,
 * one per row. Note that it also supports finding a widgets inside multi level repeaters, something like 
 * invoices/./movements/./amount or courseYears/./exams/./preparatoryCourses/./title . 
 * </p>
 * <p>
 * Class has been designed to offer good performances, since the widget list is built only once and
 * is automatically updated when a repeater row is added or removed.
 * {@link org.apache.cocoon.forms.event.RepeaterListener}s can be attached directly to receive notifications
 * of widget additions or removals. 
 * </p>
 * <p>
 * This class is used in {@link org.apache.cocoon.forms.formmodel.CalculatedField}s and 
 * {@link org.apache.cocoon.forms.formmodel.CalculatedFieldAlgorithm}s. 
 * </p>
 * @version $Id$
 */
public class WidgetFinder {

    private boolean keepUpdated = false;

    // Holds all the widgets not child of a repeater.
    private List noRepeaterWidgets = null;
    // Map repeater -> Set of Strings containing paths
    private Map repeaterPaths = null;
    // Map repeater -> Set of Widgets
    private Map repeaterWidgets = null;
    // A List of recently added widgets, will get cleared when getNewAdditions is called.
    private List newAdditions = new ArrayList();

    private RefreshingRepeaterListener refreshingListener = new RefreshingRepeaterListener();

    private RepeaterListener listener;

    /**
     * Searches for widgets. It will iterate on the given paths and find all
     * corresponding widgets. If a path is in the forms repeater/* /widget
     * then all the rows of the repeater will be iterated and subwidgets
     * will be fetched. 
     * @param context The context widget to start from.
     * @param paths An iterator of Strings containing the paths.
     * @param keepUpdated If true, listeners will be installed on repeaters
     * to keep lists updated without polling.
     */
    public WidgetFinder(Widget context, Iterator paths, boolean keepUpdated) {
        this.keepUpdated = keepUpdated;
        while (paths.hasNext()) {
            String path = (String) paths.next();
            path = toAsterisk(path);
            if (path.indexOf('*') == -1) {
                addSimpleWidget(context, path);
            } else {
                recurseRepeaters(context, path, true);
            }
        }
    }

    /**
     * Searches for widgets. If path is in the forms repeater/* /widget
     * then all the rows of the repeater will be iterated and subwidgets
     * will be fetched. 
     * @param context The context widget to start from.
     * @param path Path to search for..
     * @param keepUpdated If true, listeners will be installed on repeaters
     * to keep lists updated without polling.
     */
    public WidgetFinder(Widget context, String path, boolean keepUpdated) {
        path = toAsterisk(path);
        this.keepUpdated = keepUpdated;
        if (path.indexOf('*') == -1) {
            addSimpleWidget(context, path);
        } else {
            recurseRepeaters(context, path, true);
        }
    }

    private String toAsterisk(String path) {
        return StringUtils.replace(path, "/./", "/*/");
    }

    /**
     * Recurses a repeater path with asterisk.
     * @param context The context widget.
     * @param path The path.
     */
    private void recurseRepeaters(Widget context, String path, boolean root) {
        String reppath = path.substring(0, path.indexOf('*') - 1);
        String childpath = path.substring(path.indexOf('*') + 2);
        Widget wdg = context.lookupWidget(reppath);
        if (wdg == null) {
            if (root) {
                throw new IllegalArgumentException(
                        "Cannot find a repeater with path " + reppath + " relative to widget " + context.getName());
            } else {
                return;
            }
        }
        if (!(wdg instanceof Repeater)) {
            throw new IllegalArgumentException("The widget with path " + reppath + " relative to widget "
                    + context.getName() + " is not a repeater!");
        }
        Repeater repeater = (Repeater) wdg;
        if (context instanceof Repeater.RepeaterRow) {
            // Add this repeater to the repeater widgets
            addRepeaterWidget((Repeater) context.getParent(), repeater);
        }

        addRepeaterPath(repeater, childpath);
        if (childpath.indexOf('*') != -1) {
            for (int i = 0; i < repeater.getSize(); i++) {
                Repeater.RepeaterRow row = repeater.getRow(i);
                recurseRepeaters(row, childpath, false);
            }
        } else {
            for (int i = 0; i < repeater.getSize(); i++) {
                Repeater.RepeaterRow row = repeater.getRow(i);
                Widget okwdg = row.lookupWidget(childpath);
                if (okwdg != null) {
                    addRepeaterWidget(repeater, okwdg);
                }
            }
        }
    }

    /**
     * Adds to the list a widget descendant of a repeater.
     * @param repeater The repeater.
     * @param okwdg The widget.
     */
    private void addRepeaterWidget(Repeater repeater, Widget okwdg) {
        if (this.repeaterWidgets == null)
            this.repeaterWidgets = new HashMap();
        Set widgets = (Set) this.repeaterWidgets.get(repeater);
        if (widgets == null) {
            widgets = new HashSet();
            this.repeaterWidgets.put(repeater, widgets);
        }
        widgets.add(okwdg);
        newAdditions.add(okwdg);
    }

    /**
     * Adds a repeater monitored path.
     * @param repeater The repeater.
     * @param childpath The child part of the path.
     */
    private void addRepeaterPath(Repeater repeater, String childpath) {
        if (this.repeaterPaths == null)
            this.repeaterPaths = new HashMap();
        Set paths = (Set) this.repeaterPaths.get(repeater);
        if (paths == null) {
            paths = new HashSet();
            this.repeaterPaths.put(repeater, paths);
            if (keepUpdated)
                repeater.addRepeaterListener(refreshingListener);
        }
        paths.add(childpath);
    }

    /**
     * Called when a new row addition event is received from a monitored repeater.
     * @param repeater The repeated that generated the event.
     * @param index The new row index.
     */
    protected void refreshForAdd(Repeater repeater, int index) {
        Repeater.RepeaterRow row = repeater.getRow(index);
        if (this.repeaterPaths == null)
            this.repeaterPaths = new HashMap();
        Set paths = (Set) this.repeaterPaths.get(repeater);
        for (Iterator iter = paths.iterator(); iter.hasNext();) {
            String path = (String) iter.next();
            if (path.indexOf('*') != -1) {
                recurseRepeaters(row, path, false);
            } else {
                Widget wdg = row.lookupWidget(path);
                if (wdg == null) {
                    throw new IllegalStateException("Even after row addition cannot find a widget with path " + path
                            + " in repeater " + repeater.getName());
                }
                addRepeaterWidget(repeater, wdg);
            }
        }
    }

    /**
     * Called when a row deletion event is received from a monitored repeater.
     * @param repeater The repeated that generated the event.
     * @param index The deleted row index.
     */
    protected void refreshForDelete(Repeater repeater, int index) {
        Repeater.RepeaterRow row = repeater.getRow(index);
        Set widgets = (Set) this.repeaterWidgets.get(repeater);
        for (Iterator iter = widgets.iterator(); iter.hasNext();) {
            Widget widget = (Widget) iter.next();
            boolean ischild = false;
            Widget parent = widget.getParent();
            while (parent != null) {
                if (parent == row) {
                    ischild = true;
                    break;
                }
                parent = parent.getParent();
            }
            if (ischild) {
                iter.remove();
                if (widget instanceof Repeater) {
                    if (this.repeaterPaths != null)
                        this.repeaterPaths.remove(widget);
                    this.repeaterWidgets.remove(widget);
                }
            }
        }
    }

    /**
     * Called when a repeater clear event is received from a monitored repeater.
     * @param repeater The repeated that generated the event.
     */
    protected void refreshForClear(Repeater repeater) {
        Set widgets = (Set) this.repeaterWidgets.get(repeater);
        for (Iterator iter = widgets.iterator(); iter.hasNext();) {
            Widget widget = (Widget) iter.next();
            if (widget instanceof Repeater) {
                if (this.repeaterPaths != null)
                    this.repeaterPaths.remove(widget);
                this.repeaterWidgets.remove(widget);
            }
        }
        widgets.clear();
    }

    /**
     * Adds a widget not contained in a repeater.
     * @param context
     * @param path
     */
    private void addSimpleWidget(Widget context, String path) {
        Widget widget = context.lookupWidget(path);
        if (widget == null)
            throw new IllegalArgumentException(
                    "Cannot find a widget with path " + path + " relative to widget " + context.getName());
        if (this.noRepeaterWidgets == null)
            this.noRepeaterWidgets = new ArrayList();
        this.noRepeaterWidgets.add(widget);
        newAdditions.add(widget);
    }

    /**
     * Return all widgets found for the given paths.
     * @return A Collection of {@link Widget}s.
     */
    public Collection getWidgets() {
        List list = new ArrayList();
        if (this.noRepeaterWidgets != null)
            list.addAll(this.noRepeaterWidgets);
        if (this.repeaterWidgets != null) {
            for (Iterator iter = this.repeaterWidgets.keySet().iterator(); iter.hasNext();) {
                Repeater repeater = (Repeater) iter.next();
                list.addAll((Collection) this.repeaterWidgets.get(repeater));
            }
        }
        return list;
    }

    /**
     * @return true if this finder is mutable (i.e. it's monitoring some repeaters) or false if getWidgets() will always return the same list (i.e. it's not monitoring any widget).
     */
    public boolean isMutable() {
        return (this.repeaterPaths != null) && this.repeaterPaths.size() > 0;
    }

    class RefreshingRepeaterListener implements RepeaterListener {
        public void repeaterModified(RepeaterEvent event) {
            if (event.getAction() == RepeaterEventAction.ROW_ADDED) {
                refreshForAdd((Repeater) event.getSourceWidget(), event.getRow());
            }
            if (event.getAction() == RepeaterEventAction.ROW_DELETING) {
                refreshForDelete((Repeater) event.getSourceWidget(), event.getRow());
            }
            if (event.getAction() == RepeaterEventAction.ROWS_CLEARING) {
                refreshForClear((Repeater) event.getSourceWidget());
            }
            if (listener != null) {
                listener.repeaterModified(event);
            }
        }
    }

    /**
     * @return true if new widgets have been added to this list (i.e. new repeater rows have been created) since last time getNewAdditions() was called. 
     */
    public boolean hasNewAdditions() {
        return this.newAdditions.size() > 0;
    }

    /**
     * Gets the new widgets that has been added to the list, as a consequence of new repeater rows additions, since
     * last time this method was called or the finder was initialized. 
     * @return A List of {@link Widget}s.
     */
    public List getNewAdditions() {
        List ret = new ArrayList(newAdditions);
        newAdditions.clear();
        return ret;
    }

    /**
     * Adds a repeater listener. New widget additions or deletions will be notified thru this listener (events received
     * from monitored repeaters will be forwarded, use {@link #getNewAdditions()} to retrieve new widgets).
     * @param listener The listener to add.
     */
    public void addRepeaterListener(RepeaterListener listener) {
        this.listener = WidgetEventMulticaster.add(this.listener, listener);
    }

    /**
     * Removes a listener. See {@link #addRepeaterListener(RepeaterListener)}.
     * @param listener The listener to remove.
     */
    public void removeRepeaterListener(RepeaterListener listener) {
        this.listener = WidgetEventMulticaster.remove(this.listener, listener);
    }

    /**
     * @return true if there are listeners registered on this instance. See {@link #addRepeaterListener(RepeaterListener)}.
     */
    public boolean hasRepeaterListeners() {
        return this.listener != null;
    }
}