001/* 002 * (C) Copyright 2006-2020 Nuxeo (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 * Bogdan Stefanescu 018 * Florent Guillaume 019 */ 020package org.nuxeo.ecm.core.api.model.impl; 021 022import java.io.Serializable; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.Iterator; 028import java.util.List; 029import java.util.ListIterator; 030 031import org.nuxeo.common.collections.PrimitiveArrays; 032import org.nuxeo.ecm.core.api.ListDiff; 033import org.nuxeo.ecm.core.api.PropertyException; 034import org.nuxeo.ecm.core.api.model.InvalidPropertyValueException; 035import org.nuxeo.ecm.core.api.model.Property; 036import org.nuxeo.ecm.core.api.model.PropertyConversionException; 037import org.nuxeo.ecm.core.api.model.PropertyVisitor; 038import org.nuxeo.ecm.core.api.model.ReadOnlyPropertyException; 039import org.nuxeo.ecm.core.schema.types.Field; 040import org.nuxeo.ecm.core.schema.types.ListType; 041 042public class ListProperty extends AbstractProperty implements List<Property> { 043 044 private static final long serialVersionUID = 1L; 045 046 /** 047 * The corresponding field. 048 */ 049 protected final Field field; 050 051 protected final List<Property> children = new ArrayList<>(); 052 053 public ListProperty(Property parent, Field field) { 054 super(parent); 055 this.field = field; 056 } 057 058 public ListProperty(Property parent, Field field, int flags) { 059 super(parent, flags); 060 this.field = field; 061 } 062 063 @Override 064 public void internalSetValue(Serializable value) throws PropertyException { 065 // ListProperty uses a specific setValue implementation 066 } 067 068 /** 069 * TODO FIXME XXX uncommented <code>return true;</code> see NXP-1653. 070 * 071 * @see #getValue 072 * @see #accept 073 */ 074 @Override 075 public boolean isContainer() { 076 // return true; // - this can be uncommented when scalar list will be fixed 077 return !getType().isScalarList(); 078 } 079 080 @Override 081 public Property addValue(int index, Object value) throws PropertyException { 082 Field lfield = getType().getField(); 083 Property property = getRoot().createProperty(this, lfield, IS_NEW); 084 property.setValue(value); 085 children.add(index, property); 086 return property; 087 } 088 089 @Override 090 public Property addValue(Object value) throws PropertyException { 091 Field lfield = getType().getField(); 092 Property property = getRoot().createProperty(this, lfield, IS_NEW); 093 property.setValue(value); 094 children.add(property); 095 return property; 096 } 097 098 @Override 099 public Property addEmpty() { 100 Field lfield = getType().getField(); 101 Property property = getRoot().createProperty(this, lfield, 0); 102 children.add(property); 103 return property; 104 } 105 106 @Override 107 public Collection<Property> getChildren() { 108 return Collections.unmodifiableCollection(children); 109 } 110 111 @Override 112 public String getName() { 113 return field.getName().getPrefixedName(); 114 } 115 116 @Override 117 public ListType getType() { 118 return (ListType) field.getType(); 119 } 120 121 @Override 122 public Property get(String name) { 123 try { 124 return get(Integer.parseInt(name)); 125 } catch (NumberFormatException e) { 126 return null; 127 } 128 } 129 130 @Override 131 public Property get(int index) { 132 try { 133 return children.get(index); 134 } catch (IndexOutOfBoundsException e) { 135 return null; 136 } 137 } 138 139 @Override 140 public void set(String name, Property property) throws PropertyException { 141 try { 142 // don't implement set(int, Property) for now as we don't have usage 143 int index = Integer.parseInt(name); 144 children.set(index, property); 145 setIsModified(); 146 } catch (NumberFormatException e) { 147 throw new UnsupportedOperationException("Child name: " + name + " cannot be used on property: " + this); 148 } 149 } 150 151 @Override 152 protected Serializable getDefaultValue() { 153 Serializable value = (Serializable) field.getDefaultValue(); 154 if (value == null) { 155 value = new ArrayList<Serializable>(); 156 } 157 return value; 158 } 159 160 @Override 161 public Serializable internalGetValue() throws PropertyException { 162 if (children.isEmpty()) { 163 return new ArrayList<String>(); 164 } 165 var list = new ArrayList<>(children.size()); 166 for (Property property : children) { 167 list.add(property.getValue()); 168 } 169 // TODO XXX FIXME for compatibility - this treats scalar lists as array 170 // see NXP-1653. remove this when will be fixed 171 // see also isContainer(), setValue() and accept() 172 // if (getType().isScalarList()) { 173 // if (list.isEmpty()) return null; 174 // Object o = list.get(0); 175 // Class<?> type = o.getClass(); 176 // if (o == null) { 177 // // don't know the class of the element 178 // // try to use schema information 179 // type = JavaTypes.getPrimitiveClass(getType().getFieldType()); 180 // if (type == null) { // this should be a bug 181 // throw new IllegalStateException("Scalar list type is not known - this 182 // should be a bug"); 183 // } 184 // } else { 185 // type = o.getClass(); 186 // } 187 // return list.toArray((Object[])Array.newInstance(type, list.size())); 188 // } 189 // end of compatibility code <-------- 190 return list; 191 } 192 193 @Override 194 public Serializable getValueForWrite() throws PropertyException { 195 if (isPhantom() || isRemoved()) { 196 return getDefaultValue(); 197 } 198 if (children.isEmpty()) { 199 return new ArrayList<String>(); 200 } 201 var list = new ArrayList<>(children.size()); 202 for (Property property : children) { 203 list.add(property.getValueForWrite()); 204 } 205 return list; 206 } 207 208 @Override 209 @SuppressWarnings("unchecked") 210 public void init(Serializable value) throws PropertyException { 211 if (value == null) { // IGNORE null values - properties will be considered PHANTOMS 212 return; 213 } 214 List<Serializable> list; 215 if (value.getClass().isArray()) { // accept also arrays 216 list = (List<Serializable>) PrimitiveArrays.toList(value); 217 } else { 218 list = (List<Serializable>) value; 219 } 220 children.clear(); // do not use clear() method since it is marking the list as dirty 221 Field lfield = getType().getField(); 222 for (Serializable obj : list) { 223 Property property = getRoot().createProperty(this, lfield, 0); 224 property.init(obj); 225 children.add(property); 226 } 227 removePhantomFlag(); 228 } 229 230 @Override 231 public void setValue(Object value) throws PropertyException { 232 if (isReadOnly() || isSecuredForContext()) { 233 throw new ReadOnlyPropertyException( 234 String.format("Cannot set the value of property: %s since it is readonly", getXPath())); 235 } 236 if (value == null) { 237 List<Property> temp = new ArrayList<>(children); 238 for (Property p : temp) { // remove all children 239 p.remove(); // call #remove(Property) which set isModified flag 240 } 241 // handle SimpleDocumentModel case 242 if (isForceDirty()) { 243 setIsModified(); 244 } 245 return; 246 } 247 Collection<?> col; 248 Class<?> klass = value.getClass(); 249 if (klass == ListDiff.class) { // listdiff support for compatibility 250 applyListDiff((ListDiff) value); 251 return; 252 } else if (klass.isArray()) { // array support 253 col = arrayToList(value); 254 } else if (value instanceof Collection) { // collection support 255 col = (Collection<?>) value; 256 } else { 257 throw new InvalidPropertyValueException(getXPath()); 258 } 259 clear(); 260 Field lfield = getType().getField(); 261 for (Object obj : col) { 262 Property property = getRoot().createProperty(this, lfield, IS_NEW); 263 property.setValue(obj); 264 children.add(property); 265 } 266 } 267 268 @Override 269 public void clear() { 270 children.clear(); 271 setIsModified(); 272 } 273 274 @Override 275 public Field getField() { 276 return field; 277 } 278 279 public boolean remove(Property property) { 280 int index = children.indexOf(property); 281 if (index == -1) { 282 return false; // no such item 283 } 284 remove(index); 285 return true; 286 } 287 288 @Override 289 public Property remove(int index) { 290 Property property = children.remove(index); 291 setIsModified(); 292 // properties after index have been moved 293 for (int i = index; i < children.size(); i++) { 294 ((AbstractProperty) children.get(i)).setIsMoved(); 295 } 296 return property; 297 } 298 299 @Override 300 public ListProperty clone() throws CloneNotSupportedException { 301 return (ListProperty) super.clone(); 302 } 303 304 @Override 305 public void accept(PropertyVisitor visitor, Object arg) throws PropertyException { 306 arg = visitor.visit(this, arg); 307 if (arg != null && isContainer()) { 308 for (Property property : children) { 309 property.accept(visitor, arg); 310 } 311 } 312 } 313 314 /* ---------------------------- type conversion ------------------------ */ 315 316 @Override 317 public boolean isNormalized(Object value) { 318 return value == null || (value instanceof Collection && value instanceof Serializable); 319 } 320 321 @Override 322 public Serializable normalize(Object value) throws PropertyConversionException { 323 if (isNormalized(value)) { 324 return (Serializable) value; 325 } 326 if (value.getClass().isArray()) { 327 return arrayToList(value); 328 } 329 throw new PropertyConversionException(value.getClass(), List.class); 330 } 331 332 @SuppressWarnings("unchecked") 333 @Override 334 public <T> T convertTo(Serializable value, Class<T> toType) throws PropertyConversionException { 335 if (value == null) { 336 return null; 337 } else if (toType.isAssignableFrom(value.getClass())) { 338 return toType.cast(value); 339 } 340 if (toType.isArray()) { 341 return (T) ((Collection<?>) value).toArray(); 342 } else if (toType == List.class || toType == Collection.class) { 343 // TODO we need this for compatibility with scalar lists 344 if (value.getClass().isArray()) { 345 if (value.getClass().isPrimitive()) { 346 return (T) PrimitiveArrays.toList(value); 347 } else { 348 return (T) Arrays.asList((Object[]) value); 349 } 350 } 351 } 352 return super.convertTo(value, toType); 353 } 354 355 // Must return ArrayList 356 public static ArrayList<?> arrayToList(Object obj) { 357 Object[] ar = PrimitiveArrays.toObjectArray(obj); 358 ArrayList<Object> list = new ArrayList<>(ar.length); 359 list.addAll(Arrays.asList(ar)); 360 return list; 361 } 362 363 /** 364 * Supports ListDiff for compatibility. 365 */ 366 public void applyListDiff(ListDiff ld) throws PropertyException { 367 for (ListDiff.Entry entry : ld.diff()) { 368 switch (entry.type) { 369 case ListDiff.ADD: 370 addValue(entry.value); 371 break; 372 case ListDiff.INSERT: 373 addValue(entry.index, entry.value); 374 break; 375 case ListDiff.REMOVE: 376 remove(entry.index); 377 break; 378 case ListDiff.CLEAR: 379 clear(); 380 break; 381 case ListDiff.MODIFY: 382 get(entry.index).setValue(entry.value); 383 break; 384 case ListDiff.MOVE: 385 int toIndex = (Integer) entry.value; 386 int fromIndex = entry.index; 387 Property src = children.get(fromIndex); 388 src.moveTo(toIndex); 389 break; 390 } 391 } 392 } 393 394 @Override 395 public boolean isSameAs(Property property) throws PropertyException { 396 if (!(property instanceof ListProperty)) { 397 return false; 398 } 399 ListProperty lp = (ListProperty) property; 400 List<Property> c1 = children; 401 List<Property> c2 = lp.children; 402 if (c1.size() != c2.size()) { 403 return false; 404 } 405 for (int i = 0, size = c1.size(); i < size; i++) { 406 Property p1 = c1.get(i); 407 Property p2 = c2.get(i); 408 if (!p1.isSameAs(p2)) { 409 return false; 410 } 411 } 412 return true; 413 } 414 415 @Override 416 public Iterator<Property> getDirtyChildren() { 417 if (!isContainer()) { 418 throw new UnsupportedOperationException("Cannot iterate over children of scalar properties"); 419 } 420 return new DirtyPropertyIterator(children.iterator()); 421 } 422 423 public int indexOf(Property property) { 424 for (int i = 0, size = children.size(); i < size; i++) { 425 Property p = children.get(i); 426 if (p == property) { 427 return i; 428 } 429 } 430 return -1; 431 } 432 433 boolean moveTo(Property property, int index) { 434 if (index < 0 || index > children.size()) { 435 throw new IndexOutOfBoundsException("Index out of bounds: " + index + ". Bounds are: 0 - " 436 + children.size()); 437 } 438 int i = indexOf(property); 439 if (i == -1) { 440 throw new UnsupportedOperationException("You are trying to move a property that is not part of a list"); 441 } 442 if (i == index) { 443 return false; 444 } 445 if (i < index) { 446 children.add(index + 1, property); 447 children.remove(i); 448 } else { 449 children.add(index, property); 450 children.remove(i + 1); 451 } 452 // new property must be dirty 453 for (int j = Math.min(index, i); j < children.size(); j++) { 454 ((AbstractProperty) children.get(j)).setIsModified(); 455 } 456 return true; 457 } 458 459 /** 460 * Throws UnsupportedOperationException, added to implement List<Property> interface. 461 */ 462 @Override 463 public void add(int index, Property element) { 464 throw new UnsupportedOperationException(); 465 } 466 467 /** 468 * Throws UnsupportedOperationException, added to implement List<Property> interface. 469 */ 470 @Override 471 public boolean add(Property o) { 472 throw new UnsupportedOperationException(); 473 } 474 475 /** 476 * Throws UnsupportedOperationException, added to implement List<Property> interface. 477 */ 478 @Override 479 public boolean addAll(Collection<? extends Property> c) { 480 throw new UnsupportedOperationException(); 481 } 482 483 /** 484 * Throws UnsupportedOperationException, added to implement List<Property> interface. 485 */ 486 @Override 487 public boolean addAll(int index, Collection<? extends Property> c) { 488 throw new UnsupportedOperationException(); 489 } 490 491 /** 492 * Throws UnsupportedOperationException, added to implement List<Property> interface. 493 */ 494 @Override 495 public boolean contains(Object o) { 496 throw new UnsupportedOperationException(); 497 } 498 499 /** 500 * Throws UnsupportedOperationException, added to implement List<Property> interface. 501 */ 502 @Override 503 public boolean containsAll(Collection<?> c) { 504 throw new UnsupportedOperationException(); 505 } 506 507 /** 508 * Throws UnsupportedOperationException, added to implement List<Property> interface. 509 */ 510 @Override 511 public int indexOf(Object o) { 512 throw new UnsupportedOperationException(); 513 } 514 515 @Override 516 public boolean isEmpty() { 517 return children.isEmpty(); 518 } 519 520 /** 521 * Throws UnsupportedOperationException, added to implement List<Property> interface. 522 */ 523 @Override 524 public int lastIndexOf(Object o) { 525 throw new UnsupportedOperationException(); 526 } 527 528 /** 529 * Throws UnsupportedOperationException, added to implement List<Property> interface. 530 */ 531 @Override 532 public ListIterator<Property> listIterator() { 533 return children.listIterator(); 534 } 535 536 /** 537 * Throws UnsupportedOperationException, added to implement List<Property> interface. 538 */ 539 @Override 540 public ListIterator<Property> listIterator(int index) { 541 return children.listIterator(index); 542 } 543 544 /** 545 * Throws UnsupportedOperationException, added to implement List<Property> interface. 546 */ 547 @Override 548 public boolean remove(Object o) { 549 throw new UnsupportedOperationException(); 550 } 551 552 /** 553 * Throws UnsupportedOperationException, added to implement List<Property> interface. 554 */ 555 @Override 556 public boolean removeAll(Collection<?> c) { 557 throw new UnsupportedOperationException(); 558 } 559 560 /** 561 * Throws UnsupportedOperationException, added to implement List<Property> interface. 562 */ 563 @Override 564 public boolean retainAll(Collection<?> c) { 565 throw new UnsupportedOperationException(); 566 } 567 568 /** 569 * Throws UnsupportedOperationException, added to implement List<Property> interface. 570 */ 571 @Override 572 public Property set(int index, Property element) { 573 throw new UnsupportedOperationException(); 574 } 575 576 /** 577 * Throws UnsupportedOperationException, added to implement List<Property> interface. 578 */ 579 @Override 580 public List<Property> subList(int fromIndex, int toIndex) { 581 throw new UnsupportedOperationException(); 582 } 583 584 @Override 585 public Object[] toArray() { 586 return children.toArray(); 587 } 588 589 @Override 590 public <T> T[] toArray(T[] a) { 591 return children.toArray(a); 592 } 593 594 @Override 595 public void clearDirtyFlags() { 596 // even makes child properties not dirty 597 super.clearDirtyFlags(); 598 for (Property child : children) { 599 child.clearDirtyFlags(); 600 } 601 } 602 603}