001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.forms.util;
018:
019: import java.util.ArrayList;
020: import java.util.Collection;
021: import java.util.HashMap;
022: import java.util.HashSet;
023: import java.util.Iterator;
024: import java.util.List;
025: import java.util.Map;
026: import java.util.Set;
027:
028: import org.apache.cocoon.forms.event.RepeaterEvent;
029: import org.apache.cocoon.forms.event.RepeaterEventAction;
030: import org.apache.cocoon.forms.event.RepeaterListener;
031: import org.apache.cocoon.forms.event.WidgetEventMulticaster;
032: import org.apache.cocoon.forms.formmodel.Repeater;
033: import org.apache.cocoon.forms.formmodel.Widget;
034: import org.apache.commons.lang.StringUtils;
035:
036: /**
037: * An utility class to manage list of widgets.
038: *
039: * <p>
040: * The {@link org.apache.cocoon.forms.formmodel.Widget#lookupWidget(String)} method is able
041: * to only return one widget, while this class returns a list of widgets. It uses a path syntax containing a /./,
042: * <code>repeater/./foo</code>, which repreesents all the instances of the foo widget inside the repeater,
043: * one per row. Note that it also supports finding a widgets inside multi level repeaters, something like
044: * invoices/./movements/./amount or courseYears/./exams/./preparatoryCourses/./title .
045: * </p>
046: * <p>
047: * Class has been designed to offer good performances, since the widget list is built only once and
048: * is automatically updated when a repeater row is added or removed.
049: * {@link org.apache.cocoon.forms.event.RepeaterListener}s can be attached directly to receive notifications
050: * of widget additions or removals.
051: * </p>
052: * <p>
053: * This class is used in {@link org.apache.cocoon.forms.formmodel.CalculatedField}s and
054: * {@link org.apache.cocoon.forms.formmodel.CalculatedFieldAlgorithm}s.
055: * </p>
056: * @version $Id$
057: */
058: public class WidgetFinder {
059:
060: private boolean keepUpdated = false;
061:
062: // Holds all the widgets not child of a repeater.
063: private List noRepeaterWidgets = null;
064: // Map repeater -> Set of Strings containing paths
065: private Map repeaterPaths = null;
066: // Map repeater -> Set of Widgets
067: private Map repeaterWidgets = null;
068: // A List of recently added widgets, will get cleared when getNewAdditions is called.
069: private List newAdditions = new ArrayList();
070:
071: private RefreshingRepeaterListener refreshingListener = new RefreshingRepeaterListener();
072:
073: private RepeaterListener listener;
074:
075: /**
076: * Searches for widgets. It will iterate on the given paths and find all
077: * corresponding widgets. If a path is in the forms repeater/* /widget
078: * then all the rows of the repeater will be iterated and subwidgets
079: * will be fetched.
080: * @param context The context widget to start from.
081: * @param paths An iterator of Strings containing the paths.
082: * @param keepUpdated If true, listeners will be installed on repeaters
083: * to keep lists updated without polling.
084: */
085: public WidgetFinder(Widget context, Iterator paths,
086: boolean keepUpdated) {
087: this .keepUpdated = keepUpdated;
088: while (paths.hasNext()) {
089: String path = (String) paths.next();
090: path = toAsterisk(path);
091: if (path.indexOf('*') == -1) {
092: addSimpleWidget(context, path);
093: } else {
094: recurseRepeaters(context, path, true);
095: }
096: }
097: }
098:
099: /**
100: * Searches for widgets. If path is in the forms repeater/* /widget
101: * then all the rows of the repeater will be iterated and subwidgets
102: * will be fetched.
103: * @param context The context widget to start from.
104: * @param path Path to search for..
105: * @param keepUpdated If true, listeners will be installed on repeaters
106: * to keep lists updated without polling.
107: */
108: public WidgetFinder(Widget context, String path, boolean keepUpdated) {
109: path = toAsterisk(path);
110: this .keepUpdated = keepUpdated;
111: if (path.indexOf('*') == -1) {
112: addSimpleWidget(context, path);
113: } else {
114: recurseRepeaters(context, path, true);
115: }
116: }
117:
118: private String toAsterisk(String path) {
119: return StringUtils.replace(path, "/./", "/*/");
120: }
121:
122: /**
123: * Recurses a repeater path with asterisk.
124: * @param context The context widget.
125: * @param path The path.
126: */
127: private void recurseRepeaters(Widget context, String path,
128: boolean root) {
129: String reppath = path.substring(0, path.indexOf('*') - 1);
130: String childpath = path.substring(path.indexOf('*') + 2);
131: Widget wdg = context.lookupWidget(reppath);
132: if (wdg == null) {
133: if (root) {
134: throw new IllegalArgumentException(
135: "Cannot find a repeater with path " + reppath
136: + " relative to widget "
137: + context.getName());
138: } else {
139: return;
140: }
141: }
142: if (!(wdg instanceof Repeater)) {
143: throw new IllegalArgumentException("The widget with path "
144: + reppath + " relative to widget "
145: + context.getName() + " is not a repeater!");
146: }
147: Repeater repeater = (Repeater) wdg;
148: if (context instanceof Repeater.RepeaterRow) {
149: // Add this repeater to the repeater widgets
150: addRepeaterWidget((Repeater) context.getParent(), repeater);
151: }
152:
153: addRepeaterPath(repeater, childpath);
154: if (childpath.indexOf('*') != -1) {
155: for (int i = 0; i < repeater.getSize(); i++) {
156: Repeater.RepeaterRow row = repeater.getRow(i);
157: recurseRepeaters(row, childpath, false);
158: }
159: } else {
160: for (int i = 0; i < repeater.getSize(); i++) {
161: Repeater.RepeaterRow row = repeater.getRow(i);
162: Widget okwdg = row.lookupWidget(childpath);
163: if (okwdg != null) {
164: addRepeaterWidget(repeater, okwdg);
165: }
166: }
167: }
168: }
169:
170: /**
171: * Adds to the list a widget descendant of a repeater.
172: * @param repeater The repeater.
173: * @param okwdg The widget.
174: */
175: private void addRepeaterWidget(Repeater repeater, Widget okwdg) {
176: if (this .repeaterWidgets == null)
177: this .repeaterWidgets = new HashMap();
178: Set widgets = (Set) this .repeaterWidgets.get(repeater);
179: if (widgets == null) {
180: widgets = new HashSet();
181: this .repeaterWidgets.put(repeater, widgets);
182: }
183: widgets.add(okwdg);
184: newAdditions.add(okwdg);
185: }
186:
187: /**
188: * Adds a repeater monitored path.
189: * @param repeater The repeater.
190: * @param childpath The child part of the path.
191: */
192: private void addRepeaterPath(Repeater repeater, String childpath) {
193: if (this .repeaterPaths == null)
194: this .repeaterPaths = new HashMap();
195: Set paths = (Set) this .repeaterPaths.get(repeater);
196: if (paths == null) {
197: paths = new HashSet();
198: this .repeaterPaths.put(repeater, paths);
199: if (keepUpdated)
200: repeater.addRepeaterListener(refreshingListener);
201: }
202: paths.add(childpath);
203: }
204:
205: /**
206: * Called when a new row addition event is received from a monitored repeater.
207: * @param repeater The repeated that generated the event.
208: * @param index The new row index.
209: */
210: protected void refreshForAdd(Repeater repeater, int index) {
211: Repeater.RepeaterRow row = repeater.getRow(index);
212: if (this .repeaterPaths == null)
213: this .repeaterPaths = new HashMap();
214: Set paths = (Set) this .repeaterPaths.get(repeater);
215: for (Iterator iter = paths.iterator(); iter.hasNext();) {
216: String path = (String) iter.next();
217: if (path.indexOf('*') != -1) {
218: recurseRepeaters(row, path, false);
219: } else {
220: Widget wdg = row.lookupWidget(path);
221: if (wdg == null) {
222: throw new IllegalStateException(
223: "Even after row addition cannot find a widget with path "
224: + path + " in repeater "
225: + repeater.getName());
226: }
227: addRepeaterWidget(repeater, wdg);
228: }
229: }
230: }
231:
232: /**
233: * Called when a row deletion event is received from a monitored repeater.
234: * @param repeater The repeated that generated the event.
235: * @param index The deleted row index.
236: */
237: protected void refreshForDelete(Repeater repeater, int index) {
238: Repeater.RepeaterRow row = repeater.getRow(index);
239: Set widgets = (Set) this .repeaterWidgets.get(repeater);
240: for (Iterator iter = widgets.iterator(); iter.hasNext();) {
241: Widget widget = (Widget) iter.next();
242: boolean ischild = false;
243: Widget parent = widget.getParent();
244: while (parent != null) {
245: if (parent == row) {
246: ischild = true;
247: break;
248: }
249: parent = parent.getParent();
250: }
251: if (ischild) {
252: iter.remove();
253: if (widget instanceof Repeater) {
254: if (this .repeaterPaths != null)
255: this .repeaterPaths.remove(widget);
256: this .repeaterWidgets.remove(widget);
257: }
258: }
259: }
260: }
261:
262: /**
263: * Called when a repeater clear event is received from a monitored repeater.
264: * @param repeater The repeated that generated the event.
265: */
266: protected void refreshForClear(Repeater repeater) {
267: Set widgets = (Set) this .repeaterWidgets.get(repeater);
268: for (Iterator iter = widgets.iterator(); iter.hasNext();) {
269: Widget widget = (Widget) iter.next();
270: if (widget instanceof Repeater) {
271: if (this .repeaterPaths != null)
272: this .repeaterPaths.remove(widget);
273: this .repeaterWidgets.remove(widget);
274: }
275: }
276: widgets.clear();
277: }
278:
279: /**
280: * Adds a widget not contained in a repeater.
281: * @param context
282: * @param path
283: */
284: private void addSimpleWidget(Widget context, String path) {
285: Widget widget = context.lookupWidget(path);
286: if (widget == null)
287: throw new IllegalArgumentException(
288: "Cannot find a widget with path " + path
289: + " relative to widget "
290: + context.getName());
291: if (this .noRepeaterWidgets == null)
292: this .noRepeaterWidgets = new ArrayList();
293: this .noRepeaterWidgets.add(widget);
294: newAdditions.add(widget);
295: }
296:
297: /**
298: * Return all widgets found for the given paths.
299: * @return A Collection of {@link Widget}s.
300: */
301: public Collection getWidgets() {
302: List list = new ArrayList();
303: if (this .noRepeaterWidgets != null)
304: list.addAll(this .noRepeaterWidgets);
305: if (this .repeaterWidgets != null) {
306: for (Iterator iter = this .repeaterWidgets.keySet()
307: .iterator(); iter.hasNext();) {
308: Repeater repeater = (Repeater) iter.next();
309: list.addAll((Collection) this .repeaterWidgets
310: .get(repeater));
311: }
312: }
313: return list;
314: }
315:
316: /**
317: * @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).
318: */
319: public boolean isMutable() {
320: return (this .repeaterPaths != null)
321: && this .repeaterPaths.size() > 0;
322: }
323:
324: class RefreshingRepeaterListener implements RepeaterListener {
325: public void repeaterModified(RepeaterEvent event) {
326: if (event.getAction() == RepeaterEventAction.ROW_ADDED) {
327: refreshForAdd((Repeater) event.getSourceWidget(), event
328: .getRow());
329: }
330: if (event.getAction() == RepeaterEventAction.ROW_DELETING) {
331: refreshForDelete((Repeater) event.getSourceWidget(),
332: event.getRow());
333: }
334: if (event.getAction() == RepeaterEventAction.ROWS_CLEARING) {
335: refreshForClear((Repeater) event.getSourceWidget());
336: }
337: if (listener != null) {
338: listener.repeaterModified(event);
339: }
340: }
341: }
342:
343: /**
344: * @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.
345: */
346: public boolean hasNewAdditions() {
347: return this .newAdditions.size() > 0;
348: }
349:
350: /**
351: * Gets the new widgets that has been added to the list, as a consequence of new repeater rows additions, since
352: * last time this method was called or the finder was initialized.
353: * @return A List of {@link Widget}s.
354: */
355: public List getNewAdditions() {
356: List ret = new ArrayList(newAdditions);
357: newAdditions.clear();
358: return ret;
359: }
360:
361: /**
362: * Adds a repeater listener. New widget additions or deletions will be notified thru this listener (events received
363: * from monitored repeaters will be forwarded, use {@link #getNewAdditions()} to retrieve new widgets).
364: * @param listener The listener to add.
365: */
366: public void addRepeaterListener(RepeaterListener listener) {
367: this .listener = WidgetEventMulticaster.add(this .listener,
368: listener);
369: }
370:
371: /**
372: * Removes a listener. See {@link #addRepeaterListener(RepeaterListener)}.
373: * @param listener The listener to remove.
374: */
375: public void removeRepeaterListener(RepeaterListener listener) {
376: this .listener = WidgetEventMulticaster.remove(this .listener,
377: listener);
378: }
379:
380: /**
381: * @return true if there are listeners registered on this instance. See {@link #addRepeaterListener(RepeaterListener)}.
382: */
383: public boolean hasRepeaterListeners() {
384: return this.listener != null;
385: }
386: }
|