001/* 002 * (C) Copyright 2006-2019 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.Iterator; 024import java.util.Optional; 025import java.util.regex.Matcher; 026import java.util.regex.Pattern; 027 028import org.apache.commons.logging.Log; 029import org.apache.commons.logging.LogFactory; 030import org.nuxeo.common.utils.Path; 031import org.nuxeo.ecm.core.api.NuxeoException; 032import org.nuxeo.ecm.core.api.NuxeoPrincipal; 033import org.nuxeo.ecm.core.api.PropertyException; 034import org.nuxeo.ecm.core.api.model.DocumentPart; 035import org.nuxeo.ecm.core.api.model.Property; 036import org.nuxeo.ecm.core.api.model.PropertyConversionException; 037import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 038import org.nuxeo.ecm.core.api.model.ReadOnlyPropertyException; 039import org.nuxeo.ecm.core.api.model.resolver.PropertyObjectResolver; 040import org.nuxeo.ecm.core.api.model.resolver.PropertyObjectResolverImpl; 041import org.nuxeo.ecm.core.schema.PropertyCharacteristicHandler; 042import org.nuxeo.ecm.core.schema.types.Schema; 043import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver; 044import org.nuxeo.runtime.api.Framework; 045 046public abstract class AbstractProperty implements Property { 047 048 private static final long serialVersionUID = 1L; 049 050 private static final Log log = LogFactory.getLog(AbstractProperty.class); 051 052 protected static final Pattern NON_CANON_INDEX = Pattern.compile("[^/\\[\\]]+" // name 053 + "\\[(-?\\d+)\\]" // index in brackets - could be -1 if element is new to list 054 ); 055 056 /** 057 * Whether or not this property is read only. 058 */ 059 public static final int IS_READONLY = 32; 060 061 /** 062 * Whether or not this property is secured from edition. 063 * 064 * @since 11.1 065 */ 066 public static final int IS_SECURED = 64; 067 068 /** 069 * Whether or not this property is deprecated. 070 * 071 * @since 11.1 072 */ 073 public static final int IS_DEPRECATED = 128; 074 075 public final Property parent; 076 077 /** 078 * for SimpleDocumentModel uses 079 */ 080 public boolean forceDirty = false; 081 082 protected int flags; 083 084 protected AbstractProperty(Property parent) { 085 this.parent = parent; 086 } 087 088 protected AbstractProperty(Property parent, int flags) { 089 this.parent = parent; 090 this.flags = flags; 091 } 092 093 /** 094 * Sets the given normalized value. 095 * <p> 096 * This applies only for nodes that physically store a value (that means non container nodes). Container nodes does 097 * nothing. 098 */ 099 public abstract void internalSetValue(Serializable value) throws PropertyException; 100 101 public abstract Serializable internalGetValue() throws PropertyException; 102 103 @Override 104 public void init(Serializable value) throws PropertyException { 105 if (value == null || (value instanceof Object[] && ((Object[]) value).length == 0)) { 106 // ignore null or empty values, properties will be considered phantoms 107 return; 108 } 109 internalSetValue(value); 110 removePhantomFlag(); 111 } 112 113 public void removePhantomFlag() { 114 flags &= ~IS_PHANTOM; 115 if (parent != null) { 116 ((AbstractProperty) parent).removePhantomFlag(); 117 } 118 } 119 120 @Override 121 public void setValue(int index, Object value) throws PropertyException { 122 Property property = get(index); 123 property.setValue(value); 124 } 125 126 @Override 127 public int size() { 128 return getChildren().size(); 129 } 130 131 @Override 132 public Iterator<Property> iterator() { 133 return getChildren().iterator(); 134 } 135 136 @Override 137 public Serializable remove() throws PropertyException { 138 Serializable value = getValue(); 139 if (parent != null && parent.isList()) { // remove from list is 140 // handled separately 141 ListProperty list = (ListProperty) parent; 142 list.remove(this); 143 } else if (!isPhantom()) { // remove from map is easier -> mark the 144 // field as removed and remove the value 145 // do not remove the field if the previous value was null, except if its a property from a 146 // SimpleDocumentModel (forceDirty mode) 147 Serializable previous = internalGetValue(); 148 init(null); 149 if (previous != null || isForceDirty()) { 150 setIsRemoved(); 151 } 152 } 153 return value; 154 } 155 156 @Override 157 public Property getParent() { 158 return parent; 159 } 160 161 @Override 162 public String getXPath() { 163 StringBuilder sb = new StringBuilder(); 164 getXPath(sb); 165 return sb.toString(); 166 } 167 168 protected void getXPath(StringBuilder sb) { 169 if (parent != null) { 170 ((AbstractProperty) parent).getXPath(sb); 171 if (parent.isList()) { 172 sb.append('/'); 173 int i = ((ListProperty) parent).children.indexOf(this); 174 sb.append(i); 175 } else { 176 if (sb.length() != 0) { 177 sb.append('/'); 178 } 179 sb.append(getName()); 180 } 181 } 182 } 183 184 @Override 185 @Deprecated 186 public String getPath() { 187 Path path = collectPath(new Path("/")); 188 return path.toString(); 189 } 190 191 protected Path collectPath(Path path) { 192 String name = getName(); 193 if (parent != null) { 194 if (parent.isList()) { 195 int i = ((ListProperty) parent).children.indexOf(this); 196 name = name + '[' + i + ']'; 197 } 198 path = ((AbstractProperty) parent).collectPath(path); 199 } 200 return path.append(name); 201 } 202 203 @Override 204 public Schema getSchema() { 205 return getRoot().getSchema(); 206 } 207 208 @Override 209 public boolean isList() { 210 return getType().isListType(); 211 } 212 213 @Override 214 public boolean isComplex() { 215 return getType().isComplexType(); 216 } 217 218 @Override 219 public boolean isScalar() { 220 return getType().isSimpleType(); 221 } 222 223 @Override 224 public boolean isNew() { 225 return areFlagsSet(IS_NEW); 226 } 227 228 @Override 229 public boolean isRemoved() { 230 return areFlagsSet(IS_REMOVED); 231 } 232 233 @Override 234 public boolean isMoved() { 235 return areFlagsSet(IS_MOVED); 236 } 237 238 @Override 239 public boolean isModified() { 240 return areFlagsSet(IS_MODIFIED); 241 } 242 243 @Override 244 public boolean isPhantom() { 245 return areFlagsSet(IS_PHANTOM); 246 } 247 248 @Override 249 public final boolean isDirty() { 250 return (flags & IS_DIRTY) != 0; 251 } 252 253 protected final void setDirtyFlags(int dirtyFlags) { 254 flags = dirtyFlags & DIRTY_MASK | flags & ~DIRTY_MASK; 255 } 256 257 protected final void appendDirtyFlags(int dirtyFlags) { 258 flags |= (dirtyFlags & DIRTY_MASK); 259 } 260 261 @Override 262 public boolean isReadOnly() { 263 return areFlagsSet(IS_READONLY); 264 } 265 266 @Override 267 public void setReadOnly(boolean value) { 268 if (value) { 269 setFlags(IS_READONLY); 270 } else { 271 clearFlags(IS_READONLY); 272 } 273 } 274 275 public final boolean areFlagsSet(long flags) { 276 return (this.flags & flags) != 0; 277 } 278 279 public final void setFlags(long flags) { 280 this.flags |= flags; 281 } 282 283 public final void clearFlags(long flags) { 284 this.flags &= ~flags; 285 } 286 287 @Override 288 public int getDirtyFlags() { 289 return flags & DIRTY_MASK; 290 } 291 292 @Override 293 public void clearDirtyFlags() { 294 if ((flags & IS_REMOVED) != 0) { 295 // if is removed the property becomes a phantom 296 setDirtyFlags(IS_PHANTOM); 297 } else { 298 setDirtyFlags(NONE); 299 } 300 } 301 302 @Override 303 public boolean hasDefaultValue() { 304 if (isComplex()) { 305 return getChildren().stream().anyMatch(Property::hasDefaultValue); 306 } else { 307 return getField().getDefaultValue() != null; 308 } 309 } 310 311 /** 312 * This method is public because of DataModelImpl which use it. 313 * <p> 314 * TODO after removing DataModelImpl make it protected. 315 */ 316 public void setIsModified() { 317 if ((flags & IS_MODIFIED) == 0) { // if not already modified 318 // clear dirty + phatom flag if any 319 flags |= IS_MODIFIED; // set the modified flag 320 flags &= ~IS_PHANTOM; // remove phantom flag if any 321 } 322 if (parent != null) { 323 ((AbstractProperty) parent).setIsModified(); 324 } 325 } 326 327 protected void setIsNew() { 328 if (isDirty()) { 329 throw new IllegalStateException("Cannot set IS_NEW flag on a dirty property"); 330 } 331 // clear dirty + phantom flag if any 332 setDirtyFlags(IS_NEW); // this clear any dirty flag and set the new 333 // flag 334 if (parent != null) { 335 ((AbstractProperty) parent).setIsModified(); 336 } 337 } 338 339 protected void setIsRemoved() { 340 if (isPhantom() || parent == null || parent.isList()) { 341 throw new IllegalStateException("Cannot set IS_REMOVED on removed or properties that are not map elements"); 342 } 343 if ((flags & IS_REMOVED) == 0) { // if not already removed 344 // clear dirty + phatom flag if any 345 setDirtyFlags(IS_REMOVED); 346 ((AbstractProperty) parent).setIsModified(); 347 } 348 } 349 350 protected void setIsMoved() { 351 if (parent == null || !parent.isList()) { 352 throw new IllegalStateException("Cannot set IS_MOVED on removed or properties that are not map elements"); 353 } 354 if ((flags & IS_MOVED) == 0) { 355 flags |= IS_MOVED; 356 ((AbstractProperty) parent).setIsModified(); 357 } 358 } 359 360 protected boolean isDeprecated() { 361 return areFlagsSet(IS_DEPRECATED); 362 } 363 364 /** 365 * Returns the {@link Property} fallback to use if the current property is deprecated. 366 * 367 * @return the fallback as {@link Property} if exist 368 */ 369 protected Optional<Property> getDeprecatedFallback() { 370 if (parent instanceof AbstractProperty && ((AbstractProperty) parent).isDeprecated()) { 371 return ((AbstractProperty) parent).getDeprecatedFallback().map(p -> p.resolvePath(getName())); 372 } 373 PropertyCharacteristicHandler propertyHandler = Framework.getService(PropertyCharacteristicHandler.class); 374 return propertyHandler.getFallback(getSchema().getName(), getXPath()).map(f -> resolvePath('/' + f)); 375 } 376 377 /** 378 * Returns whether or not current user can edit this property. 379 * 380 * @since 11.1 381 */ 382 protected boolean isSecuredForContext() { 383 return isSecured() && !NuxeoPrincipal.isCurrentAdministrator(); 384 } 385 386 /** 387 * {@inheritDoc} 388 */ 389 @Override 390 public boolean isSecured() { 391 return areFlagsSet(IS_SECURED); 392 } 393 394 @Override 395 public <T> T getValue(Class<T> type) throws PropertyException { 396 return convertTo(getValue(), type); 397 } 398 399 @Override 400 public void setValue(Object value) throws PropertyException { 401 // 1. check the read only flag or security flag 402 if (isReadOnly() || isSecuredForContext()) { 403 throw new ReadOnlyPropertyException( 404 String.format("Cannot set the value of property: %s since it is readonly", getXPath())); 405 } 406 // 1. normalize the value 407 Serializable normalizedValue = normalize(value); 408 // 2. backup the current 409 Serializable current = internalGetValue(); 410 // if its a phantom, no need to check for changes, set it dirty 411 if (!isSameValue(normalizedValue, current) || isForceDirty()) { 412 // 3. set the normalized value and 413 internalSetValue(normalizedValue); 414 // 4. set also value to fallback if this property is deprecated and has one 415 setValueDeprecation(normalizedValue, true); 416 // 5. update flags 417 setIsModified(); 418 } else { 419 removePhantomFlag(); 420 } 421 } 422 423 /** 424 * If this property is deprecated and has a fallback, set value to fallback. 425 */ 426 protected void setValueDeprecation(Object value, boolean setFallback) throws PropertyException { 427 if (isDeprecated()) { 428 // First check if we need to set the fallback value 429 Optional<Property> deprecatedFallback = getDeprecatedFallback(); 430 if (setFallback) { 431 deprecatedFallback.ifPresent(p -> p.setValue(value)); 432 } 433 // Second check if we need to log deprecation message 434 if (log.isWarnEnabled()) { 435 StringBuilder msg = newDeprecatedMessage(); 436 msg.append("Set value to deprecated property"); 437 deprecatedFallback.ifPresent( 438 p -> msg.append(" and to fallback property '").append(p.getXPath()).append("'")); 439 440 if (log.isTraceEnabled()) { 441 log.warn(msg, new NuxeoException("debug stack trace")); 442 } else { 443 log.warn(msg); 444 } 445 } 446 } 447 } 448 449 protected boolean isSameValue(Serializable value1, Serializable value2) { 450 return ((value1 == null && value2 == null) || (value1 != null && value1.equals(value2))); 451 } 452 453 @Override 454 public void setValue(String path, Object value) throws PropertyException { 455 resolvePath(path).setValue(value); 456 } 457 458 @Override 459 public <T> T getValue(Class<T> type, String path) throws PropertyException { 460 return resolvePath(path).getValue(type); 461 } 462 463 @Override 464 public Serializable getValue(String path) throws PropertyException { 465 return resolvePath(path).getValue(); 466 } 467 468 @Override 469 public Serializable getValue() throws PropertyException { 470 if (isPhantom() || isRemoved()) { 471 return getDefaultValue(); 472 } 473 Serializable fallbackValue = getValueDeprecation(); 474 return fallbackValue == null ? internalGetValue() : fallbackValue; 475 } 476 477 /** 478 * @return the fallback value if this property is deprecated and has a fallback, otherwise return null 479 */ 480 protected Serializable getValueDeprecation() { 481 if (isDeprecated()) { 482 Optional<Property> deprecatedFallback = getDeprecatedFallback(); 483 // Check if we need to log deprecation message 484 if (log.isWarnEnabled()) { 485 StringBuilder msg = newDeprecatedMessage(); 486 deprecatedFallback.ifPresentOrElse( 487 p -> msg.append("Return value from '") 488 .append(p.getXPath()) 489 .append("' if not null, from deprecated property otherwise"), 490 () -> msg.append("Return value from deprecated property")); 491 if (log.isTraceEnabled()) { 492 log.warn(msg, new NuxeoException()); 493 } else { 494 log.warn(msg); 495 } 496 } 497 return deprecatedFallback.map(Property::getValue).orElse(null); 498 } 499 return null; 500 } 501 502 protected StringBuilder newDeprecatedMessage() { 503 StringBuilder builder = new StringBuilder().append("Property '") 504 .append(getXPath()) 505 .append("' is marked as deprecated from '") 506 .append(getSchema().getName()) 507 .append("' schema"); 508 Property deprecatedParent = getDeprecatedParent(); 509 if (deprecatedParent != this) { 510 builder.append(" because property '") 511 .append(deprecatedParent.getXPath()) 512 .append("' is marked as deprecated"); 513 } 514 return builder.append(", don't use it anymore. "); 515 } 516 517 /** 518 * @return the higher deprecated parent. 519 */ 520 protected AbstractProperty getDeprecatedParent() { 521 if (parent instanceof AbstractProperty) { 522 AbstractProperty absParent = (AbstractProperty) parent; 523 if (absParent.isDeprecated()) { 524 return absParent.getDeprecatedParent(); 525 } 526 } 527 return this; 528 } 529 530 @Override 531 public Serializable getValueForWrite() throws PropertyException { 532 return getValue(); 533 } 534 535 protected Serializable getDefaultValue() { 536 return (Serializable) getField().getDefaultValue(); 537 } 538 539 @Override 540 public void moveTo(int index) { 541 if (parent == null || !parent.isList()) { 542 throw new UnsupportedOperationException("Not a list item property"); 543 } 544 ListProperty list = (ListProperty) parent; 545 if (list.moveTo(this, index)) { 546 setIsMoved(); 547 } 548 } 549 550 @Override 551 public DocumentPart getRoot() { 552 return parent == null ? (DocumentPart) this : parent.getRoot(); 553 } 554 555 @Override 556 public Property resolvePath(String path) throws PropertyNotFoundException { 557 return resolvePath(new Path(path)); 558 } 559 560 @Override 561 public Property resolvePath(Path path) throws PropertyNotFoundException { 562 // handle absolute paths -> resolve them relative to the root 563 if (path.isAbsolute()) { 564 return getRoot().resolvePath(path.makeRelative()); 565 } 566 567 String[] segments = path.segments(); 568 // handle ../../ paths 569 Property property = this; 570 int start = 0; 571 for (; start < segments.length; start++) { 572 if (segments[start].equals("..")) { 573 property = property.getParent(); 574 } else { 575 break; 576 } 577 } 578 579 // now start resolving the path from 'start' depth relative to 580 // 'property' 581 for (int i = start; i < segments.length; i++) { 582 String segment = segments[i]; 583 if (property.isScalar()) { 584 throw new PropertyNotFoundException(path.toString(), 585 "segment " + segment + " points to a scalar property"); 586 } 587 String index = null; 588 if (segment.endsWith("]")) { 589 int p = segment.lastIndexOf('['); 590 if (p == -1) { 591 throw new PropertyNotFoundException(path.toString(), "Parse error: no matching '[' was found"); 592 } 593 index = segment.substring(p + 1, segment.length() - 1); 594 } 595 if (index == null) { 596 property = property.get(segment); 597 if (property == null) { 598 throw new PropertyNotFoundException(path.toString(), "segment " + segment + " cannot be resolved"); 599 } 600 } else { 601 property = property.get(index); 602 } 603 } 604 return property; 605 } 606 607 /** 608 * Returns the {@link RemovedProperty} if it is a removed property or null otherwise. 609 * 610 * @since 9.2 611 */ 612 protected Property computeRemovedProperty(String name) { 613 String schema = getSchema().getName(); 614 // name is only the property name we try to get, build its path in order to check it against configuration 615 String originalPath = collectPath(new Path("/")).append(name).toString().substring(1); 616 String path; 617 // replace all something[..] in a path by *, for example files/item[2]/filename -> files/*/filename 618 if (originalPath.indexOf('[') != -1) { 619 path = NON_CANON_INDEX.matcher(originalPath).replaceAll("*"); 620 } else { 621 path = originalPath; 622 } 623 PropertyCharacteristicHandler propertyHandler = Framework.getService(PropertyCharacteristicHandler.class); 624 if (!propertyHandler.isRemoved(schema, path)) { 625 return null; 626 } 627 return propertyHandler.getFallback(schema, path).map(fallback -> { 628 // Retrieve fallback property 629 Matcher matcher = NON_CANON_INDEX.matcher(originalPath); 630 while (matcher.find()) { 631 fallback = fallback.replaceFirst("\\*", matcher.group(0)); 632 } 633 Property fallbackProperty; 634 // Handle creation of complex property in a list ie: path contains [-1] 635 int i = fallback.lastIndexOf("[-1]"); 636 if (i != -1) { 637 // skip [-1]/ to get next property 638 fallbackProperty = get(fallback.substring(i + 5)); 639 } else { 640 fallbackProperty = resolvePath('/' + fallback); 641 } 642 return new RemovedProperty(this, name, fallbackProperty); 643 }).orElseGet(() -> new RemovedProperty(this, name)); 644 } 645 646 @Override 647 public Serializable normalize(Object value) throws PropertyConversionException { 648 if (isNormalized(value)) { 649 return (Serializable) value; 650 } 651 throw new PropertyConversionException(value.getClass(), Serializable.class, getXPath()); 652 } 653 654 @Override 655 public boolean isNormalized(Object value) { 656 return value == null || value instanceof Serializable; 657 } 658 659 @Override 660 public <T> T convertTo(Serializable value, Class<T> toType) throws PropertyConversionException { 661 // TODO FIXME XXX make it abstract at this level 662 throw new UnsupportedOperationException("Not implemented"); 663 } 664 665 @Override 666 public boolean validateType(Class<?> type) { 667 return true; // TODO XXX FIXME 668 } 669 670 @Override 671 public Object newInstance() { 672 return null; // TODO XXX FIXME 673 } 674 675 @Override 676 public String toString() { 677 return getClass().getSimpleName() + '(' + getXPath() + ')'; 678 } 679 680 @Override 681 public PropertyObjectResolver getObjectResolver() { 682 ObjectResolver resolver = getType().getObjectResolver(); 683 if (resolver != null) { 684 return new PropertyObjectResolverImpl(this, resolver); 685 } 686 return null; 687 } 688 689 @Override 690 public boolean isForceDirty() { 691 return forceDirty; 692 } 693 694 @Override 695 public void setForceDirty(boolean forceDirty) { 696 this.forceDirty = forceDirty; 697 } 698 699}