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