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 index = -1; 150 if (data == null) { 151 this.data = null; 152 } else { 153 this.data = (List) data; 154 for (int i = 0; i < this.data.size(); i++) { 155 keyMap.put(i, i); 156 } 157 } 158 } 159 160 // row data methods 161 162 /** 163 * Returns the initial data for the given key. 164 * <p> 165 * Returns null marker if key is invalid or data did not exist for given key in the original data. 166 */ 167 protected Object getOriginalRowDataForKey(int key) { 168 if (originalData instanceof List) { 169 List list = (List) originalData; 170 if (key < 0 || key >= list.size()) { 171 return _NULL; 172 } else { 173 // if key exists in original data, then it's equal to the 174 // index. 175 return list.get(key); 176 } 177 } else if (originalData instanceof Object[]) { 178 Object[] array = (Object[]) originalData; 179 if (key < 0 || key >= array.length) { 180 return _NULL; 181 } else { 182 // if key exists in original data, then it's equal to the 183 // index. 184 return array[key]; 185 } 186 } else { 187 return _NULL; 188 } 189 } 190 191 /** 192 * Returns a new row key that is not already used. 193 */ 194 protected int getNewRowKey() { 195 Collection<Integer> keys = keyMap.values(); 196 if (keys.isEmpty()) { 197 return 0; 198 } else { 199 List<Integer> lkeys = Arrays.asList(keys.toArray(new Integer[] {})); 200 Comparator<Integer> comp = Collections.reverseOrder(); 201 Collections.sort(lkeys, comp); 202 Integer max = lkeys.get(0); 203 return max + 1; 204 } 205 } 206 207 @Override 208 public boolean isRowAvailable() { 209 if (data == null) { 210 return false; 211 } 212 return (index == -2) || (index >= 0 && index < data.size()); 213 } 214 215 @Override 216 public boolean isRowModified() { 217 if (!isRowAvailable()) { 218 return false; 219 } else { 220 Integer rowKey = getRowKey(); 221 if (rowKey == null) { 222 return false; 223 } else { 224 Object oldData = getOriginalRowDataForKey(rowKey); 225 if (oldData == _NULL) { 226 return false; 227 } 228 Object newData = getRowData(); 229 if (newData == null && oldData == null) { 230 return false; 231 } else { 232 if (newData != null) { 233 return !newData.equals(oldData); 234 } else { 235 return !oldData.equals(newData); 236 } 237 } 238 } 239 } 240 } 241 242 @Override 243 public boolean isRowNew() { 244 if (!isRowAvailable()) { 245 return false; 246 } else { 247 Integer rowKey = getRowKey(); 248 if (rowKey == null) { 249 return false; 250 } else { 251 Object oldData = getOriginalRowDataForKey(rowKey); 252 return oldData == _NULL; 253 } 254 } 255 } 256 257 @Override 258 public void recordValueModified(int index, Object newValue) { 259 listDiff.modify(index, newValue); 260 } 261 262 @Override 263 public int getRowCount() { 264 if (data == null) { 265 return -1; 266 } 267 return data.size(); 268 } 269 270 @Override 271 public Object getRowData() { 272 if (data == null) { 273 return null; 274 } else if (!isRowAvailable()) { 275 throw new IllegalArgumentException("No row available on " + this); 276 } else { 277 if (index == -2) { 278 // XXX return template instead (?) 279 return null; 280 } 281 return data.get(index); 282 } 283 } 284 285 @Override 286 public void setRowData(Object rowData) { 287 if (isRowAvailable()) { 288 data.set(index, rowData); 289 } 290 } 291 292 @Override 293 public int getRowIndex() { 294 return index; 295 } 296 297 @Override 298 public void setRowIndex(int rowIndex) { 299 if (rowIndex < -2) { 300 throw new IllegalArgumentException(); 301 } 302 int old = index; 303 index = rowIndex; 304 if (data == null) { 305 return; 306 } 307 DataModelListener[] listeners = getDataModelListeners(); 308 if (old != index && listeners != null) { 309 Object rowData = null; 310 if (isRowAvailable()) { 311 rowData = getRowData(); 312 } 313 DataModelEvent event = new DataModelEvent(this, index, rowData); 314 int n = listeners.length; 315 for (int i = 0; i < n; i++) { 316 if (null != listeners[i]) { 317 listeners[i].rowSelected(event); 318 } 319 } 320 } 321 } 322 323 @Override 324 public Integer getRowKey() { 325 if (index == -2) { 326 return index; 327 } 328 if (index < 0) { 329 return null; 330 } 331 return keyMap.get(index); 332 } 333 334 @Override 335 public void setRowKey(Integer key) { 336 // find index for that key 337 if (key != null) { 338 for (Integer i : keyMap.keySet()) { 339 Integer k = keyMap.get(i); 340 if (key.equals(k)) { 341 setRowIndex(i); 342 break; 343 } 344 } 345 } else { 346 setRowIndex(-1); 347 } 348 } 349 350 @Override 351 public ListDiff getListDiff() { 352 return listDiff; 353 } 354 355 @Override 356 public void setListDiff(ListDiff listDiff) { 357 this.listDiff = new ListDiff(listDiff); 358 } 359 360 @Override 361 public boolean isDirty() { 362 return listDiff != null && listDiff.isDirty(); 363 } 364 365 @Override 366 public void addTemplateValue() { 367 addValue(getUnreferencedTemplate()); 368 } 369 370 @Override 371 public boolean addValue(Object value) { 372 int position = data.size(); 373 boolean res = data.add(value); 374 listDiff.add(value); 375 int newRowKey = getNewRowKey(); 376 keyMap.put(position, newRowKey); 377 return res; 378 } 379 380 @Override 381 public void insertTemplateValue(int index) { 382 insertValue(index, getUnreferencedTemplate()); 383 } 384 385 @Override 386 public void insertValue(int index, Object value) { 387 if (index > data.size()) { 388 // make sure enough rows are made available 389 for (int i = data.size(); i < index; i++) { 390 addTemplateValue(); 391 } 392 } 393 data.add(index, value); 394 listDiff.insert(index, value); 395 // update key map to reflect new structure 396 Map<Integer, Integer> newKeyMap = new HashMap<Integer, Integer>(); 397 for (Integer i : keyMap.keySet()) { 398 Integer key = keyMap.get(i); 399 if (i >= index) { 400 newKeyMap.put(i + 1, key); 401 } else { 402 newKeyMap.put(i, key); 403 } 404 } 405 keyMap = newKeyMap; 406 // insert new key 407 int newRowKey = getNewRowKey(); 408 keyMap.put(index, newRowKey); 409 } 410 411 @Override 412 public Object moveValue(int fromIndex, int toIndex) { 413 Object old = data.remove(fromIndex); 414 data.add(toIndex, old); 415 listDiff.move(fromIndex, toIndex); 416 // update key map to reflect new structure 417 Map<Integer, Integer> newKeyMap = new HashMap<Integer, Integer>(); 418 if (fromIndex < toIndex) { 419 for (Integer i : keyMap.keySet()) { 420 Integer key = keyMap.get(i); 421 if (i < fromIndex) { 422 newKeyMap.put(i, key); 423 } else if (i > fromIndex && i <= toIndex) { 424 newKeyMap.put(i - 1, key); 425 } else if (i > toIndex) { 426 newKeyMap.put(i, key); 427 } 428 } 429 } else if (fromIndex > toIndex) { 430 for (Integer i : keyMap.keySet()) { 431 Integer key = keyMap.get(i); 432 if (i < toIndex) { 433 newKeyMap.put(i, key); 434 } else if (i >= toIndex && i < fromIndex) { 435 newKeyMap.put(i + 1, key); 436 } else if (i > fromIndex) { 437 newKeyMap.put(i, key); 438 } 439 } 440 } 441 newKeyMap.put(toIndex, keyMap.get(fromIndex)); 442 keyMap = newKeyMap; 443 return old; 444 } 445 446 @Override 447 public Object removeValue(int index) { 448 Object old = data.remove(index); 449 listDiff.remove(index); 450 // update key map to reflect new structure 451 Map<Integer, Integer> newKeyMap = new HashMap<Integer, Integer>(); 452 for (Integer i : keyMap.keySet()) { 453 Integer key = keyMap.get(i); 454 if (i > index) { 455 newKeyMap.put(i - 1, key); 456 } else if (i < index) { 457 newKeyMap.put(i, key); 458 } 459 } 460 keyMap = newKeyMap; 461 return old; 462 } 463 464 @Override 465 public int size() { 466 if (data != null) { 467 return data.size(); 468 } 469 return 0; 470 } 471 472 @Override 473 public String toString() { 474 final StringBuilder buf = new StringBuilder(); 475 buf.append(EditableModelImpl.class.getSimpleName()); 476 buf.append(" {"); 477 buf.append("originalData: "); 478 buf.append(originalData); 479 buf.append(", data: "); 480 buf.append(data); 481 buf.append(", index: "); 482 buf.append(index); 483 buf.append(", keyMap: "); 484 buf.append(keyMap); 485 buf.append(", dirty: "); 486 buf.append(isDirty()); 487 buf.append('}'); 488 return buf.toString(); 489 } 490 491}