001/*
002 * (C) Copyright 2007 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Nuxeo - initial API and implementation
016 *
017 * $Id: EditableModelImpl.java 25559 2007-10-01 12:48:23Z atchertchian $
018 */
019
020package org.nuxeo.ecm.platform.ui.web.model.impl;
021
022import java.io.ByteArrayInputStream;
023import java.io.ByteArrayOutputStream;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.ObjectInputStream;
027import java.io.ObjectOutputStream;
028import java.io.Serializable;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.Collection;
032import java.util.Collections;
033import java.util.Comparator;
034import java.util.HashMap;
035import java.util.List;
036import java.util.Map;
037
038import javax.faces.model.DataModel;
039import javax.faces.model.DataModelEvent;
040import javax.faces.model.DataModelListener;
041
042import org.apache.commons.logging.Log;
043import org.apache.commons.logging.LogFactory;
044import org.nuxeo.ecm.core.api.ListDiff;
045import org.nuxeo.ecm.platform.ui.web.model.EditableModel;
046import org.nuxeo.ecm.platform.ui.web.util.DeepCopy;
047
048/**
049 * Editable data model that handles value changes.
050 * <p>
051 * Only accepts lists or arrays of Serializable objects for now.
052 *
053 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
054 */
055@SuppressWarnings({ "unchecked", "rawtypes" })
056public class EditableModelImpl extends DataModel implements EditableModel, Serializable {
057
058    private static final long serialVersionUID = 2550850486035521538L;
059
060    private static final Log log = LogFactory.getLog(EditableModelImpl.class);
061
062    // use this key to indicate unset values
063    private static final Object _NULL = new Object();
064
065    protected final Object originalData;
066
067    // current data list
068    protected List data;
069
070    // current row index (zero relative)
071    protected int index = -1;
072
073    // XXX AT: not thread safe (?)
074    protected Map<Integer, Integer> keyMap;
075
076    protected ListDiff listDiff;
077
078    protected Object template;
079
080    public EditableModelImpl(Object value, Object template) {
081        if (value != null) {
082            if (!(value instanceof List) && !(value instanceof Object[])) {
083                log.error("Cannot build editable model from " + value + ", list or array needed");
084                value = null;
085            }
086        }
087        originalData = value;
088        listDiff = new ListDiff();
089        keyMap = new HashMap<Integer, Integer>();
090        initializeData(value);
091        this.template = template;
092    }
093
094    protected void initializeData(Object originalData) {
095        List data = null;
096        if (originalData == null) {
097            data = new ArrayList<Object>();
098        } else if (originalData instanceof Object[]) {
099            data = new ArrayList<Object>();
100            for (Object item : (Object[]) originalData) {
101                data.add(DeepCopy.deepCopy(item));
102            }
103        } else if (originalData instanceof List) {
104            data = new ArrayList<Object>();
105            data.addAll((List) DeepCopy.deepCopy(originalData));
106        }
107        setWrappedData(data);
108    }
109
110    @Override
111    public Object getUnreferencedTemplate() {
112        if (template == null) {
113            return null;
114        }
115        if (template instanceof Serializable) {
116            try {
117                Serializable serializableTemplate = (Serializable) template;
118                ByteArrayOutputStream out = new ByteArrayOutputStream();
119                ObjectOutputStream oos = new ObjectOutputStream(out);
120                oos.writeObject(serializableTemplate);
121                oos.close();
122                // deserialize to make sure it is not the same instance
123                byte[] pickled = out.toByteArray();
124                InputStream in = new ByteArrayInputStream(pickled);
125                ObjectInputStream ois = new ObjectInputStream(in);
126                Object newTemplate = ois.readObject();
127                return newTemplate;
128            } catch (IOException | ClassNotFoundException e) {
129                throw new RuntimeException(e);
130            }
131        } else {
132            log.warn("Template is not serializable, cannot clone " + "to add unreferenced value into model.");
133            return template;
134        }
135    }
136
137    @Override
138    public Object getOriginalData() {
139        return originalData;
140    }
141
142    @Override
143    public Object getWrappedData() {
144        return data;
145    }
146
147    @Override
148    public void setWrappedData(Object data) {
149        if (data == null) {
150            this.data = null;
151            setRowIndex(-1);
152        } else {
153            this.data = (List) data;
154            index = -1;
155            setRowIndex(0);
156            for (int i = 0; i < this.data.size(); i++) {
157                keyMap.put(i, i);
158            }
159        }
160    }
161
162    // row data methods
163
164    /**
165     * Returns the initial data for the given key.
166     * <p>
167     * Returns null marker if key is invalid or data did not exist for given key in the original data.
168     */
169    protected Object getOriginalRowDataForKey(int key) {
170        if (originalData instanceof List) {
171            List list = (List) originalData;
172            if (key < 0 || key >= list.size()) {
173                return _NULL;
174            } else {
175                // if key exists in original data, then it's equal to the
176                // index.
177                return list.get(key);
178            }
179        } else if (originalData instanceof Object[]) {
180            Object[] array = (Object[]) originalData;
181            if (key < 0 || key >= array.length) {
182                return _NULL;
183            } else {
184                // if key exists in original data, then it's equal to the
185                // index.
186                return array[key];
187            }
188        } else {
189            return _NULL;
190        }
191    }
192
193    /**
194     * Returns a new row key that is not already used.
195     */
196    protected int getNewRowKey() {
197        Collection<Integer> keys = keyMap.values();
198        if (keys.isEmpty()) {
199            return 0;
200        } else {
201            List<Integer> lkeys = Arrays.asList(keys.toArray(new Integer[] {}));
202            Comparator<Integer> comp = Collections.reverseOrder();
203            Collections.sort(lkeys, comp);
204            Integer max = lkeys.get(0);
205            return max + 1;
206        }
207    }
208
209    @Override
210    public boolean isRowAvailable() {
211        if (data == null) {
212            return false;
213        }
214        return (index == -2) || (index >= 0 && index < data.size());
215    }
216
217    @Override
218    public boolean isRowModified() {
219        if (!isRowAvailable()) {
220            return false;
221        } else {
222            Integer rowKey = getRowKey();
223            if (rowKey == null) {
224                return false;
225            } else {
226                Object oldData = getOriginalRowDataForKey(rowKey);
227                if (oldData == _NULL) {
228                    return false;
229                }
230                Object newData = getRowData();
231                if (newData == null && oldData == null) {
232                    return false;
233                } else {
234                    if (newData != null) {
235                        return !newData.equals(oldData);
236                    } else {
237                        return !oldData.equals(newData);
238                    }
239                }
240            }
241        }
242    }
243
244    @Override
245    public boolean isRowNew() {
246        if (!isRowAvailable()) {
247            return false;
248        } else {
249            Integer rowKey = getRowKey();
250            if (rowKey == null) {
251                return false;
252            } else {
253                Object oldData = getOriginalRowDataForKey(rowKey);
254                return oldData == _NULL;
255            }
256        }
257    }
258
259    @Override
260    public void recordValueModified(int index, Object newValue) {
261        listDiff.modify(index, newValue);
262    }
263
264    @Override
265    public int getRowCount() {
266        if (data == null) {
267            return -1;
268        }
269        return data.size();
270    }
271
272    @Override
273    public Object getRowData() {
274        if (data == null) {
275            return null;
276        } else if (!isRowAvailable()) {
277            throw new IllegalArgumentException("No row available on " + this);
278        } else {
279            if (index == -2) {
280                // XXX return template instead (?)
281                return null;
282            }
283            return data.get(index);
284        }
285    }
286
287    @Override
288    public void setRowData(Object rowData) {
289        if (isRowAvailable()) {
290            data.set(index, rowData);
291        }
292    }
293
294    @Override
295    public int getRowIndex() {
296        return index;
297    }
298
299    @Override
300    public void setRowIndex(int rowIndex) {
301        if (rowIndex < -2) {
302            throw new IllegalArgumentException();
303        }
304        int old = index;
305        index = rowIndex;
306        if (data == null) {
307            return;
308        }
309        DataModelListener[] listeners = getDataModelListeners();
310        if (old != index && listeners != null) {
311            Object rowData = null;
312            if (isRowAvailable()) {
313                rowData = getRowData();
314            }
315            DataModelEvent event = new DataModelEvent(this, index, rowData);
316            int n = listeners.length;
317            for (int i = 0; i < n; i++) {
318                if (null != listeners[i]) {
319                    listeners[i].rowSelected(event);
320                }
321            }
322        }
323    }
324
325    @Override
326    public Integer getRowKey() {
327        if (index == -2) {
328            return index;
329        }
330        if (index < 0) {
331            return null;
332        }
333        return keyMap.get(index);
334    }
335
336    @Override
337    public void setRowKey(Integer key) {
338        // find index for that key
339        if (key != null) {
340            for (Integer i : keyMap.keySet()) {
341                Integer k = keyMap.get(i);
342                if (key.equals(k)) {
343                    setRowIndex(i);
344                    break;
345                }
346            }
347        } else {
348            setRowIndex(-1);
349        }
350    }
351
352    @Override
353    public ListDiff getListDiff() {
354        return listDiff;
355    }
356
357    @Override
358    public void setListDiff(ListDiff listDiff) {
359        this.listDiff = new ListDiff(listDiff);
360    }
361
362    @Override
363    public boolean isDirty() {
364        return listDiff != null && listDiff.isDirty();
365    }
366
367    @Override
368    public void addTemplateValue() {
369        addValue(getUnreferencedTemplate());
370    }
371
372    @Override
373    public boolean addValue(Object value) {
374        int position = data.size();
375        boolean res = data.add(value);
376        listDiff.add(value);
377        int newRowKey = getNewRowKey();
378        keyMap.put(position, newRowKey);
379        return res;
380    }
381
382    @Override
383    public void insertTemplateValue(int index) {
384        insertValue(index, getUnreferencedTemplate());
385    }
386
387    @Override
388    public void insertValue(int index, Object value) {
389        if (index > data.size()) {
390            // make sure enough rows are made available
391            for (int i = data.size(); i < index; i++) {
392                addTemplateValue();
393            }
394        }
395        data.add(index, value);
396        listDiff.insert(index, value);
397        // update key map to reflect new structure
398        Map<Integer, Integer> newKeyMap = new HashMap<Integer, Integer>();
399        for (Integer i : keyMap.keySet()) {
400            Integer key = keyMap.get(i);
401            if (i >= index) {
402                newKeyMap.put(i + 1, key);
403            } else {
404                newKeyMap.put(i, key);
405            }
406        }
407        keyMap = newKeyMap;
408        // insert new key
409        int newRowKey = getNewRowKey();
410        keyMap.put(index, newRowKey);
411    }
412
413    @Override
414    public Object moveValue(int fromIndex, int toIndex) {
415        Object old = data.remove(fromIndex);
416        data.add(toIndex, old);
417        listDiff.move(fromIndex, toIndex);
418        // update key map to reflect new structure
419        Map<Integer, Integer> newKeyMap = new HashMap<Integer, Integer>();
420        if (fromIndex < toIndex) {
421            for (Integer i : keyMap.keySet()) {
422                Integer key = keyMap.get(i);
423                if (i < fromIndex) {
424                    newKeyMap.put(i, key);
425                } else if (i > fromIndex && i <= toIndex) {
426                    newKeyMap.put(i - 1, key);
427                } else if (i > toIndex) {
428                    newKeyMap.put(i, key);
429                }
430            }
431        } else if (fromIndex > toIndex) {
432            for (Integer i : keyMap.keySet()) {
433                Integer key = keyMap.get(i);
434                if (i < toIndex) {
435                    newKeyMap.put(i, key);
436                } else if (i >= toIndex && i < fromIndex) {
437                    newKeyMap.put(i + 1, key);
438                } else if (i > fromIndex) {
439                    newKeyMap.put(i, key);
440                }
441            }
442        }
443        newKeyMap.put(toIndex, keyMap.get(fromIndex));
444        keyMap = newKeyMap;
445        return old;
446    }
447
448    @Override
449    public Object removeValue(int index) {
450        Object old = data.remove(index);
451        listDiff.remove(index);
452        // update key map to reflect new structure
453        Map<Integer, Integer> newKeyMap = new HashMap<Integer, Integer>();
454        for (Integer i : keyMap.keySet()) {
455            Integer key = keyMap.get(i);
456            if (i > index) {
457                newKeyMap.put(i - 1, key);
458            } else if (i < index) {
459                newKeyMap.put(i, key);
460            }
461        }
462        keyMap = newKeyMap;
463        return old;
464    }
465
466    @Override
467    public int size() {
468        if (data != null) {
469            return data.size();
470        }
471        return 0;
472    }
473
474    @Override
475    public String toString() {
476        final StringBuilder buf = new StringBuilder();
477        buf.append(EditableModelImpl.class.getSimpleName());
478        buf.append(" {");
479        buf.append("originalData: ");
480        buf.append(originalData);
481        buf.append(", data: ");
482        buf.append(data);
483        buf.append(", index: ");
484        buf.append(index);
485        buf.append(", keyMap: ");
486        buf.append(keyMap);
487        buf.append(", dirty: ");
488        buf.append(isDirty());
489        buf.append('}');
490        return buf.toString();
491    }
492
493}