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; 024 025import org.nuxeo.common.utils.Path; 026import org.nuxeo.ecm.core.api.PropertyException; 027import org.nuxeo.ecm.core.api.model.DocumentPart; 028import org.nuxeo.ecm.core.api.model.Property; 029import org.nuxeo.ecm.core.api.model.PropertyConversionException; 030import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 031import org.nuxeo.ecm.core.api.model.ReadOnlyPropertyException; 032import org.nuxeo.ecm.core.api.model.resolver.PropertyObjectResolver; 033import org.nuxeo.ecm.core.api.model.resolver.PropertyObjectResolverImpl; 034import org.nuxeo.ecm.core.schema.types.Schema; 035import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver; 036 037public abstract class AbstractProperty implements Property { 038 039 private static final long serialVersionUID = 1L; 040 041 /** 042 * Whether or not this property is read only. 043 */ 044 public static final int IS_READONLY = 32; 045 046 public final Property parent; 047 048 /** 049 * for SimpleDocumentModel uses 050 */ 051 public boolean forceDirty = false; 052 053 protected int flags; 054 055 protected AbstractProperty(Property parent) { 056 this.parent = parent; 057 } 058 059 protected AbstractProperty(Property parent, int flags) { 060 this.parent = parent; 061 this.flags = flags; 062 } 063 064 /** 065 * Sets the given normalized value. 066 * <p> 067 * This applies only for nodes that physically store a value (that means non container nodes). Container nodes does 068 * nothing. 069 * 070 * @param value 071 */ 072 public abstract void internalSetValue(Serializable value) throws PropertyException; 073 074 public abstract Serializable internalGetValue() throws PropertyException; 075 076 @Override 077 public void init(Serializable value) throws PropertyException { 078 if (value == null || (value instanceof Object[] && ((Object[]) value).length == 0)) { 079 // ignore null or empty values, properties will be considered phantoms 080 return; 081 } 082 internalSetValue(value); 083 removePhantomFlag(); 084 } 085 086 public void removePhantomFlag() { 087 flags &= ~IS_PHANTOM; 088 if (parent != null) { 089 ((AbstractProperty) parent).removePhantomFlag(); 090 } 091 } 092 093 @Override 094 public void setValue(int index, Object value) throws PropertyException { 095 Property property = get(index); 096 property.setValue(value); 097 } 098 099 @Override 100 public int size() { 101 return getChildren().size(); 102 } 103 104 @Override 105 public Iterator<Property> iterator() { 106 return getChildren().iterator(); 107 } 108 109 @Override 110 public Serializable remove() throws PropertyException { 111 Serializable value = getValue(); 112 if (parent != null && parent.isList()) { // remove from list is 113 // handled separately 114 ListProperty list = (ListProperty) parent; 115 list.remove(this); 116 } else if (!isPhantom()) { // remove from map is easier -> mark the 117 // field as removed and remove the value 118 // do not remove the field if the previous value was null, except if its a property from a SimpleDocumentModel (forceDirty mode) 119 Serializable previous = internalGetValue(); 120 init(null); 121 if (previous != null || isForceDirty()) { 122 setIsRemoved(); 123 } 124 } 125 return value; 126 } 127 128 @Override 129 public Property getParent() { 130 return parent; 131 } 132 133 @Override 134 public String getXPath() { 135 StringBuilder buf = new StringBuilder(); 136 getXPath(buf); 137 return buf.toString(); 138 } 139 140 protected void getXPath(StringBuilder buf) { 141 if (parent != null) { 142 ((AbstractProperty) parent).getXPath(buf); 143 if (parent.isList()) { 144 buf.append('/'); 145 int i = ((ListProperty) parent).children.indexOf(this); 146 buf.append(i); 147 } else { 148 if (buf.length() != 0) { 149 buf.append('/'); 150 } 151 buf.append(getName()); 152 } 153 } 154 } 155 156 @Override 157 public String getPath() { 158 Path path = collectPath(new Path("/")); 159 return path.toString(); 160 } 161 162 protected Path collectPath(Path path) { 163 String name = getName(); 164 if (parent != null) { 165 if (parent.isList()) { 166 int i = ((ListProperty) parent).children.indexOf(this); 167 name = name + '[' + i + ']'; 168 } 169 path = ((AbstractProperty) parent).collectPath(path); 170 } 171 return path.append(name); 172 } 173 174 @Override 175 public Schema getSchema() { 176 return getRoot().getSchema(); 177 } 178 179 @Override 180 public boolean isList() { 181 return getType().isListType(); 182 } 183 184 @Override 185 public boolean isComplex() { 186 return getType().isComplexType(); 187 } 188 189 @Override 190 public boolean isScalar() { 191 return getType().isSimpleType(); 192 } 193 194 @Override 195 public boolean isNew() { 196 return areFlagsSet(IS_NEW); 197 } 198 199 @Override 200 public boolean isRemoved() { 201 return areFlagsSet(IS_REMOVED); 202 } 203 204 @Override 205 public boolean isMoved() { 206 return areFlagsSet(IS_MOVED); 207 } 208 209 @Override 210 public boolean isModified() { 211 return areFlagsSet(IS_MODIFIED); 212 } 213 214 @Override 215 public boolean isPhantom() { 216 return areFlagsSet(IS_PHANTOM); 217 } 218 219 @Override 220 public final boolean isDirty() { 221 return (flags & IS_DIRTY) != 0; 222 } 223 224 protected final void setDirtyFlags(int dirtyFlags) { 225 flags = dirtyFlags & DIRTY_MASK | flags & ~DIRTY_MASK; 226 } 227 228 protected final void appendDirtyFlags(int dirtyFlags) { 229 flags |= (dirtyFlags & DIRTY_MASK); 230 } 231 232 @Override 233 public boolean isReadOnly() { 234 return areFlagsSet(IS_READONLY); 235 } 236 237 @Override 238 public void setReadOnly(boolean value) { 239 if (value) { 240 setFlags(IS_READONLY); 241 } else { 242 clearFlags(IS_READONLY); 243 } 244 } 245 246 public final boolean areFlagsSet(long flags) { 247 return (this.flags & flags) != 0; 248 } 249 250 public final void setFlags(long flags) { 251 this.flags |= flags; 252 } 253 254 public final void clearFlags(long flags) { 255 this.flags &= ~flags; 256 } 257 258 @Override 259 public int getDirtyFlags() { 260 return flags & DIRTY_MASK; 261 } 262 263 @Override 264 public void clearDirtyFlags() { 265 if ((flags & IS_REMOVED) != 0) { 266 // if is removed the property becomes a phantom 267 setDirtyFlags(IS_PHANTOM); 268 } else { 269 setDirtyFlags(NONE); 270 } 271 } 272 273 /** 274 * This method is public because of DataModelImpl which use it. 275 * <p> 276 * TODO after removing DataModelImpl make it protected. 277 */ 278 public void setIsModified() { 279 if ((flags & IS_MODIFIED) == 0) { // if not already modified 280 // clear dirty + phatom flag if any 281 flags |= IS_MODIFIED; // set the modified flag 282 flags &= ~IS_PHANTOM; // remove phantom flag if any 283 } 284 if (parent != null) { 285 ((AbstractProperty) parent).setIsModified(); 286 } 287 } 288 289 protected void setIsNew() { 290 if (isDirty()) { 291 throw new IllegalStateException("Cannot set IS_NEW flag on a dirty property"); 292 } 293 // clear dirty + phantom flag if any 294 setDirtyFlags(IS_NEW); // this clear any dirty flag and set the new 295 // flag 296 if (parent != null) { 297 ((AbstractProperty) parent).setIsModified(); 298 } 299 } 300 301 protected void setIsRemoved() { 302 if (isPhantom() || parent == null || parent.isList()) { 303 throw new IllegalStateException("Cannot set IS_REMOVED on removed or properties that are not map elements"); 304 } 305 if ((flags & IS_REMOVED) == 0) { // if not already removed 306 // clear dirty + phatom flag if any 307 setDirtyFlags(IS_REMOVED); 308 ((AbstractProperty) parent).setIsModified(); 309 } 310 } 311 312 protected void setIsMoved() { 313 if (parent == null || !parent.isList()) { 314 throw new IllegalStateException("Cannot set IS_MOVED on removed or properties that are not map elements"); 315 } 316 if ((flags & IS_MOVED) == 0) { 317 flags |= IS_MOVED; 318 ((AbstractProperty) parent).setIsModified(); 319 } 320 } 321 322 @Override 323 public <T> T getValue(Class<T> type) throws PropertyException { 324 return convertTo(getValue(), type); 325 } 326 327 @Override 328 public void setValue(Object value) throws PropertyException { 329 // 1. check the read only flag 330 if (isReadOnly()) { 331 throw new ReadOnlyPropertyException(getXPath()); 332 } 333 // 1. normalize the value 334 Serializable normalizedValue = normalize(value); 335 // 2. backup the current 336 Serializable current = internalGetValue(); 337 // if its a phantom, no need to check for changes, set it dirty 338 if (!isSameValue(normalizedValue, current) || isForceDirty()) { 339 // 3. set the normalized value and 340 internalSetValue(normalizedValue); 341 // 4. update flags 342 setIsModified(); 343 } else { 344 removePhantomFlag(); 345 } 346 } 347 348 protected boolean isSameValue(Serializable value1, Serializable value2) { 349 return ((value1 == null && value2 == null) || (value1 != null && value1.equals(value2))); 350 } 351 352 @Override 353 public void setValue(String path, Object value) throws PropertyException { 354 resolvePath(path).setValue(value); 355 } 356 357 @Override 358 public <T> T getValue(Class<T> type, String path) throws PropertyException { 359 return resolvePath(path).getValue(type); 360 } 361 362 @Override 363 public Serializable getValue(String path) throws PropertyException { 364 return resolvePath(path).getValue(); 365 } 366 367 @Override 368 public Serializable getValue() throws PropertyException { 369 if (isPhantom() || isRemoved()) { 370 return getDefaultValue(); 371 } 372 return internalGetValue(); 373 } 374 375 @Override 376 public Serializable getValueForWrite() throws PropertyException { 377 return getValue(); 378 } 379 380 protected Serializable getDefaultValue() { 381 return (Serializable) getField().getDefaultValue(); 382 } 383 384 @Override 385 public void moveTo(int index) { 386 if (parent == null || !parent.isList()) { 387 throw new UnsupportedOperationException("Not a list item property"); 388 } 389 ListProperty list = (ListProperty) parent; 390 if (list.moveTo(this, index)) { 391 setIsMoved(); 392 } 393 } 394 395 @Override 396 public DocumentPart getRoot() { 397 return parent == null ? (DocumentPart) this : parent.getRoot(); 398 } 399 400 @Override 401 public Property resolvePath(String path) throws PropertyNotFoundException { 402 return resolvePath(new Path(path)); 403 } 404 405 @Override 406 public Property resolvePath(Path path) throws PropertyNotFoundException { 407 // handle absolute paths -> resolve them relative to the root 408 if (path.isAbsolute()) { 409 return getRoot().resolvePath(path.makeRelative()); 410 } 411 412 String[] segments = path.segments(); 413 // handle ../../ paths 414 Property property = this; 415 int start = 0; 416 for (; start < segments.length; start++) { 417 if (segments[start].equals("..")) { 418 property = property.getParent(); 419 } else { 420 break; 421 } 422 } 423 424 // now start resolving the path from 'start' depth relative to 425 // 'property' 426 for (int i = start; i < segments.length; i++) { 427 String segment = segments[i]; 428 if (property.isScalar()) { 429 throw new PropertyNotFoundException(path.toString(), "segment " + segment 430 + " points to a scalar property"); 431 } 432 String index = null; 433 if (segment.endsWith("]")) { 434 int p = segment.lastIndexOf('['); 435 if (p == -1) { 436 throw new PropertyNotFoundException(path.toString(), "Parse error: no matching '[' was found"); 437 } 438 index = segment.substring(p + 1, segment.length() - 1); 439 segment = segment.substring(0, p); 440 } 441 if (index == null) { 442 property = property.get(segment); 443 if (property == null) { 444 throw new PropertyNotFoundException(path.toString(), "segment " + segments[i] 445 + " cannot be resolved"); 446 } 447 } else { 448 property = property.get(index); 449 } 450 } 451 return property; 452 } 453 454 @Override 455 public Serializable normalize(Object value) throws PropertyConversionException { 456 if (isNormalized(value)) { 457 return (Serializable) value; 458 } 459 throw new PropertyConversionException(value.getClass(), Serializable.class, getXPath()); 460 } 461 462 @Override 463 public boolean isNormalized(Object value) { 464 return value == null || value instanceof Serializable; 465 } 466 467 @Override 468 public <T> T convertTo(Serializable value, Class<T> toType) throws PropertyConversionException { 469 // TODO FIXME XXX make it abstract at this level 470 throw new UnsupportedOperationException("Not implemented"); 471 } 472 473 @Override 474 public boolean validateType(Class<?> type) { 475 return true; // TODO XXX FIXME 476 } 477 478 @Override 479 public Object newInstance() { 480 return null; // TODO XXX FIXME 481 } 482 483 @Override 484 public String toString() { 485 return getClass().getSimpleName() + '(' + getXPath() + ')'; 486 } 487 488 @Override 489 public PropertyObjectResolver getObjectResolver() { 490 ObjectResolver resolver = getType().getObjectResolver(); 491 if (resolver != null) { 492 return new PropertyObjectResolverImpl(this, resolver); 493 } 494 return null; 495 } 496 497 @Override 498 public boolean isForceDirty() { 499 return forceDirty; 500 } 501 502 @Override 503 public void setForceDirty(boolean forceDirty) { 504 this.forceDirty = forceDirty; 505 } 506 507}