| /* |
| * 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.binding; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.apache.avalon.framework.logger.Logger; |
| |
| import org.apache.cocoon.forms.datatype.convertor.ConversionResult; |
| import org.apache.cocoon.forms.formmodel.Repeater; |
| import org.apache.cocoon.forms.formmodel.Widget; |
| |
| import org.apache.commons.collections.ListUtils; |
| import org.apache.commons.jxpath.JXPathContext; |
| import org.apache.commons.jxpath.Pointer; |
| |
| /** |
| * RepeaterJXPathBinding provides an implementation of a {@link Binding} |
| * that allows for bidirectional binding of a repeater-widget to/from |
| * repeating structures in the back-end object model. |
| * |
| * @version $Id$ |
| */ |
| public class RepeaterJXPathBinding extends JXPathBindingBase { |
| |
| private final String repeaterId; |
| private final String repeaterPath; |
| private final String rowPath; |
| private final String rowPathForInsert; |
| private final JXPathBindingBase rowBinding; |
| private final JXPathBindingBase insertRowBinding; |
| private final JXPathBindingBase deleteRowBinding; |
| private final ComposedJXPathBindingBase identityBinding; |
| |
| /** |
| * Constructs RepeaterJXPathBinding |
| */ |
| public RepeaterJXPathBinding(JXPathBindingBuilderBase.CommonAttributes commonAtts, |
| String repeaterId, |
| String repeaterPath, |
| String rowPath, |
| String rowPathForInsert, |
| JXPathBindingBase[] childBindings, |
| JXPathBindingBase insertBinding, |
| JXPathBindingBase[] deleteBindings, |
| JXPathBindingBase[] identityBindings) { |
| super(commonAtts); |
| this.repeaterId = repeaterId; |
| this.repeaterPath = repeaterPath; |
| this.rowPath = rowPath; |
| this.rowPathForInsert = rowPathForInsert; |
| |
| this.rowBinding = new ComposedJXPathBindingBase( |
| JXPathBindingBuilderBase.CommonAttributes.DEFAULT, |
| childBindings); |
| this.rowBinding.setParent(this); |
| |
| this.insertRowBinding = insertBinding; |
| if (this.insertRowBinding != null) { |
| this.insertRowBinding.setParent(this); |
| } |
| |
| if (deleteBindings != null) { |
| this.deleteRowBinding = new ComposedJXPathBindingBase( |
| JXPathBindingBuilderBase.CommonAttributes.DEFAULT, |
| deleteBindings); |
| this.deleteRowBinding.setParent(this); |
| } else { |
| this.deleteRowBinding = null; |
| } |
| |
| |
| if (identityBindings != null) { |
| |
| this.identityBinding = new ComposedJXPathBindingBase( |
| JXPathBindingBuilderBase.CommonAttributes.DEFAULT, |
| identityBindings); |
| this.identityBinding.setParent(this); |
| } |
| else |
| this.identityBinding = null; |
| } |
| |
| public void enableLogging(Logger logger) { |
| super.enableLogging(logger); |
| if (this.deleteRowBinding != null) { |
| this.deleteRowBinding.enableLogging(logger); |
| } |
| if (this.insertRowBinding != null) { |
| this.insertRowBinding.enableLogging(logger); |
| } |
| this.rowBinding.enableLogging(logger); |
| if (this.identityBinding != null) { |
| this.identityBinding.enableLogging(logger); |
| } |
| } |
| |
| public String getId() { return repeaterId; } |
| public String getRepeaterPath() { return repeaterPath; } |
| public String getRowPath() { return rowPath; } |
| public String getInsertRowPath() { return rowPathForInsert; } |
| public ComposedJXPathBindingBase getRowBinding() { return (ComposedJXPathBindingBase)rowBinding; } |
| public ComposedJXPathBindingBase getDeleteRowBinding() { return (ComposedJXPathBindingBase)deleteRowBinding; } |
| public ComposedJXPathBindingBase getIdentityBinding() { return identityBinding; } |
| public JXPathBindingBase getInsertRowBinding() { return insertRowBinding; } |
| |
| /** |
| * Binds the unique-id of the repeated rows, and narrows the context on |
| * objectModelContext and Repeater to the repeated rows before handing |
| * over to the actual binding-children. |
| */ |
| public void doLoad(Widget frmModel, JXPathContext jxpc) |
| throws BindingException { |
| // Find the repeater |
| Repeater repeater = (Repeater) selectWidget(frmModel, this.repeaterId); |
| if (repeater == null) { |
| throw new BindingException("The repeater with the ID [" + this.repeaterId |
| + "] referenced in the binding does not exist in the form definition."); |
| } |
| |
| repeater.clear(); |
| |
| Pointer ptr = jxpc.getPointer(this.repeaterPath); |
| if (ptr.getNode() != null) { |
| // There are some nodes to load from |
| final int initialSize = repeater.getSize(); |
| |
| // build a jxpath iterator for pointers |
| JXPathContext repeaterContext = jxpc.getRelativeContext(ptr); |
| Iterator rowPointers = repeaterContext.iteratePointers(this.rowPath); |
| //iterate through it |
| int currentRow = 0; |
| while (rowPointers.hasNext()) { |
| // create a new row, take that as the frmModelSubContext |
| Repeater.RepeaterRow thisRow; |
| if (currentRow < initialSize) { |
| thisRow = repeater.getRow(currentRow++); |
| } else { |
| thisRow = repeater.addRow(); |
| } |
| // make a jxpath ObjectModelSubcontext on the iterated element |
| Pointer jxp = (Pointer)rowPointers.next(); |
| JXPathContext rowContext = repeaterContext.getRelativeContext(jxp); |
| // hand it over to children |
| if (this.identityBinding != null) { |
| this.identityBinding.loadFormFromModel(thisRow, rowContext); |
| } |
| this.rowBinding.loadFormFromModel(thisRow, rowContext); |
| } |
| } |
| if (getLogger().isDebugEnabled()) |
| getLogger().debug("done loading rows " + this); |
| } |
| |
| /** |
| * Uses the mapped identity of each row to detect if rows have been |
| * updated, inserted or removed. Depending on what happened the appropriate |
| * child-bindings are allowed to visit the narrowed contexts. |
| */ |
| public void doSave(Widget frmModel, JXPathContext jxpc) |
| throws BindingException { |
| // Find the repeater |
| Repeater repeater = (Repeater) selectWidget(frmModel, this.repeaterId); |
| |
| // and his context, creating the path if needed |
| JXPathContext repeaterContext = |
| jxpc.getRelativeContext(jxpc.createPath(this.repeaterPath)); |
| |
| // create set of updatedRowIds |
| Set updatedRows = new HashSet(); |
| //create list of rows to insert at end |
| List rowsToInsert = new ArrayList(); |
| |
| // iterate rows in the form model... |
| int formRowCount = repeater.getSize(); |
| for (int i = 0; i < formRowCount; i++) { |
| Repeater.RepeaterRow thisRow = repeater.getRow(i); |
| |
| // Get the identity |
| List identity = getIdentity(thisRow); |
| |
| if (hasNonNullElements(identity)) { |
| // iterate nodes to find match |
| Iterator rowPointers = repeaterContext.iteratePointers(this.rowPath); |
| boolean found = false; |
| while (rowPointers.hasNext()) { |
| Pointer jxp = (Pointer) rowPointers.next(); |
| JXPathContext rowContext = repeaterContext.getRelativeContext(jxp); |
| List contextIdentity = getIdentity(rowContext); |
| if (ListUtils.isEqualList(identity, contextIdentity)) { |
| // match! --> bind to children |
| this.rowBinding.saveFormToModel(thisRow, rowContext); |
| // --> store rowIdValue in list of updatedRowIds |
| updatedRows.add(identity); |
| found = true; |
| break; |
| } |
| } |
| if (!found) { |
| // this is a new row |
| rowsToInsert.add(thisRow); |
| // also add it to the updated row id's so that this row doesn't get deleted |
| updatedRows.add(identity); |
| } |
| } else { |
| // if there is no value to determine the identity --> this is a new row |
| rowsToInsert.add(thisRow); |
| } |
| } |
| // Iterate again nodes for deletion |
| Iterator rowPointers = repeaterContext.iteratePointers(this.rowPath); |
| List rowsToDelete = new ArrayList(); |
| while (rowPointers.hasNext()) { |
| Pointer jxp = (Pointer)rowPointers.next(); |
| JXPathContext rowContext = repeaterContext.getRelativeContext((Pointer)jxp.clone()); |
| List contextIdentity = getIdentity(rowContext); |
| // check if the identity of the rowContext is in the updated rows |
| // if not --> bind for delete |
| if (!isIdentityInUpdatedRows(updatedRows, contextIdentity)) { |
| rowsToDelete.add(rowContext); |
| } |
| } |
| if (rowsToDelete.size() > 0) { |
| // run backwards through the list, so that we don't get into |
| // trouble by shifting indexes |
| for (int i = rowsToDelete.size() - 1; i >= 0; i--) { |
| if (this.deleteRowBinding != null) { |
| this.deleteRowBinding.saveFormToModel(frmModel, |
| rowsToDelete.get(i)); |
| } else { |
| // Simply remove the corresponding path |
| ((JXPathContext)rowsToDelete.get(i)).removePath("."); |
| } |
| } |
| } |
| // count how many we have now |
| int indexCount = 1; |
| rowPointers = repeaterContext.iteratePointers(this.rowPathForInsert); |
| while (rowPointers.hasNext()) { |
| rowPointers.next(); |
| indexCount++; |
| } |
| // end with rows to insert (to make sure they don't get deleted!) |
| if (rowsToInsert.size() > 0) { |
| Iterator rowIterator = rowsToInsert.iterator(); |
| //register the factory! |
| while (rowIterator.hasNext()) { |
| Repeater.RepeaterRow thisRow = (Repeater.RepeaterRow)rowIterator.next(); |
| // Perform the insert row binding. |
| if (this.insertRowBinding != null) { |
| this.insertRowBinding.saveFormToModel(repeater, repeaterContext); |
| } |
| // --> create the path to let the context be created |
| Pointer newRowContextPointer = repeaterContext.createPath( |
| this.rowPathForInsert + "[" + indexCount + "]"); |
| JXPathContext newRowContext = |
| repeaterContext.getRelativeContext(newRowContextPointer); |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("inserted row at " + newRowContextPointer.asPath()); |
| } |
| // + rebind to children for update |
| this.rowBinding.saveFormToModel(thisRow, newRowContext); |
| getLogger().debug("bound new row"); |
| indexCount++; |
| } |
| // } else { |
| // if (getLogger().isWarnEnabled()) { |
| // getLogger().warn("RepeaterBinding has detected rows to insert, but misses " |
| // + "the <on-insert-row> binding to do it."); |
| // } |
| // } |
| } |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("done saving rows " + this); |
| } |
| } |
| |
| /** |
| * Tests if an identity is already contained in a Set of identities. |
| * @param identitySet the Set of identities. |
| * @param identity the identity that is tested if it is already in the Set. |
| * @return true if the Set contains the identity, false otherwise. |
| */ |
| private boolean isIdentityInUpdatedRows(Set identitySet, List identity) { |
| Iterator iter = identitySet.iterator(); |
| while (iter.hasNext()) { |
| List identityFromSet = (List)iter.next(); |
| if (ListUtils.isEqualList(identityFromSet, identity)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Tests if any of the elements in a List is not null. |
| * @param list |
| */ |
| protected boolean hasNonNullElements(List list) { |
| Iterator iter = list.iterator(); |
| while (iter.hasNext()) { |
| if (iter.next() != null) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Get the identity of the given row context. That's infact a list of all |
| * the values of the fields in the bean or XML that constitute the identity. |
| * @param rowContext |
| * @return List the identity of the row context |
| */ |
| protected List getIdentity(JXPathContext rowContext) { |
| List identity = Collections.EMPTY_LIST; |
| if (this.identityBinding != null) { |
| JXPathBindingBase[] childBindings = this.identityBinding.getChildBindings(); |
| if (childBindings != null) { |
| int size = childBindings.length; |
| identity = new ArrayList(size); |
| for (int i = 0; i < size; i++) { |
| ValueJXPathBinding vBinding = (ValueJXPathBinding)childBindings[i]; |
| Object value = rowContext.getValue(vBinding.getXPath()); |
| if (value != null && vBinding.getConvertor() != null) { |
| if (value instanceof String) { |
| ConversionResult conversionResult = vBinding.getConvertor().convertFromString( |
| (String)value, vBinding.getConvertorLocale(), null); |
| if (conversionResult.isSuccessful()) { |
| value = conversionResult.getResult(); |
| } else { |
| value = null; |
| } |
| } else { |
| if (getLogger().isWarnEnabled()) { |
| getLogger().warn("Convertor ignored on backend-value " + |
| "which isn't of type String."); |
| } |
| } |
| } |
| identity.add(value); |
| } |
| } |
| } |
| return identity; |
| } |
| |
| /** |
| * Get the identity of the given row. That's in fact a list of all the values |
| * of the fields in the form model that constitute the identity. |
| * @param row |
| * @return List the identity of the row |
| */ |
| protected List getIdentity(Repeater.RepeaterRow row) { |
| // quit if we don't have an identity binding |
| List identity = Collections.EMPTY_LIST; |
| if (this.identityBinding != null) { |
| JXPathBindingBase[] childBindings = this.identityBinding.getChildBindings(); |
| if (childBindings != null) { |
| int size = childBindings.length; |
| identity = new ArrayList(size); |
| for (int i = 0; i < size; i++) { |
| String fieldId = ((ValueJXPathBinding) childBindings[i]).getFieldId(); |
| Widget widget = row.lookupWidget(fieldId); |
| Object value = widget.getValue(); |
| identity.add(value); |
| } |
| } |
| } |
| return identity; |
| } |
| |
| public String toString() { |
| return "RepeaterJXPathBinding [widget=" + this.repeaterId + |
| ", xpath=" + this.repeaterPath + "]"; |
| } |
| } |