/*
* (C) Copyright 2007 Nuxeo SAS (http://nuxeo.com/) and contributors.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public License
* (LGPL) version 2.1 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl.html
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* Contributors:
* Nuxeo - initial API and implementation
*
* $Id: EditableModelImpl.java 25559 2007-10-01 12:48:23Z atchertchian $
*/
package org.nuxeo.ecm.platform.ui.web.model.impl;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.faces.model.DataModel;
import javax.faces.model.DataModelEvent;
import javax.faces.model.DataModelListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.ListDiff;
import org.nuxeo.ecm.platform.ui.web.model.EditableModel;
import org.nuxeo.ecm.platform.ui.web.util.DeepCopy;
/**
* Editable data model that handles value changes.
* <p>
* Only accepts lists or arrays of Serializable objects for now.
*
* @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
*/
@SuppressWarnings("unchecked")
public class EditableModelImpl extends DataModel implements EditableModel,
Serializable {
private static final long serialVersionUID = 2550850486035521538L;
private static final Log log = LogFactory.getLog(EditableModelImpl.class);
// use this key to indicate unset values
private static final Object _NULL = new Object();
protected final Object originalData;
// current data list
protected List data;
// current row index (zero relative)
protected int index = -1;
// XXX AT: not thread safe (?)
protected Map<Integer, Integer> keyMap;
protected ListDiff listDiff;
public EditableModelImpl(Object value) {
if (value != null) {
if (!(value instanceof List) && !(value instanceof Object[])) {
log.error("Cannot build editable model from " + value
+ ", list or array needed");
value = null;
}
}
originalData = value;
listDiff = new ListDiff();
keyMap = new HashMap<Integer, Integer>();
initializeData(value);
}
protected void initializeData(Object originalData) {
List data = null;
if (originalData == null) {
data = new ArrayList<Object>();
} else if (originalData instanceof Object[]) {
data = new ArrayList<Object>();
for (Object item : (Object[]) originalData) {
data.add(DeepCopy.deepCopy(item));
}
} else if (originalData instanceof List) {
data = (List) DeepCopy.deepCopy(originalData);
}
setWrappedData(data);
}
public Object getOriginalData() {
return originalData;
}
@Override
public Object getWrappedData() {
return data;
}
@Override
public void setWrappedData(Object data) {
if (data == null) {
this.data = null;
setRowIndex(-1);
} else {
this.data = (List) data;
index = -1;
setRowIndex(0);
for (int i = 0; i < this.data.size(); i++) {
keyMap.put(i, i);
}
}
}
// row data methods
/**
* Returns the initial data for the given key.
* <p>
* Returns null marker if key is invalid or data did not exist for given key
* in the original data.
*/
protected Object getOriginalRowDataForKey(int key) {
if (originalData instanceof List) {
List list = (List) originalData;
if (key < 0 || key >= list.size()) {
return _NULL;
} else {
// if key exists in original data, then it's equal to the
// index.
return list.get(key);
}
} else if (originalData instanceof Object[]) {
Object[] array = (Object[]) originalData;
if (key < 0 || key >= array.length) {
return _NULL;
} else {
// if key exists in original data, then it's equal to the
// index.
return array[key];
}
} else {
return _NULL;
}
}
/**
* Returns a new row key that is not already used.
*/
protected int getNewRowKey() {
Collection<Integer> keys = keyMap.values();
if (keys.isEmpty()) {
return 0;
} else {
List<Integer> lkeys = Arrays.asList(keys.toArray(new Integer[]{}));
Comparator<Integer> comp = Collections.reverseOrder();
Collections.sort(lkeys, comp);
Integer max = lkeys.get(0);
return max + 1;
}
}
@Override
public boolean isRowAvailable() {
if (data == null) {
return false;
}
return index >= 0 && index < data.size();
}
public boolean isRowModified() {
if (!isRowAvailable()) {
return false;
} else {
Integer rowKey = getRowKey();
if (rowKey == null) {
return false;
} else {
Object oldData = getOriginalRowDataForKey(rowKey);
if (oldData == _NULL) {
return false;
}
Object newData = getRowData();
if (newData == null && oldData == null) {
return false;
} else {
if (newData != null) {
return !newData.equals(oldData);
} else {
return !oldData.equals(newData);
}
}
}
}
}
public boolean isRowNew() {
if (!isRowAvailable()) {
return false;
} else {
Integer rowKey = getRowKey();
if (rowKey == null) {
return false;
} else {
Object oldData = getOriginalRowDataForKey(rowKey);
return oldData == _NULL;
}
}
}
public void recordValueModified(int index, Object newValue) {
listDiff.modify(index, newValue);
}
@Override
public int getRowCount() {
if (data == null) {
return -1;
}
return data.size();
}
@Override
public Object getRowData() {
if (data == null) {
return null;
} else if (!isRowAvailable()) {
throw new IllegalArgumentException();
} else {
return data.get(index);
}
}
public void setRowData(Object rowData) {
if (isRowAvailable()) {
data.set(index, rowData);
}
}
@Override
public int getRowIndex() {
return index;
}
@Override
public void setRowIndex(int rowIndex) {
if (rowIndex < -1) {
throw new IllegalArgumentException();
}
int old = index;
index = rowIndex;
if (data == null) {
return;
}
DataModelListener[] listeners = getDataModelListeners();
if (old != index && listeners != null) {
Object rowData = null;
if (isRowAvailable()) {
rowData = getRowData();
}
DataModelEvent event = new DataModelEvent(this, index, rowData);
int n = listeners.length;
for (int i = 0; i < n; i++) {
if (null != listeners[i]) {
listeners[i].rowSelected(event);
}
}
}
}
public Integer getRowKey() {
return keyMap.get(index);
}
public void setRowKey(Integer key) {
// find index for that key
if (key != null) {
for (Integer i : keyMap.keySet()) {
Integer k = keyMap.get(i);
if (key.equals(k)) {
setRowIndex(i);
break;
}
}
} else {
setRowIndex(-1);
}
}
public ListDiff getListDiff() {
return listDiff;
}
public void setListDiff(ListDiff listDiff) {
this.listDiff = new ListDiff(listDiff);
}
public boolean isDirty() {
return listDiff != null && listDiff.isDirty();
}
public boolean addValue(Object value) {
int position = data.size();
boolean res = data.add(value);
listDiff.add(value);
int newRowKey = getNewRowKey();
keyMap.put(position, newRowKey);
return res;
}
public void insertValue(int index, Object value) {
data.add(index, value);
listDiff.insert(index, value);
// update key map to reflect new structure
Map<Integer, Integer> newKeyMap = new HashMap<Integer, Integer>();
for (Integer i : keyMap.keySet()) {
Integer key = keyMap.get(i);
if (i >= index) {
newKeyMap.put(i + 1, key);
} else {
newKeyMap.put(i, key);
}
}
keyMap = newKeyMap;
// insert new key
int newRowKey = getNewRowKey();
keyMap.put(index, newRowKey);
}
public Object moveValue(int fromIndex, int toIndex) {
Object old = data.remove(fromIndex);
data.add(toIndex, old);
listDiff.move(fromIndex, toIndex);
// update key map to reflect new structure
Map<Integer, Integer> newKeyMap = new HashMap<Integer, Integer>();
if (fromIndex < toIndex) {
for (Integer i : keyMap.keySet()) {
Integer key = keyMap.get(i);
if (i < fromIndex) {
newKeyMap.put(i, key);
} else if (i > fromIndex && i <= toIndex) {
newKeyMap.put(i - 1, key);
} else if (i > toIndex) {
newKeyMap.put(i, key);
}
}
} else if (fromIndex > toIndex) {
for (Integer i : keyMap.keySet()) {
Integer key = keyMap.get(i);
if (i < toIndex) {
newKeyMap.put(i, key);
} else if (i >= toIndex && i < fromIndex) {
newKeyMap.put(i + 1, key);
} else if (i > fromIndex) {
newKeyMap.put(i, key);
}
}
}
newKeyMap.put(toIndex, keyMap.get(fromIndex));
keyMap = newKeyMap;
return old;
}
public Object removeValue(int index) {
Object old = data.remove(index);
listDiff.remove(index);
// update key map to reflect new structure
Map<Integer, Integer> newKeyMap = new HashMap<Integer, Integer>();
for (Integer i : keyMap.keySet()) {
Integer key = keyMap.get(i);
if (i > index) {
newKeyMap.put(i - 1, key);
} else if (i < index) {
newKeyMap.put(i, key);
}
}
keyMap = newKeyMap;
return old;
}
public int size() {
if (data != null) {
return data.size();
}
return 0;
}
@Override
public String toString() {
final StringBuilder buf = new StringBuilder();
buf.append(EditableModelImpl.class.getSimpleName());
buf.append(" {");
buf.append("originalData: ");
buf.append(originalData);
buf.append(", data: ");
buf.append(data);
buf.append(", index: ");
buf.append(index);
buf.append(", keyMap: ");
buf.append(keyMap);
buf.append(", dirty: ");
buf.append(isDirty());
buf.append('}');
return buf.toString();
}
}