blob: 603d3942a9ff78d6ddb2e06652702f4a71cb325e [file] [log] [blame]
/*
* 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.woody.binding;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.apache.avalon.framework.logger.Logger;
import org.apache.cocoon.woody.datatype.convertor.Convertor;
import org.apache.cocoon.woody.formmodel.Widget;
import org.apache.cocoon.woody.formmodel.Repeater;
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 CVS $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 List uniqueRowBinding;
/**
* Constructs RepeaterJXPathBinding
*/
public RepeaterJXPathBinding(
JXPathBindingBuilderBase.CommonAttributes commonAtts,
String repeaterId, String repeaterPath, String rowPath,
String rowPathForInsert, String uniqueRowId,
String uniqueRowPath, JXPathBindingBase[] childBindings,
JXPathBindingBase insertBinding,
JXPathBindingBase[] deleteBindings, JXPathBindingBase[] uniqueBindings) {
this(commonAtts, repeaterId, repeaterPath, rowPath, rowPathForInsert,
uniqueRowId, uniqueRowPath, null, null, childBindings,
insertBinding, deleteBindings, uniqueBindings);
}
/**
* Constructs RepeaterJXPathBinding
*/
public RepeaterJXPathBinding(
JXPathBindingBuilderBase.CommonAttributes commonAtts,
String repeaterId, String repeaterPath, String rowPath,
String rowPathForInsert, String uniqueRowId,
String uniqueRowPath, Convertor convertor, Locale convertorLocale,
JXPathBindingBase[] childBindings, JXPathBindingBase insertBinding,
JXPathBindingBase[] deleteBindings, JXPathBindingBase[] uniqueBindings) {
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;
}
// New unique key management
uniqueRowBinding = new ArrayList();
// Create a UniqueFieldJXPathBining for the unique define in old-style
if (uniqueRowId != null && uniqueRowPath != null) {
uniqueRowBinding.add(new UniqueFieldJXPathBinding(
JXPathBindingBuilderBase.CommonAttributes.DEFAULT,
uniqueRowId, uniqueRowPath, convertor, convertorLocale));
}
if (uniqueBindings != null) {
for (int i=0; i < uniqueBindings.length; i++) {
uniqueRowBinding.add(uniqueBindings[i]);
}
}
}
/**
* 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) frmModel.getWidget(this.repeaterId);
repeater.removeRows();
int initialSize = repeater.getSize();
// build a jxpath iterator for pointers
JXPathContext repeaterContext =
jxpc.getRelativeContext(jxpc.getPointer(this.repeaterPath));
Iterator rowPointers = repeaterContext.iteratePointers(this.rowPath);
//iterate through it
while (rowPointers.hasNext()) {
// create a new row, take that as the frmModelSubContext
Repeater.RepeaterRow thisRow;
if (initialSize > 0) {
thisRow = repeater.getRow(--initialSize);
} 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
Iterator iter = this.uniqueRowBinding.iterator();
while (iter.hasNext()) {
((UniqueFieldJXPathBinding)iter.next()).loadFormFromModel(thisRow, rowContext);
}
this.rowBinding.loadFormFromModel(thisRow, rowContext);
}
if (getLogger().isDebugEnabled())
getLogger().debug("done loading rows " + toString());
}
/**
* Uses the mapped unique-id of each row to detect if rows have been
* updated, inserted or removed. Depending on what happened the appropriate
* child-bindings are alowed to visit the narrowed contexts.
*/
public void doSave(Widget frmModel, JXPathContext jxpc)
throws BindingException {
// Find the repeater
Repeater repeater = (Repeater) frmModel.getWidget(this.repeaterId);
// and his context
JXPathContext repeaterContext =
jxpc.getRelativeContext(jxpc.getPointer(this.repeaterPath));
// create set of updatedRowIds
Set updatedRowIds = 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 key values
List rowIdValues = getUniqueRowValues(thisRow);
if (isAnyListElementNotNull(rowIdValues)) {
// 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 matchIds = getMatchIds(rowContext);
if (ListUtils.isEqualList(rowIdValues, matchIds)) {
// match! --> bind to children
this.rowBinding.saveFormToModel(thisRow, rowContext);
// --> store rowIdValue in list of updatedRowIds
updatedRowIds.add(rowIdValues);
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
updatedRowIds.add(rowIdValues);
}
} else {
// if all rowIdValues == null --> 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 matchIds = getMatchIds(rowContext);
// check if matchPath was in list of updates, if not --> bind for delete
if (!isListInSet(updatedRowIds, matchIds)) {
rowsToDelete.add(rowContext);
}
}
if (rowsToDelete.size() > 0) {
if (this.deleteRowBinding != null) {
// 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--) {
this.deleteRowBinding.saveFormToModel(frmModel,
rowsToDelete.get(i));
}
} else {
if (getLogger().isWarnEnabled()) {
getLogger().warn(
"RepeaterBinding has detected rows to delete, " +
"but misses the <on-delete-row> binding to do it."
);
}
}
}
// 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) {
if (this.insertRowBinding != null) {
Iterator rowIterator = rowsToInsert.iterator();
//register the factory!
while (rowIterator.hasNext()) {
Repeater.RepeaterRow thisRow = (Repeater.RepeaterRow)rowIterator.next();
// Perform the insert row binding.
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 " + toString());
}
}
/**
* Tests if a List is already contained in a Set of Lists.
* @param set the Set of Lists.
* @param list the list that is tested if it is already in the Set.
* @return true if the Set contains the List, false otherwise.
*/
private boolean isListInSet(Set set, List list) {
Iterator iter = set.iterator();
while (iter.hasNext()) {
List listFromSet = (List)iter.next();
if (ListUtils.isEqualList(listFromSet, list)) {
return true;
}
}
return false;
}
/**
* Tests if any of the elements in a List is not null.
* @param list
* @return
*/
private boolean isAnyListElementNotNull(List list) {
Iterator iter = list.iterator();
while (iter.hasNext()) {
if (iter.next() != null) {
return true;
}
}
return false;
}
/**
*
* @param rowContext
* @return
*/
private List getMatchIds(JXPathContext rowContext) {
List matchIds = new ArrayList();
Iterator iter = this.uniqueRowBinding.iterator();
while (iter.hasNext()) {
UniqueFieldJXPathBinding key = (UniqueFieldJXPathBinding)iter.next();
Object matchId = rowContext.getValue(key.getXpath());
if (matchId != null && key.getConvertor() != null) {
if (matchId instanceof String) {
matchId = key.getConvertor().convertFromString(
(String)matchId, key.getConvertorLocale(), null);
} else {
if (getLogger().isWarnEnabled()) {
getLogger().warn("Convertor ignored on backend-value " +
"which isn't of type String.");
}
}
}
matchIds.add(matchId);
}
return matchIds;
}
/**
* Get the values of the unique-fields of the given row in the formModel
* @param thisRow
* @return List
*/
private List getUniqueRowValues(Repeater.RepeaterRow thisRow) {
List values = new ArrayList();
Iterator iter = this.uniqueRowBinding.iterator();
while (iter.hasNext()) {
UniqueFieldJXPathBinding key = (UniqueFieldJXPathBinding)iter.next();
Widget rowIdWidget = thisRow.getWidget(key.getFieldId());
Object rowIdValue = rowIdWidget.getValue();
values.add(rowIdValue);
}
return values;
}
public String toString() {
return "RepeaterJXPathBinding [widget=" + this.repeaterId +
", xpath=" + this.repeaterPath + "]";
}
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);
Iterator iter = this.uniqueRowBinding.iterator();
while (iter.hasNext()) {
((UniqueFieldJXPathBinding)iter.next()).enableLogging(logger);
}
}
}