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.formmodel;
018:
019: import java.util.ArrayList;
020: import java.util.Iterator;
021: import java.util.List;
022: import java.util.Locale;
023:
024: import org.apache.cocoon.environment.Request;
025: import org.apache.cocoon.forms.FormContext;
026: import org.apache.cocoon.forms.FormsConstants;
027: import org.apache.cocoon.forms.FormsRuntimeException;
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.WidgetEvent;
032: import org.apache.cocoon.forms.event.WidgetEventMulticaster;
033: import org.apache.cocoon.forms.util.I18nMessage;
034: import org.apache.cocoon.forms.validation.ValidationError;
035: import org.apache.cocoon.forms.validation.ValidationErrorAware;
036: import org.apache.cocoon.xml.AttributesImpl;
037: import org.apache.cocoon.xml.XMLUtils;
038: import org.xml.sax.ContentHandler;
039: import org.xml.sax.SAXException;
040:
041: /**
042: * A repeater is a widget that repeats a number of other widgets.
043: *
044: * <p>Technically, the Repeater widget is a ContainerWidget whose children are
045: * {@link RepeaterRow}s, and the RepeaterRows in turn are ContainerWidgets
046: * containing the actual repeated widgets. However, in practice, you won't need
047: * to use the RepeaterRow widget directly.
048: *
049: * <p>Using the methods {@link #getSize()} and {@link #getWidget(int, java.lang.String)}
050: * you can access all of the repeated widget instances.
051: *
052: * @version $Id: Repeater.java 462520 2006-10-10 19:39:14Z vgritsenko $
053: */
054: public class Repeater extends AbstractWidget implements
055: ValidationErrorAware {
056:
057: private static final String REPEATER_EL = "repeater";
058: private static final String HEADINGS_EL = "headings";
059: private static final String HEADING_EL = "heading";
060: private static final String LABEL_EL = "label";
061: private static final String REPEATER_SIZE_EL = "repeater-size";
062:
063: protected final RepeaterDefinition definition;
064: protected final List rows = new ArrayList();
065: protected ValidationError validationError;
066: private boolean orderable = false;
067: private RepeaterListener listener;
068:
069: public Repeater(RepeaterDefinition repeaterDefinition) {
070: super (repeaterDefinition);
071: this .definition = repeaterDefinition;
072: // Setup initial size. Do not call addRow() as it will call initialize()
073: // on the newly created rows, which is not what we want here.
074: for (int i = 0; i < this .definition.getInitialSize(); i++) {
075: rows.add(new RepeaterRow(definition));
076: }
077:
078: this .orderable = this .definition.getOrderable();
079: this .listener = this .definition.getRepeaterListener();
080: }
081:
082: public WidgetDefinition getDefinition() {
083: return definition;
084: }
085:
086: public void initialize() {
087: for (int i = 0; i < this .rows.size(); i++) {
088: ((RepeaterRow) rows.get(i)).initialize();
089: // TODO(SG) Is this safe !?
090: broadcastEvent(new RepeaterEvent(this ,
091: RepeaterEventAction.ROW_ADDED, i));
092: }
093: super .initialize();
094: }
095:
096: public int getSize() {
097: return rows.size();
098: }
099:
100: public int getMinSize() {
101: return this .definition.getMinSize();
102: }
103:
104: public int getMaxSize() {
105: return this .definition.getMaxSize();
106: }
107:
108: public RepeaterRow addRow() {
109: RepeaterRow repeaterRow = new RepeaterRow(definition);
110: rows.add(repeaterRow);
111: repeaterRow.initialize();
112: getForm().addWidgetUpdate(this );
113: broadcastEvent(new RepeaterEvent(this ,
114: RepeaterEventAction.ROW_ADDED, rows.size() - 1));
115: return repeaterRow;
116: }
117:
118: public RepeaterRow addRow(int index) {
119: RepeaterRow repeaterRow = new RepeaterRow(definition);
120: if (index >= this .rows.size()) {
121: rows.add(repeaterRow);
122: index = rows.size() - 1;
123: } else {
124: rows.add(index, repeaterRow);
125: }
126: repeaterRow.initialize();
127: getForm().addWidgetUpdate(this );
128: broadcastEvent(new RepeaterEvent(this ,
129: RepeaterEventAction.ROW_ADDED, index));
130: return repeaterRow;
131: }
132:
133: public RepeaterRow getRow(int index) {
134: return (RepeaterRow) rows.get(index);
135: }
136:
137: /**
138: * Overrides {@link AbstractWidget#getChild(String)} to return the
139: * repeater-row indicated by the index in 'id'
140: *
141: * @param id index of the row as a string-id
142: * @return the repeater-row at the specified index
143: */
144: public Widget getChild(String id) {
145: int rowIndex;
146: try {
147: rowIndex = Integer.parseInt(id);
148: } catch (NumberFormatException nfe) {
149: // Not a number
150: return null;
151: }
152:
153: if (rowIndex < 0 || rowIndex >= getSize()) {
154: return null;
155: }
156:
157: return getRow(rowIndex);
158: }
159:
160: /**
161: * Crawls up the parents of a widget up to finding a repeater row.
162: *
163: * @param widget the widget whose row is to be found
164: * @return the repeater row
165: */
166: public static RepeaterRow getParentRow(Widget widget) {
167: Widget result = widget;
168: while (result != null
169: && !(result instanceof Repeater.RepeaterRow)) {
170: result = result.getParent();
171: }
172:
173: if (result == null) {
174: throw new RuntimeException(
175: "Could not find a parent row for widget " + widget);
176: }
177: return (Repeater.RepeaterRow) result;
178: }
179:
180: /**
181: * Get the position of a row in this repeater.
182: * @param row the row which we search the index for
183: * @return the row position or -1 if this row is not in this repeater
184: */
185: public int indexOf(RepeaterRow row) {
186: return this .rows.indexOf(row);
187: }
188:
189: /**
190: * @throws IndexOutOfBoundsException if the the index is outside the range of existing rows.
191: */
192: public void removeRow(int index) {
193: broadcastEvent(new RepeaterEvent(this ,
194: RepeaterEventAction.ROW_DELETING, index));
195: rows.remove(index);
196: getForm().addWidgetUpdate(this );
197: broadcastEvent(new RepeaterEvent(this ,
198: RepeaterEventAction.ROW_DELETED, index));
199: }
200:
201: /**
202: * Move a row from one place to another
203: * @param from the existing row position
204: * @param to the target position. The "from" item will be moved before that position.
205: */
206: public void moveRow(int from, int to) {
207: int size = this .rows.size();
208:
209: if (from < 0 || from >= size || to < 0 || to > size) {
210: throw new IllegalArgumentException("Cannot move from "
211: + from + " to " + to + " on repeater with " + size
212: + " rows");
213: }
214:
215: if (from == to) {
216: return;
217: }
218:
219: Object fromRow = this .rows.remove(from);
220: if (to == size) {
221: // Move at the end
222: this .rows.add(fromRow);
223:
224: } else if (to > from) {
225: // Index of "to" was moved by removing
226: this .rows.add(to - 1, fromRow);
227:
228: } else {
229: this .rows.add(to, fromRow);
230: }
231:
232: getForm().addWidgetUpdate(this );
233: broadcastEvent(new RepeaterEvent(this ,
234: RepeaterEventAction.ROWS_REARRANGED));
235: }
236:
237: /**
238: * Move a row from one place to another. In contrast to {@link #moveRow}, this
239: * method treats the to-index as the exact row-index where you want to have the
240: * row moved to.
241: *
242: * @param from the existing row position
243: * @param to the target position. The "from" item will be moved before that position.
244: */
245: public void moveRow2(int from, int to) {
246: int size = this .rows.size();
247:
248: if (from < 0 || from >= size || to < 0 || to >= size) {
249: throw new IllegalArgumentException("Cannot move from "
250: + from + " to " + to + " on repeater with " + size
251: + " rows");
252: }
253:
254: if (from == to) {
255: return;
256: }
257:
258: Object fromRow = this .rows.remove(from);
259: this .rows.add(to, fromRow);
260:
261: getForm().addWidgetUpdate(this );
262: broadcastEvent(new RepeaterEvent(this ,
263: RepeaterEventAction.ROWS_REARRANGED));
264: }
265:
266: public void moveRowLeft(int index) {
267: if (index == 0 || index >= this .rows.size()) {
268: // do nothing
269: } else {
270: Object temp = this .rows.get(index - 1);
271: this .rows.set(index - 1, this .rows.get(index));
272: this .rows.set(index, temp);
273: }
274: getForm().addWidgetUpdate(this );
275: broadcastEvent(new RepeaterEvent(this ,
276: RepeaterEventAction.ROWS_REARRANGED));
277: }
278:
279: public void moveRowRight(int index) {
280: if (index < 0 || index >= this .rows.size() - 1) {
281: // do nothing
282: } else {
283: Object temp = this .rows.get(index + 1);
284: this .rows.set(index + 1, this .rows.get(index));
285: this .rows.set(index, temp);
286: }
287: getForm().addWidgetUpdate(this );
288: broadcastEvent(new RepeaterEvent(this ,
289: RepeaterEventAction.ROWS_REARRANGED));
290: }
291:
292: /**
293: * @deprecated See {@link #clear()}
294: *
295: */
296: public void removeRows() {
297: clear();
298: }
299:
300: /**
301: * Clears all rows from the repeater and go back to the initial size
302: */
303: public void clear() {
304: broadcastEvent(new RepeaterEvent(this ,
305: RepeaterEventAction.ROWS_CLEARING));
306: rows.clear();
307: broadcastEvent(new RepeaterEvent(this ,
308: RepeaterEventAction.ROWS_CLEARED));
309:
310: // and reset to initial size
311: for (int i = 0; i < this .definition.getInitialSize(); i++) {
312: addRow();
313: }
314: getForm().addWidgetUpdate(this );
315: }
316:
317: public void addRepeaterListener(RepeaterListener listener) {
318: this .listener = WidgetEventMulticaster.add(this .listener,
319: listener);
320: }
321:
322: public void removeRepeaterListener(RepeaterListener listener) {
323: this .listener = WidgetEventMulticaster.remove(this .listener,
324: listener);
325: }
326:
327: public boolean hasRepeaterListeners() {
328: return this .listener != null;
329: }
330:
331: public void broadcastEvent(WidgetEvent event) {
332: if (event instanceof RepeaterEvent) {
333: if (this .listener != null) {
334: this .listener.repeaterModified((RepeaterEvent) event);
335: }
336: } else {
337: // Other kinds of events
338: super .broadcastEvent(event);
339: }
340: }
341:
342: /**
343: * Gets a widget on a certain row.
344: * @param rowIndex startin from 0
345: * @param id a widget id
346: * @return null if there's no such widget
347: */
348: public Widget getWidget(int rowIndex, String id) {
349: RepeaterRow row = (RepeaterRow) rows.get(rowIndex);
350: return row.getChild(id);
351: }
352:
353: public void readFromRequest(FormContext formContext) {
354: if (!getCombinedState().isAcceptingInputs()) {
355: return;
356: }
357:
358: // read number of rows from request, and make an according number of rows
359: Request req = formContext.getRequest();
360: String paramName = getRequestParameterName();
361:
362: String sizeParameter = req.getParameter(paramName + ".size");
363: if (sizeParameter != null) {
364: int size = 0;
365: try {
366: size = Integer.parseInt(sizeParameter);
367: } catch (NumberFormatException exc) {
368: // do nothing
369: }
370:
371: // some protection against people who might try to exhaust the server by supplying very large
372: // size parameters
373: if (size > 500) {
374: throw new RuntimeException(
375: "Client is not allowed to specify a repeater size larger than 500.");
376: }
377:
378: int currentSize = getSize();
379: if (currentSize < size) {
380: for (int i = currentSize; i < size; i++) {
381: addRow();
382: }
383: } else if (currentSize > size) {
384: for (int i = currentSize - 1; i >= size; i--) {
385: removeRow(i);
386: }
387: }
388: }
389:
390: // let the rows read their data from the request
391: Iterator rowIt = rows.iterator();
392: while (rowIt.hasNext()) {
393: RepeaterRow row = (RepeaterRow) rowIt.next();
394: row.readFromRequest(formContext);
395: }
396:
397: // Handle repeater-level actions
398: String action = req.getParameter(paramName + ".action");
399: if (action == null) {
400: return;
401: }
402:
403: // Handle row move. It's important for this to happen *after* row.readFromRequest,
404: // as reordering rows changes their IDs and therefore their child widget's ID too.
405: if (action.equals("move")) {
406: if (!this .orderable) {
407: throw new FormsRuntimeException("Widget " + this
408: + " is not orderable", getLocation());
409: }
410:
411: int from = Integer.parseInt(req.getParameter(paramName
412: + ".from"));
413: int before = Integer.parseInt(req.getParameter(paramName
414: + ".before"));
415:
416: Object row = this .rows.get(from);
417: // Add to the new location
418: this .rows.add(before, row);
419: // Remove from the previous one, taking into account potential location change
420: // because of the previous add()
421: if (before < from)
422: from++;
423: this .rows.remove(from);
424:
425: // Needs refresh
426: getForm().addWidgetUpdate(this );
427:
428: } else {
429: throw new FormsRuntimeException("Unknown action " + action
430: + " for " + this , getLocation());
431: }
432: }
433:
434: /**
435: * @see org.apache.cocoon.forms.formmodel.Widget#validate()
436: */
437: public boolean validate() {
438: if (!getCombinedState().isValidatingValues()) {
439: this .wasValid = true;
440: return true;
441: }
442:
443: boolean valid = true;
444: Iterator rowIt = rows.iterator();
445: while (rowIt.hasNext()) {
446: RepeaterRow row = (RepeaterRow) rowIt.next();
447: valid = valid & row.validate();
448: }
449:
450: if (rows.size() > getMaxSize() || rows.size() < getMinSize()) {
451: String[] boundaries = new String[2];
452: boundaries[0] = String.valueOf(getMinSize());
453: boundaries[1] = String.valueOf(getMaxSize());
454: this .validationError = new ValidationError(new I18nMessage(
455: "repeater.cardinality", boundaries,
456: FormsConstants.I18N_CATALOGUE));
457: valid = false;
458: }
459:
460: if (valid) {
461: valid = super .validate();
462: }
463:
464: this .wasValid = valid && this .validationError == null;
465: return this .wasValid;
466: }
467:
468: /**
469: * @return "repeater"
470: */
471: public String getXMLElementName() {
472: return REPEATER_EL;
473: }
474:
475: /**
476: * Adds @size attribute
477: */
478: public AttributesImpl getXMLElementAttributes() {
479: AttributesImpl attrs = super .getXMLElementAttributes();
480: attrs.addCDATAAttribute("size", String.valueOf(getSize()));
481: // Generate the min and max sizes if they don't have the default value
482: int size = getMinSize();
483: if (size > 0) {
484: attrs.addCDATAAttribute("min-size", String.valueOf(size));
485: }
486: size = getMaxSize();
487: if (size != Integer.MAX_VALUE) {
488: attrs.addCDATAAttribute("max-size", String.valueOf(size));
489: }
490: return attrs;
491: }
492:
493: public void generateDisplayData(ContentHandler contentHandler)
494: throws SAXException {
495: // the repeater's label
496: contentHandler.startElement(FormsConstants.INSTANCE_NS,
497: LABEL_EL, FormsConstants.INSTANCE_PREFIX_COLON
498: + LABEL_EL, XMLUtils.EMPTY_ATTRIBUTES);
499: generateLabel(contentHandler);
500: contentHandler.endElement(FormsConstants.INSTANCE_NS, LABEL_EL,
501: FormsConstants.INSTANCE_PREFIX_COLON + LABEL_EL);
502:
503: // heading element -- currently contains the labels of each widget in the repeater
504: contentHandler.startElement(FormsConstants.INSTANCE_NS,
505: HEADINGS_EL, FormsConstants.INSTANCE_PREFIX_COLON
506: + HEADINGS_EL, XMLUtils.EMPTY_ATTRIBUTES);
507: Iterator widgetDefinitionIt = definition.getWidgetDefinitions()
508: .iterator();
509: while (widgetDefinitionIt.hasNext()) {
510: WidgetDefinition widgetDefinition = (WidgetDefinition) widgetDefinitionIt
511: .next();
512: contentHandler.startElement(FormsConstants.INSTANCE_NS,
513: HEADING_EL, FormsConstants.INSTANCE_PREFIX_COLON
514: + HEADING_EL, XMLUtils.EMPTY_ATTRIBUTES);
515: widgetDefinition.generateLabel(contentHandler);
516: contentHandler.endElement(FormsConstants.INSTANCE_NS,
517: HEADING_EL, FormsConstants.INSTANCE_PREFIX_COLON
518: + HEADING_EL);
519: }
520: contentHandler.endElement(FormsConstants.INSTANCE_NS,
521: HEADINGS_EL, FormsConstants.INSTANCE_PREFIX_COLON
522: + HEADINGS_EL);
523: }
524:
525: public void generateItemSaxFragment(ContentHandler contentHandler,
526: Locale locale) throws SAXException {
527: // the actual rows in the repeater
528: Iterator rowIt = rows.iterator();
529: while (rowIt.hasNext()) {
530: RepeaterRow row = (RepeaterRow) rowIt.next();
531: row.generateSaxFragment(contentHandler, locale);
532: }
533: }
534:
535: /**
536: * Generates the label of a certain widget in this repeater.
537: */
538: public void generateWidgetLabel(String widgetId,
539: ContentHandler contentHandler) throws SAXException {
540: WidgetDefinition widgetDefinition = definition
541: .getWidgetDefinition(widgetId);
542: if (widgetDefinition == null) {
543: throw new SAXException("Repeater '"
544: + getRequestParameterName() + "' at "
545: + getLocation() + " contains no widget with id '"
546: + widgetId + "'.");
547: }
548:
549: widgetDefinition.generateLabel(contentHandler);
550: }
551:
552: /**
553: * Generates a repeater-size element with a size attribute indicating the size of this repeater.
554: */
555: public void generateSize(ContentHandler contentHandler)
556: throws SAXException {
557: AttributesImpl attrs = getXMLElementAttributes();
558: contentHandler.startElement(FormsConstants.INSTANCE_NS,
559: REPEATER_SIZE_EL, FormsConstants.INSTANCE_PREFIX_COLON
560: + REPEATER_SIZE_EL, attrs);
561: contentHandler.endElement(FormsConstants.INSTANCE_NS,
562: REPEATER_SIZE_EL, FormsConstants.INSTANCE_PREFIX_COLON
563: + REPEATER_SIZE_EL);
564: }
565:
566: /**
567: * Set a validation error on this field. This allows repeaters be externally marked as invalid by
568: * application logic.
569: *
570: * @return the validation error
571: */
572: public ValidationError getValidationError() {
573: return this .validationError;
574: }
575:
576: /**
577: * set a validation error
578: */
579: public void setValidationError(ValidationError error) {
580: this .validationError = error;
581: }
582:
583: public class RepeaterRow extends AbstractContainerWidget {
584:
585: private static final String ROW_EL = "repeater-row";
586:
587: public RepeaterRow(RepeaterDefinition definition) {
588: super (definition);
589: setParent(Repeater.this );
590: definition.createWidgets(this );
591: }
592:
593: public WidgetDefinition getDefinition() {
594: return Repeater.this .getDefinition();
595: }
596:
597: private int cachedPosition = -100;
598: private String cachedId = "--undefined--";
599:
600: public String getId() {
601: int pos = rows.indexOf(this );
602: if (pos == -1) {
603: throw new IllegalStateException(
604: "Row has currently no position");
605: }
606:
607: if (pos != this .cachedPosition) {
608: this .cachedPosition = pos;
609: // id of a RepeaterRow is the position of the row in the list of rows.
610: this .cachedId = String.valueOf(pos);
611: widgetNameChanged();
612: }
613: return this .cachedId;
614: }
615:
616: public String getRequestParameterName() {
617: // Get the id to check potential position change
618: getId();
619:
620: return super .getRequestParameterName();
621: }
622:
623: public Form getForm() {
624: return Repeater.this .getForm();
625: }
626:
627: public void initialize() {
628: // Initialize children but don't call super.initialize() that would call the repeater's
629: // on-create handlers for each row.
630: Iterator i = getChildren();
631: while (i.hasNext()) {
632: ((Widget) i.next()).initialize();
633: }
634: }
635:
636: public boolean validate() {
637: // Validate only child widtgets, as the definition's validators are those of the parent repeater
638: return widgets.validate();
639: }
640:
641: /**
642: * @return "repeater-row"
643: */
644: public String getXMLElementName() {
645: return ROW_EL;
646: }
647:
648: public void generateLabel(ContentHandler contentHandler)
649: throws SAXException {
650: // this widget has its label generated in the context of the repeater
651: }
652:
653: public void generateDisplayData(ContentHandler contentHandler)
654: throws SAXException {
655: // this widget has its display-data generated in the context of the repeater
656: }
657:
658: public void broadcastEvent(WidgetEvent event) {
659: throw new UnsupportedOperationException("Widget " + this
660: + " doesn't handle events.");
661: }
662: }
663:
664: }
|