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:[email protected]">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}