001/*
002 * (C) Copyright 2007 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nuxeo - initial API and implementation
018 *
019 * $Id: EditableModelImpl.java 25559 2007-10-01 12:48:23Z atchertchian $
020 */
021
022package org.nuxeo.ecm.platform.ui.web.model.impl;
023
024import java.io.ByteArrayInputStream;
025import java.io.ByteArrayOutputStream;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.ObjectInputStream;
029import java.io.ObjectOutputStream;
030import java.io.Serializable;
031import java.util.ArrayList;
032import java.util.Arrays;
033import java.util.Collection;
034import java.util.Collections;
035import java.util.Comparator;
036import java.util.HashMap;
037import java.util.List;
038import java.util.Map;
039
040import javax.faces.model.DataModel;
041import javax.faces.model.DataModelEvent;
042import javax.faces.model.DataModelListener;
043
044import org.apache.commons.logging.Log;
045import org.apache.commons.logging.LogFactory;
046import org.nuxeo.ecm.core.api.ListDiff;
047import org.nuxeo.ecm.platform.ui.web.model.EditableModel;
048import org.nuxeo.ecm.platform.ui.web.util.DeepCopy;
049
050/**
051 * Editable data model that handles value changes.
052 * <p>
053 * Only accepts lists or arrays of Serializable objects for now.
054 *
055 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
056 */
057@SuppressWarnings({ "unchecked", "rawtypes" })
058public class EditableModelImpl extends DataModel implements EditableModel, Serializable {
059
060    private static final long serialVersionUID = 2550850486035521538L;
061
062    private static final Log log = LogFactory.getLog(EditableModelImpl.class);
063
064    // use this key to indicate unset values
065    private static final Object _NULL = new Object();
066
067    protected final Object originalData;
068
069    // current data list
070    protected List data;
071
072    // current row index (zero relative)
073    protected int index = -1;
074
075    // XXX AT: not thread safe (?)
076    protected Map<Integer, Integer> keyMap;
077
078    protected ListDiff listDiff;
079
080    protected Object template;
081
082    public EditableModelImpl(Object value, Object template) {
083        if (value != null) {
084            if (!(value instanceof List) && !(value instanceof Object[])) {
085                log.error("Cannot build editable model from " + value + ", list or array needed");
086                value = null;
087            }
088        }
089        originalData = value;
090        listDiff = new ListDiff();
091        keyMap = new HashMap<Integer, Integer>();
092        initializeData(value);
093        this.template = template;
094    }
095
096    protected void initializeData(Object originalData) {
097        List data = null;
098        if (originalData == null) {
099            data = new ArrayList<Object>();
100        } else if (originalData instanceof Object[]) {
101            data = new ArrayList<Object>();
102            for (Object item : (Object[]) originalData) {
103                data.add(DeepCopy.deepCopy(item));
104            }
105        } else if (originalData instanceof List) {
106            data = new ArrayList<Object>();
107            data.addAll((List) DeepCopy.deepCopy(originalData));
108        }
109        setWrappedData(data);
110    }
111
112    @Override
113    public Object getUnreferencedTemplate() {
114        if (template == null) {
115            return null;
116        }
117        if (template instanceof Serializable) {
118            try {
119                Serializable serializableTemplate = (Serializable) template;
120                ByteArrayOutputStream out = new ByteArrayOutputStream();
121                ObjectOutputStream oos = new ObjectOutputStream(out);
122                oos.writeObject(serializableTemplate);
123                oos.close();
124                // deserialize to make sure it is not the same instance
125                byte[] pickled = out.toByteArray();
126                InputStream in = new ByteArrayInputStream(pickled);
127                ObjectInputStream ois = new ObjectInputStream(in);
128                Object newTemplate = ois.readObject();
129                return newTemplate;
130            } catch (IOException | ClassNotFoundException e) {
131                throw new RuntimeException(e);
132            }
133        } else {
134            log.warn("Template is not serializable, cannot clone " + "to add unreferenced value into model.");
135            return template;
136        }
137    }
138
139    @Override
140    public Object getOriginalData() {
141        return originalData;
142    }
143
144    @Override
145    public Object getWrappedData() {
146        return data;
147    }
148
149    @Override
150    public void setWrappedData(Object data) {
151        index = -1;
152        if (data == null) {
153            this.data = null;
154        } else {
155            this.data = (List) data;
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}