| /* |
| * 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; |
| } |
| } |