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