| /* |
| * 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.formmodel; |
| |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Locale; |
| |
| import org.apache.cocoon.environment.Request; |
| import org.apache.cocoon.forms.FormContext; |
| import org.apache.cocoon.forms.FormsConstants; |
| import org.apache.cocoon.forms.FormsRuntimeException; |
| 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.WidgetEvent; |
| import org.apache.cocoon.forms.event.WidgetEventMulticaster; |
| import org.apache.cocoon.forms.util.I18nMessage; |
| import org.apache.cocoon.forms.validation.ValidationError; |
| import org.apache.cocoon.forms.validation.ValidationErrorAware; |
| import org.apache.cocoon.xml.AttributesImpl; |
| import org.apache.cocoon.xml.XMLUtils; |
| import org.xml.sax.ContentHandler; |
| import org.xml.sax.SAXException; |
| |
| /** |
| * A repeater is a widget that repeats a number of other widgets. |
| * |
| * <p>Technically, the Repeater widget is a ContainerWidget whose children are |
| * {@link RepeaterRow}s, and the RepeaterRows in turn are ContainerWidgets |
| * containing the actual repeated widgets. However, in practice, you won't need |
| * to use the RepeaterRow widget directly. |
| * |
| * <p>Using the methods {@link #getSize()} and {@link #getWidget(int, java.lang.String)} |
| * you can access all of the repeated widget instances. |
| * |
| * @version $Id$ |
| */ |
| public class Repeater extends AbstractWidget |
| implements ValidationErrorAware { |
| |
| private static final String REPEATER_EL = "repeater"; |
| private static final String HEADINGS_EL = "headings"; |
| private static final String HEADING_EL = "heading"; |
| private static final String LABEL_EL = "label"; |
| private static final String REPEATER_SIZE_EL = "repeater-size"; |
| |
| protected final RepeaterDefinition definition; |
| protected final List rows = new ArrayList(); |
| protected ValidationError validationError; |
| private boolean orderable = false; |
| private RepeaterListener listener; |
| |
| public Repeater(RepeaterDefinition repeaterDefinition) { |
| super(repeaterDefinition); |
| this.definition = repeaterDefinition; |
| // Setup initial size. Do not call addRow() as it will call initialize() |
| // on the newly created rows, which is not what we want here. |
| for (int i = 0; i < this.definition.getInitialSize(); i++) { |
| rows.add(new RepeaterRow(definition)); |
| } |
| |
| this.orderable = this.definition.getOrderable(); |
| this.listener = this.definition.getRepeaterListener(); |
| } |
| |
| public WidgetDefinition getDefinition() { |
| return definition; |
| } |
| |
| public void initialize() { |
| for (int i = 0; i < this.rows.size(); i++) { |
| ((RepeaterRow)rows.get(i)).initialize(); |
| // TODO(SG) Is this safe !? |
| broadcastEvent(new RepeaterEvent(this, RepeaterEventAction.ROW_ADDED, i)); |
| } |
| super.initialize(); |
| } |
| |
| public int getSize() { |
| return rows.size(); |
| } |
| |
| public int getMinSize() { |
| return this.definition.getMinSize(); |
| } |
| |
| public int getMaxSize() { |
| return this.definition.getMaxSize(); |
| } |
| |
| public RepeaterRow addRow() { |
| RepeaterRow repeaterRow = new RepeaterRow(definition); |
| rows.add(repeaterRow); |
| repeaterRow.initialize(); |
| getForm().addWidgetUpdate(this); |
| broadcastEvent(new RepeaterEvent(this, RepeaterEventAction.ROW_ADDED, rows.size() - 1)); |
| return repeaterRow; |
| } |
| |
| public RepeaterRow addRow(int index) { |
| RepeaterRow repeaterRow = new RepeaterRow(definition); |
| if (index >= this.rows.size()) { |
| rows.add(repeaterRow); |
| index = rows.size() - 1; |
| } else { |
| rows.add(index, repeaterRow); |
| } |
| repeaterRow.initialize(); |
| getForm().addWidgetUpdate(this); |
| broadcastEvent(new RepeaterEvent(this, RepeaterEventAction.ROW_ADDED, index)); |
| return repeaterRow; |
| } |
| |
| public RepeaterRow getRow(int index) { |
| return (RepeaterRow)rows.get(index); |
| } |
| |
| /** |
| * Overrides {@link AbstractWidget#getChild(String)} to return the |
| * repeater-row indicated by the index in 'id' |
| * |
| * @param id index of the row as a string-id |
| * @return the repeater-row at the specified index |
| */ |
| public Widget getChild(String id) { |
| int rowIndex; |
| try { |
| rowIndex = Integer.parseInt(id); |
| } catch (NumberFormatException nfe) { |
| // Not a number |
| return null; |
| } |
| |
| if (rowIndex < 0 || rowIndex >= getSize()) { |
| return null; |
| } |
| |
| return getRow(rowIndex); |
| } |
| |
| /** |
| * Crawls up the parents of a widget up to finding a repeater row. |
| * |
| * @param widget the widget whose row is to be found |
| * @return the repeater row |
| */ |
| public static RepeaterRow getParentRow(Widget widget) { |
| Widget result = widget; |
| while(result != null && ! (result instanceof Repeater.RepeaterRow)) { |
| result = result.getParent(); |
| } |
| |
| if (result == null) { |
| throw new RuntimeException("Could not find a parent row for widget " + widget); |
| } |
| return (Repeater.RepeaterRow)result; |
| } |
| |
| /** |
| * Get the position of a row in this repeater. |
| * @param row the row which we search the index for |
| * @return the row position or -1 if this row is not in this repeater |
| */ |
| public int indexOf(RepeaterRow row) { |
| return this.rows.indexOf(row); |
| } |
| |
| /** |
| * @throws IndexOutOfBoundsException if the the index is outside the range of existing rows. |
| */ |
| public void removeRow(int index) { |
| broadcastEvent(new RepeaterEvent(this, RepeaterEventAction.ROW_DELETING, index)); |
| rows.remove(index); |
| getForm().addWidgetUpdate(this); |
| broadcastEvent(new RepeaterEvent(this, RepeaterEventAction.ROW_DELETED, index)); |
| } |
| |
| /** |
| * Move a row from one place to another |
| * @param from the existing row position |
| * @param to the target position. The "from" item will be moved before that position. |
| */ |
| public void moveRow(int from, int to) { |
| int size = this.rows.size(); |
| |
| if (from < 0 || from >= size || to < 0 || to > size) { |
| throw new IllegalArgumentException("Cannot move from " + from + " to " + to + |
| " on repeater with " + size + " rows"); |
| } |
| |
| if (from == to) { |
| return; |
| } |
| |
| Object fromRow = this.rows.remove(from); |
| if (to == size) { |
| // Move at the end |
| this.rows.add(fromRow); |
| |
| } else if (to > from) { |
| // Index of "to" was moved by removing |
| this.rows.add(to - 1, fromRow); |
| |
| } else { |
| this.rows.add(to, fromRow); |
| } |
| |
| getForm().addWidgetUpdate(this); |
| broadcastEvent(new RepeaterEvent(this, RepeaterEventAction.ROWS_REARRANGED)); |
| } |
| |
| /** |
| * Move a row from one place to another. In contrast to {@link #moveRow}, this |
| * method treats the to-index as the exact row-index where you want to have the |
| * row moved to. |
| * |
| * @param from the existing row position |
| * @param to the target position. The "from" item will be moved before that position. |
| */ |
| public void moveRow2(int from, int to) { |
| int size = this.rows.size(); |
| |
| if (from < 0 || from >= size || to < 0 || to >= size) { |
| throw new IllegalArgumentException("Cannot move from " + from + " to " + to + |
| " on repeater with " + size + " rows"); |
| } |
| |
| if (from == to) { |
| return; |
| } |
| |
| Object fromRow = this.rows.remove(from); |
| this.rows.add(to, fromRow); |
| |
| getForm().addWidgetUpdate(this); |
| broadcastEvent(new RepeaterEvent(this, RepeaterEventAction.ROWS_REARRANGED)); |
| } |
| |
| public void moveRowLeft(int index) { |
| if (index == 0 || index >= this.rows.size()) { |
| // do nothing |
| } else { |
| Object temp = this.rows.get(index-1); |
| this.rows.set(index-1, this.rows.get(index)); |
| this.rows.set(index, temp); |
| } |
| getForm().addWidgetUpdate(this); |
| broadcastEvent(new RepeaterEvent(this, RepeaterEventAction.ROWS_REARRANGED)); |
| } |
| |
| public void moveRowRight(int index) { |
| if (index < 0 || index >= this.rows.size() - 1) { |
| // do nothing |
| } else { |
| Object temp = this.rows.get(index+1); |
| this.rows.set(index+1, this.rows.get(index)); |
| this.rows.set(index, temp); |
| } |
| getForm().addWidgetUpdate(this); |
| broadcastEvent(new RepeaterEvent(this, RepeaterEventAction.ROWS_REARRANGED)); |
| } |
| |
| /** |
| * @deprecated See {@link #clear()} |
| * |
| */ |
| public void removeRows() { |
| clear(); |
| } |
| |
| /** |
| * Clears all rows from the repeater and go back to the initial size |
| */ |
| public void clear() { |
| broadcastEvent(new RepeaterEvent(this, RepeaterEventAction.ROWS_CLEARING)); |
| rows.clear(); |
| broadcastEvent(new RepeaterEvent(this, RepeaterEventAction.ROWS_CLEARED)); |
| |
| // and reset to initial size |
| for (int i = 0; i < this.definition.getInitialSize(); i++) { |
| addRow(); |
| } |
| getForm().addWidgetUpdate(this); |
| } |
| |
| public void addRepeaterListener(RepeaterListener listener) { |
| this.listener = WidgetEventMulticaster.add(this.listener, listener); |
| } |
| |
| public void removeRepeaterListener(RepeaterListener listener) { |
| this.listener = WidgetEventMulticaster.remove(this.listener, listener); |
| } |
| |
| public boolean hasRepeaterListeners() { |
| return this.listener != null; |
| } |
| |
| public void broadcastEvent(WidgetEvent event) { |
| if (event instanceof RepeaterEvent) { |
| if (this.listener != null) { |
| this.listener.repeaterModified((RepeaterEvent)event); |
| } |
| } else { |
| // Other kinds of events |
| super.broadcastEvent(event); |
| } |
| } |
| |
| |
| /** |
| * Gets a widget on a certain row. |
| * @param rowIndex startin from 0 |
| * @param id a widget id |
| * @return null if there's no such widget |
| */ |
| public Widget getWidget(int rowIndex, String id) { |
| RepeaterRow row = (RepeaterRow)rows.get(rowIndex); |
| return row.getChild(id); |
| } |
| |
| public void readFromRequest(FormContext formContext) { |
| if (!getCombinedState().isAcceptingInputs()) { |
| return; |
| } |
| |
| // read number of rows from request, and make an according number of rows |
| Request req = formContext.getRequest(); |
| String paramName = getRequestParameterName(); |
| |
| String sizeParameter = req.getParameter(paramName + ".size"); |
| if (sizeParameter != null) { |
| int size = 0; |
| try { |
| size = Integer.parseInt(sizeParameter); |
| } catch (NumberFormatException exc) { |
| // do nothing |
| } |
| |
| // some protection against people who might try to exhaust the server by supplying very large |
| // size parameters |
| if (size > 500) { |
| throw new RuntimeException("Client is not allowed to specify a repeater size larger than 500."); |
| } |
| |
| int currentSize = getSize(); |
| if (currentSize < size) { |
| for (int i = currentSize; i < size; i++) { |
| addRow(); |
| } |
| } else if (currentSize > size) { |
| for (int i = currentSize - 1; i >= size; i--) { |
| removeRow(i); |
| } |
| } |
| } |
| |
| // let the rows read their data from the request |
| Iterator rowIt = rows.iterator(); |
| while (rowIt.hasNext()) { |
| RepeaterRow row = (RepeaterRow)rowIt.next(); |
| row.readFromRequest(formContext); |
| } |
| |
| // Handle repeater-level actions |
| String action = req.getParameter(paramName + ".action"); |
| if (action == null) { |
| return; |
| } |
| |
| // Handle row move. It's important for this to happen *after* row.readFromRequest, |
| // as reordering rows changes their IDs and therefore their child widget's ID too. |
| if (action.equals("move")) { |
| if (!this.orderable) { |
| throw new FormsRuntimeException("Widget " + this + " is not orderable", |
| getLocation()); |
| } |
| |
| int from = Integer.parseInt(req.getParameter(paramName + ".from")); |
| int before = Integer.parseInt(req.getParameter(paramName + ".before")); |
| |
| Object row = this.rows.get(from); |
| // Add to the new location |
| this.rows.add(before, row); |
| // Remove from the previous one, taking into account potential location change |
| // because of the previous add() |
| if (before < from) from++; |
| this.rows.remove(from); |
| |
| // Needs refresh |
| getForm().addWidgetUpdate(this); |
| |
| } else { |
| throw new FormsRuntimeException("Unknown action " + action + " for " + this, getLocation()); |
| } |
| } |
| |
| /** |
| * @see org.apache.cocoon.forms.formmodel.Widget#validate() |
| */ |
| public boolean validate() { |
| if (!getCombinedState().isValidatingValues()) { |
| this.wasValid = true; |
| return true; |
| } |
| |
| boolean valid = true; |
| Iterator rowIt = rows.iterator(); |
| while (rowIt.hasNext()) { |
| RepeaterRow row = (RepeaterRow)rowIt.next(); |
| valid = valid & row.validate(); |
| } |
| |
| if (rows.size() > getMaxSize() || rows.size() < getMinSize()) { |
| String [] boundaries = new String[2]; |
| boundaries[0] = String.valueOf(getMinSize()); |
| boundaries[1] = String.valueOf(getMaxSize()); |
| this.validationError = new ValidationError(new I18nMessage("repeater.cardinality", boundaries, FormsConstants.I18N_CATALOGUE)); |
| valid=false; |
| } |
| |
| |
| if (valid) { |
| valid = super.validate(); |
| } |
| |
| this.wasValid = valid && this.validationError == null; |
| return this.wasValid; |
| } |
| |
| |
| |
| /** |
| * @return "repeater" |
| */ |
| public String getXMLElementName() { |
| return REPEATER_EL; |
| } |
| |
| |
| |
| /** |
| * Adds @size attribute |
| */ |
| public AttributesImpl getXMLElementAttributes() { |
| AttributesImpl attrs = super.getXMLElementAttributes(); |
| attrs.addCDATAAttribute("size", String.valueOf(getSize())); |
| // Generate the min and max sizes if they don't have the default value |
| int size = getMinSize(); |
| if (size > 0) { |
| attrs.addCDATAAttribute("min-size", String.valueOf(size)); |
| } |
| size = getMaxSize(); |
| if (size != Integer.MAX_VALUE) { |
| attrs.addCDATAAttribute("max-size", String.valueOf(size)); |
| } |
| return attrs; |
| } |
| |
| |
| public void generateDisplayData(ContentHandler contentHandler) |
| throws SAXException { |
| // the repeater's label |
| contentHandler.startElement(FormsConstants.INSTANCE_NS, LABEL_EL, FormsConstants.INSTANCE_PREFIX_COLON + LABEL_EL, XMLUtils.EMPTY_ATTRIBUTES); |
| generateLabel(contentHandler); |
| contentHandler.endElement(FormsConstants.INSTANCE_NS, LABEL_EL, FormsConstants.INSTANCE_PREFIX_COLON + LABEL_EL); |
| |
| // heading element -- currently contains the labels of each widget in the repeater |
| contentHandler.startElement(FormsConstants.INSTANCE_NS, HEADINGS_EL, FormsConstants.INSTANCE_PREFIX_COLON + HEADINGS_EL, XMLUtils.EMPTY_ATTRIBUTES); |
| Iterator widgetDefinitionIt = definition.getWidgetDefinitions().iterator(); |
| while (widgetDefinitionIt.hasNext()) { |
| WidgetDefinition widgetDefinition = (WidgetDefinition)widgetDefinitionIt.next(); |
| contentHandler.startElement(FormsConstants.INSTANCE_NS, HEADING_EL, FormsConstants.INSTANCE_PREFIX_COLON + HEADING_EL, XMLUtils.EMPTY_ATTRIBUTES); |
| widgetDefinition.generateLabel(contentHandler); |
| contentHandler.endElement(FormsConstants.INSTANCE_NS, HEADING_EL, FormsConstants.INSTANCE_PREFIX_COLON + HEADING_EL); |
| } |
| contentHandler.endElement(FormsConstants.INSTANCE_NS, HEADINGS_EL, FormsConstants.INSTANCE_PREFIX_COLON + HEADINGS_EL); |
| } |
| |
| public void generateItemSaxFragment(ContentHandler contentHandler, Locale locale) throws SAXException { |
| // the actual rows in the repeater |
| Iterator rowIt = rows.iterator(); |
| while (rowIt.hasNext()) { |
| RepeaterRow row = (RepeaterRow)rowIt.next(); |
| row.generateSaxFragment(contentHandler, locale); |
| } |
| } |
| |
| /** |
| * Generates the label of a certain widget in this repeater. |
| */ |
| public void generateWidgetLabel(String widgetId, ContentHandler contentHandler) throws SAXException { |
| WidgetDefinition widgetDefinition = definition.getWidgetDefinition(widgetId); |
| if (widgetDefinition == null) { |
| throw new SAXException("Repeater '" + getRequestParameterName() + "' at " + getLocation() |
| + " contains no widget with id '" + widgetId + "'."); |
| } |
| |
| widgetDefinition.generateLabel(contentHandler); |
| } |
| |
| /** |
| * Generates a repeater-size element with a size attribute indicating the size of this repeater. |
| */ |
| public void generateSize(ContentHandler contentHandler) throws SAXException { |
| AttributesImpl attrs = getXMLElementAttributes(); |
| contentHandler.startElement(FormsConstants.INSTANCE_NS, REPEATER_SIZE_EL, FormsConstants.INSTANCE_PREFIX_COLON + REPEATER_SIZE_EL, attrs); |
| contentHandler.endElement(FormsConstants.INSTANCE_NS, REPEATER_SIZE_EL, FormsConstants.INSTANCE_PREFIX_COLON + REPEATER_SIZE_EL); |
| } |
| |
| /** |
| * Set a validation error on this field. This allows repeaters be externally marked as invalid by |
| * application logic. |
| * |
| * @return the validation error |
| */ |
| public ValidationError getValidationError() { |
| return this.validationError; |
| } |
| |
| /** |
| * set a validation error |
| */ |
| public void setValidationError(ValidationError error) { |
| this.validationError = error; |
| } |
| |
| public class RepeaterRow extends AbstractContainerWidget { |
| |
| private static final String ROW_EL = "repeater-row"; |
| |
| public RepeaterRow(RepeaterDefinition definition) { |
| super(definition); |
| setParent(Repeater.this); |
| definition.createWidgets(this); |
| } |
| |
| public WidgetDefinition getDefinition() { |
| return Repeater.this.getDefinition(); |
| } |
| |
| private int cachedPosition = -100; |
| private String cachedId = "--undefined--"; |
| |
| public String getId() { |
| int pos = rows.indexOf(this); |
| if (pos == -1) { |
| throw new IllegalStateException("Row has currently no position"); |
| } |
| |
| if (pos != this.cachedPosition) { |
| this.cachedPosition = pos; |
| // id of a RepeaterRow is the position of the row in the list of rows. |
| this.cachedId = String.valueOf(pos); |
| widgetNameChanged(); |
| } |
| return this.cachedId; |
| } |
| |
| public String getRequestParameterName() { |
| // Get the id to check potential position change |
| getId(); |
| |
| return super.getRequestParameterName(); |
| } |
| |
| public Form getForm() { |
| return Repeater.this.getForm(); |
| } |
| |
| public void initialize() { |
| // Initialize children but don't call super.initialize() that would call the repeater's |
| // on-create handlers for each row. |
| Iterator i = getChildren(); |
| while (i.hasNext()) { |
| ((Widget) i.next()).initialize(); |
| } |
| } |
| |
| public boolean validate() { |
| // Validate only child widtgets, as the definition's validators are those of the parent repeater |
| return widgets.validate(); |
| } |
| |
| /** |
| * @return "repeater-row" |
| */ |
| public String getXMLElementName() { |
| return ROW_EL; |
| } |
| |
| public void generateLabel(ContentHandler contentHandler) throws SAXException { |
| // this widget has its label generated in the context of the repeater |
| } |
| |
| public void generateDisplayData(ContentHandler contentHandler) |
| throws SAXException { |
| // this widget has its display-data generated in the context of the repeater |
| } |
| |
| public void broadcastEvent(WidgetEvent event) { |
| throw new UnsupportedOperationException("Widget " + this + " doesn't handle events."); |
| } |
| } |
| |
| } |