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