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.binding;
018:
019: import java.util.ArrayList;
020: import java.util.Collections;
021: import java.util.HashSet;
022: import java.util.Iterator;
023: import java.util.List;
024: import java.util.Set;
025:
026: import org.apache.avalon.framework.logger.Logger;
027:
028: import org.apache.cocoon.forms.datatype.convertor.ConversionResult;
029: import org.apache.cocoon.forms.formmodel.Repeater;
030: import org.apache.cocoon.forms.formmodel.Widget;
031:
032: import org.apache.commons.collections.ListUtils;
033: import org.apache.commons.jxpath.JXPathContext;
034: import org.apache.commons.jxpath.Pointer;
035:
036: /**
037: * RepeaterJXPathBinding provides an implementation of a {@link Binding}
038: * that allows for bidirectional binding of a repeater-widget to/from
039: * repeating structures in the back-end object model.
040: *
041: * @version $Id: RepeaterJXPathBinding.java 601839 2007-12-06 20:05:24Z antonio $
042: */
043: public class RepeaterJXPathBinding extends JXPathBindingBase {
044:
045: private final String repeaterId;
046: private final String repeaterPath;
047: private final String rowPath;
048: private final String rowPathForInsert;
049: private final JXPathBindingBase rowBinding;
050: private final JXPathBindingBase insertRowBinding;
051: private final JXPathBindingBase deleteRowBinding;
052: private final ComposedJXPathBindingBase identityBinding;
053:
054: /**
055: * Constructs RepeaterJXPathBinding
056: */
057: public RepeaterJXPathBinding(
058: JXPathBindingBuilderBase.CommonAttributes commonAtts,
059: String repeaterId, String repeaterPath, String rowPath,
060: String rowPathForInsert, JXPathBindingBase[] childBindings,
061: JXPathBindingBase insertBinding,
062: JXPathBindingBase[] deleteBindings,
063: JXPathBindingBase[] identityBindings) {
064: super (commonAtts);
065: this .repeaterId = repeaterId;
066: this .repeaterPath = repeaterPath;
067: this .rowPath = rowPath;
068: this .rowPathForInsert = rowPathForInsert;
069:
070: this .rowBinding = new ComposedJXPathBindingBase(
071: JXPathBindingBuilderBase.CommonAttributes.DEFAULT,
072: childBindings);
073: this .rowBinding.setParent(this );
074:
075: this .insertRowBinding = insertBinding;
076: if (this .insertRowBinding != null) {
077: this .insertRowBinding.setParent(this );
078: }
079:
080: if (deleteBindings != null) {
081: this .deleteRowBinding = new ComposedJXPathBindingBase(
082: JXPathBindingBuilderBase.CommonAttributes.DEFAULT,
083: deleteBindings);
084: this .deleteRowBinding.setParent(this );
085: } else {
086: this .deleteRowBinding = null;
087: }
088:
089: if (identityBindings != null) {
090:
091: this .identityBinding = new ComposedJXPathBindingBase(
092: JXPathBindingBuilderBase.CommonAttributes.DEFAULT,
093: identityBindings);
094: this .identityBinding.setParent(this );
095: } else
096: this .identityBinding = null;
097: }
098:
099: public void enableLogging(Logger logger) {
100: super .enableLogging(logger);
101: if (this .deleteRowBinding != null) {
102: this .deleteRowBinding.enableLogging(logger);
103: }
104: if (this .insertRowBinding != null) {
105: this .insertRowBinding.enableLogging(logger);
106: }
107: this .rowBinding.enableLogging(logger);
108: if (this .identityBinding != null) {
109: this .identityBinding.enableLogging(logger);
110: }
111: }
112:
113: public String getId() {
114: return repeaterId;
115: }
116:
117: public String getRepeaterPath() {
118: return repeaterPath;
119: }
120:
121: public String getRowPath() {
122: return rowPath;
123: }
124:
125: public String getInsertRowPath() {
126: return rowPathForInsert;
127: }
128:
129: public ComposedJXPathBindingBase getRowBinding() {
130: return (ComposedJXPathBindingBase) rowBinding;
131: }
132:
133: public ComposedJXPathBindingBase getDeleteRowBinding() {
134: return (ComposedJXPathBindingBase) deleteRowBinding;
135: }
136:
137: public ComposedJXPathBindingBase getIdentityBinding() {
138: return identityBinding;
139: }
140:
141: public JXPathBindingBase getInsertRowBinding() {
142: return insertRowBinding;
143: }
144:
145: /**
146: * Binds the unique-id of the repeated rows, and narrows the context on
147: * objectModelContext and Repeater to the repeated rows before handing
148: * over to the actual binding-children.
149: */
150: public void doLoad(Widget frmModel, JXPathContext jxpc)
151: throws BindingException {
152: // Find the repeater
153: Repeater repeater = (Repeater) selectWidget(frmModel,
154: this .repeaterId);
155: if (repeater == null) {
156: throw new BindingException(
157: "The repeater with the ID ["
158: + this .repeaterId
159: + "] referenced in the binding does not exist in the form definition.");
160: }
161:
162: repeater.clear();
163:
164: Pointer ptr = jxpc.getPointer(this .repeaterPath);
165: if (ptr.getNode() != null) {
166: // There are some nodes to load from
167: final int initialSize = repeater.getSize();
168:
169: // build a jxpath iterator for pointers
170: JXPathContext repeaterContext = jxpc
171: .getRelativeContext(ptr);
172: Iterator rowPointers = repeaterContext
173: .iteratePointers(this .rowPath);
174: //iterate through it
175: int currentRow = 0;
176: while (rowPointers.hasNext()) {
177: // create a new row, take that as the frmModelSubContext
178: Repeater.RepeaterRow this Row;
179: if (currentRow < initialSize) {
180: this Row = repeater.getRow(currentRow++);
181: } else {
182: this Row = repeater.addRow();
183: }
184: // make a jxpath ObjectModelSubcontext on the iterated element
185: Pointer jxp = (Pointer) rowPointers.next();
186: JXPathContext rowContext = repeaterContext
187: .getRelativeContext(jxp);
188: // hand it over to children
189: if (this .identityBinding != null) {
190: this .identityBinding.loadFormFromModel(this Row,
191: rowContext);
192: }
193: this .rowBinding.loadFormFromModel(this Row, rowContext);
194: }
195: }
196: if (getLogger().isDebugEnabled())
197: getLogger().debug("done loading rows " + this );
198: }
199:
200: /**
201: * Uses the mapped identity of each row to detect if rows have been
202: * updated, inserted or removed. Depending on what happened the appropriate
203: * child-bindings are allowed to visit the narrowed contexts.
204: */
205: public void doSave(Widget frmModel, JXPathContext jxpc)
206: throws BindingException {
207: // Find the repeater
208: Repeater repeater = (Repeater) selectWidget(frmModel,
209: this .repeaterId);
210:
211: // and his context, creating the path if needed
212: JXPathContext repeaterContext = jxpc.getRelativeContext(jxpc
213: .createPath(this .repeaterPath));
214:
215: // create set of updatedRowIds
216: Set updatedRows = new HashSet();
217: //create list of rows to insert at end
218: List rowsToInsert = new ArrayList();
219:
220: // iterate rows in the form model...
221: int formRowCount = repeater.getSize();
222: for (int i = 0; i < formRowCount; i++) {
223: Repeater.RepeaterRow this Row = repeater.getRow(i);
224:
225: // Get the identity
226: List identity = getIdentity(this Row);
227:
228: if (hasNonNullElements(identity)) {
229: // iterate nodes to find match
230: Iterator rowPointers = repeaterContext
231: .iteratePointers(this .rowPath);
232: boolean found = false;
233: while (rowPointers.hasNext()) {
234: Pointer jxp = (Pointer) rowPointers.next();
235: JXPathContext rowContext = repeaterContext
236: .getRelativeContext(jxp);
237: List contextIdentity = getIdentity(rowContext);
238: if (ListUtils
239: .isEqualList(identity, contextIdentity)) {
240: // match! --> bind to children
241: this .rowBinding.saveFormToModel(this Row,
242: rowContext);
243: // --> store rowIdValue in list of updatedRowIds
244: updatedRows.add(identity);
245: found = true;
246: break;
247: }
248: }
249: if (!found) {
250: // this is a new row
251: rowsToInsert.add(this Row);
252: // also add it to the updated row id's so that this row doesn't get deleted
253: updatedRows.add(identity);
254: }
255: } else {
256: // if there is no value to determine the identity --> this is a new row
257: rowsToInsert.add(this Row);
258: }
259: }
260: // Iterate again nodes for deletion
261: Iterator rowPointers = repeaterContext
262: .iteratePointers(this .rowPath);
263: List rowsToDelete = new ArrayList();
264: while (rowPointers.hasNext()) {
265: Pointer jxp = (Pointer) rowPointers.next();
266: JXPathContext rowContext = repeaterContext
267: .getRelativeContext((Pointer) jxp.clone());
268: List contextIdentity = getIdentity(rowContext);
269: // check if the identity of the rowContext is in the updated rows
270: // if not --> bind for delete
271: if (!isIdentityInUpdatedRows(updatedRows, contextIdentity)) {
272: rowsToDelete.add(rowContext);
273: }
274: }
275: if (rowsToDelete.size() > 0) {
276: // run backwards through the list, so that we don't get into
277: // trouble by shifting indexes
278: for (int i = rowsToDelete.size() - 1; i >= 0; i--) {
279: if (this .deleteRowBinding != null) {
280: this .deleteRowBinding.saveFormToModel(frmModel,
281: rowsToDelete.get(i));
282: } else {
283: // Simply remove the corresponding path
284: ((JXPathContext) rowsToDelete.get(i))
285: .removePath(".");
286: }
287: }
288: }
289: // count how many we have now
290: int indexCount = 1;
291: rowPointers = repeaterContext
292: .iteratePointers(this .rowPathForInsert);
293: while (rowPointers.hasNext()) {
294: rowPointers.next();
295: indexCount++;
296: }
297: // end with rows to insert (to make sure they don't get deleted!)
298: if (rowsToInsert.size() > 0) {
299: Iterator rowIterator = rowsToInsert.iterator();
300: //register the factory!
301: while (rowIterator.hasNext()) {
302: Repeater.RepeaterRow this Row = (Repeater.RepeaterRow) rowIterator
303: .next();
304: // Perform the insert row binding.
305: if (this .insertRowBinding != null) {
306: this .insertRowBinding.saveFormToModel(repeater,
307: repeaterContext);
308: }
309: // --> create the path to let the context be created
310: Pointer newRowContextPointer = repeaterContext
311: .createPath(this .rowPathForInsert + "["
312: + indexCount + "]");
313: JXPathContext newRowContext = repeaterContext
314: .getRelativeContext(newRowContextPointer);
315: if (getLogger().isDebugEnabled()) {
316: getLogger().debug(
317: "inserted row at "
318: + newRowContextPointer.asPath());
319: }
320: // + rebind to children for update
321: this .rowBinding.saveFormToModel(this Row, newRowContext);
322: getLogger().debug("bound new row");
323: indexCount++;
324: }
325: // } else {
326: // if (getLogger().isWarnEnabled()) {
327: // getLogger().warn("RepeaterBinding has detected rows to insert, but misses "
328: // + "the <on-insert-row> binding to do it.");
329: // }
330: // }
331: }
332: if (getLogger().isDebugEnabled()) {
333: getLogger().debug("done saving rows " + this );
334: }
335: }
336:
337: /**
338: * Tests if an identity is already contained in a Set of identities.
339: * @param identitySet the Set of identities.
340: * @param identity the identity that is tested if it is already in the Set.
341: * @return true if the Set contains the identity, false otherwise.
342: */
343: private boolean isIdentityInUpdatedRows(Set identitySet,
344: List identity) {
345: Iterator iter = identitySet.iterator();
346: while (iter.hasNext()) {
347: List identityFromSet = (List) iter.next();
348: if (ListUtils.isEqualList(identityFromSet, identity)) {
349: return true;
350: }
351: }
352: return false;
353: }
354:
355: /**
356: * Tests if any of the elements in a List is not null.
357: * @param list
358: */
359: protected boolean hasNonNullElements(List list) {
360: Iterator iter = list.iterator();
361: while (iter.hasNext()) {
362: if (iter.next() != null) {
363: return true;
364: }
365: }
366: return false;
367: }
368:
369: /**
370: * Get the identity of the given row context. That's infact a list of all
371: * the values of the fields in the bean or XML that constitute the identity.
372: * @param rowContext
373: * @return List the identity of the row context
374: */
375: protected List getIdentity(JXPathContext rowContext) {
376: List identity = Collections.EMPTY_LIST;
377: if (this .identityBinding != null) {
378: JXPathBindingBase[] childBindings = this .identityBinding
379: .getChildBindings();
380: if (childBindings != null) {
381: int size = childBindings.length;
382: identity = new ArrayList(size);
383: for (int i = 0; i < size; i++) {
384: ValueJXPathBinding vBinding = (ValueJXPathBinding) childBindings[i];
385: Object value = rowContext.getValue(vBinding
386: .getXPath());
387: if (value != null
388: && vBinding.getConvertor() != null) {
389: if (value instanceof String) {
390: ConversionResult conversionResult = vBinding
391: .getConvertor()
392: .convertFromString(
393: (String) value,
394: vBinding
395: .getConvertorLocale(),
396: null);
397: if (conversionResult.isSuccessful()) {
398: value = conversionResult.getResult();
399: } else {
400: value = null;
401: }
402: } else {
403: if (getLogger().isWarnEnabled()) {
404: getLogger()
405: .warn(
406: "Convertor ignored on backend-value "
407: + "which isn't of type String.");
408: }
409: }
410: }
411: identity.add(value);
412: }
413: }
414: }
415: return identity;
416: }
417:
418: /**
419: * Get the identity of the given row. That's in fact a list of all the values
420: * of the fields in the form model that constitute the identity.
421: * @param row
422: * @return List the identity of the row
423: */
424: protected List getIdentity(Repeater.RepeaterRow row) {
425: // quit if we don't have an identity binding
426: List identity = Collections.EMPTY_LIST;
427: if (this .identityBinding != null) {
428: JXPathBindingBase[] childBindings = this .identityBinding
429: .getChildBindings();
430: if (childBindings != null) {
431: int size = childBindings.length;
432: identity = new ArrayList(size);
433: for (int i = 0; i < size; i++) {
434: String fieldId = ((ValueJXPathBinding) childBindings[i])
435: .getFieldId();
436: Widget widget = row.lookupWidget(fieldId);
437: Object value = widget.getValue();
438: identity.add(value);
439: }
440: }
441: }
442: return identity;
443: }
444:
445: public String toString() {
446: return "RepeaterJXPathBinding [widget=" + this .repeaterId
447: + ", xpath=" + this .repeaterPath + "]";
448: }
449: }
|