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) { // IGNORE null values - properties will be 079 // 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 getPath() { 135 Path path = collectPath(new Path("/")); 136 return path.toString(); 137 } 138 139 protected Path collectPath(Path path) { 140 String name = getName(); 141 if (parent != null) { 142 if (parent.isList()) { 143 int i = ((ListProperty) parent).children.indexOf(this); 144 name = name + '[' + i + ']'; 145 } 146 path = ((AbstractProperty) parent).collectPath(path); 147 } 148 return path.append(name); 149 } 150 151 @Override 152 public Schema getSchema() { 153 return getRoot().getSchema(); 154 } 155 156 @Override 157 public boolean isList() { 158 return getType().isListType(); 159 } 160 161 @Override 162 public boolean isComplex() { 163 return getType().isComplexType(); 164 } 165 166 @Override 167 public boolean isScalar() { 168 return getType().isSimpleType(); 169 } 170 171 @Override 172 public boolean isNew() { 173 return areFlagsSet(IS_NEW); 174 } 175 176 @Override 177 public boolean isRemoved() { 178 return areFlagsSet(IS_REMOVED); 179 } 180 181 @Override 182 public boolean isMoved() { 183 return areFlagsSet(IS_MOVED); 184 } 185 186 @Override 187 public boolean isModified() { 188 return areFlagsSet(IS_MODIFIED); 189 } 190 191 @Override 192 public boolean isPhantom() { 193 return areFlagsSet(IS_PHANTOM); 194 } 195 196 @Override 197 public final boolean isDirty() { 198 return (flags & IS_DIRTY) != 0; 199 } 200 201 protected final void setDirtyFlags(int dirtyFlags) { 202 flags = dirtyFlags & DIRTY_MASK | flags & ~DIRTY_MASK; 203 } 204 205 protected final void appendDirtyFlags(int dirtyFlags) { 206 flags |= (dirtyFlags & DIRTY_MASK); 207 } 208 209 @Override 210 public boolean isReadOnly() { 211 return areFlagsSet(IS_READONLY); 212 } 213 214 @Override 215 public void setReadOnly(boolean value) { 216 if (value) { 217 setFlags(IS_READONLY); 218 } else { 219 clearFlags(IS_READONLY); 220 } 221 } 222 223 public final boolean areFlagsSet(long flags) { 224 return (this.flags & flags) != 0; 225 } 226 227 public final void setFlags(long flags) { 228 this.flags |= flags; 229 } 230 231 public final void clearFlags(long flags) { 232 this.flags &= ~flags; 233 } 234 235 @Override 236 public int getDirtyFlags() { 237 return flags & DIRTY_MASK; 238 } 239 240 @Override 241 public void clearDirtyFlags() { 242 if ((flags & IS_REMOVED) != 0) { 243 // if is removed the property becomes a phantom 244 setDirtyFlags(IS_PHANTOM); 245 } else { 246 setDirtyFlags(NONE); 247 } 248 } 249 250 /** 251 * This method is public because of DataModelImpl which use it. 252 * <p> 253 * TODO after removing DataModelImpl make it protected. 254 */ 255 public void setIsModified() { 256 if ((flags & IS_MODIFIED) == 0) { // if not already modified 257 // clear dirty + phatom flag if any 258 flags |= IS_MODIFIED; // set the modified flag 259 flags &= ~IS_PHANTOM; // remove phantom flag if any 260 } 261 if (parent != null) { 262 ((AbstractProperty) parent).setIsModified(); 263 } 264 } 265 266 protected void setIsNew() { 267 if (isDirty()) { 268 throw new IllegalStateException("Cannot set IS_NEW flag on a dirty property"); 269 } 270 // clear dirty + phantom flag if any 271 setDirtyFlags(IS_NEW); // this clear any dirty flag and set the new 272 // flag 273 if (parent != null) { 274 ((AbstractProperty) parent).setIsModified(); 275 } 276 } 277 278 protected void setIsRemoved() { 279 if (isPhantom() || parent == null || parent.isList()) { 280 throw new IllegalStateException("Cannot set IS_REMOVED on removed or properties that are not map elements"); 281 } 282 if ((flags & IS_REMOVED) == 0) { // if not already removed 283 // clear dirty + phatom flag if any 284 setDirtyFlags(IS_REMOVED); 285 ((AbstractProperty) parent).setIsModified(); 286 } 287 } 288 289 protected void setIsMoved() { 290 if (parent == null || !parent.isList()) { 291 throw new IllegalStateException("Cannot set IS_MOVED on removed or properties that are not map elements"); 292 } 293 if ((flags & IS_MOVED) == 0) { 294 flags |= IS_MOVED; 295 ((AbstractProperty) parent).setIsModified(); 296 } 297 } 298 299 @Override 300 public <T> T getValue(Class<T> type) throws PropertyException { 301 return convertTo(getValue(), type); 302 } 303 304 @Override 305 public void setValue(Object value) throws PropertyException { 306 // 1. check the read only flag 307 if (isReadOnly()) { 308 throw new ReadOnlyPropertyException(getPath()); 309 } 310 // 1. normalize the value 311 Serializable normalizedValue = normalize(value); 312 // 2. backup the current 313 Serializable current = internalGetValue(); 314 // if its a phantom, no need to check for changes, set it dirty 315 if (!isSameValue(normalizedValue, current) || isForceDirty()) { 316 // 3. set the normalized value and 317 internalSetValue(normalizedValue); 318 // 4. update flags 319 setIsModified(); 320 } else { 321 removePhantomFlag(); 322 } 323 } 324 325 protected boolean isSameValue(Serializable value1, Serializable value2) { 326 return ((value1 == null && value2 == null) || (value1 != null && value1.equals(value2))); 327 } 328 329 @Override 330 public void setValue(String path, Object value) throws PropertyException { 331 resolvePath(path).setValue(value); 332 } 333 334 @Override 335 public <T> T getValue(Class<T> type, String path) throws PropertyException { 336 return resolvePath(path).getValue(type); 337 } 338 339 @Override 340 public Serializable getValue(String path) throws PropertyException { 341 return resolvePath(path).getValue(); 342 } 343 344 @Override 345 public Serializable getValue() throws PropertyException { 346 if (isPhantom() || isRemoved()) { 347 return getDefaultValue(); 348 } 349 return internalGetValue(); 350 } 351 352 @Override 353 public Serializable getValueForWrite() throws PropertyException { 354 return getValue(); 355 } 356 357 protected Serializable getDefaultValue() { 358 return (Serializable) getField().getDefaultValue(); 359 } 360 361 @Override 362 public void moveTo(int index) { 363 if (parent == null || !parent.isList()) { 364 throw new UnsupportedOperationException("Not a list item property"); 365 } 366 ListProperty list = (ListProperty) parent; 367 if (list.moveTo(this, index)) { 368 setIsMoved(); 369 } 370 } 371 372 @Override 373 public DocumentPart getRoot() { 374 return parent == null ? (DocumentPart) this : parent.getRoot(); 375 } 376 377 @Override 378 public Property resolvePath(String path) throws PropertyNotFoundException { 379 return resolvePath(new Path(path)); 380 } 381 382 @Override 383 public Property resolvePath(Path path) throws PropertyNotFoundException { 384 // handle absolute paths -> resolve them relative to the root 385 if (path.isAbsolute()) { 386 return getRoot().resolvePath(path.makeRelative()); 387 } 388 389 String[] segments = path.segments(); 390 // handle ../../ paths 391 Property property = this; 392 int start = 0; 393 for (; start < segments.length; start++) { 394 if (segments[start].equals("..")) { 395 property = property.getParent(); 396 } else { 397 break; 398 } 399 } 400 401 // now start resolving the path from 'start' depth relative to 402 // 'property' 403 for (int i = start; i < segments.length; i++) { 404 String segment = segments[i]; 405 if (property.isScalar()) { 406 throw new PropertyNotFoundException(path.toString(), "segment " + segment 407 + " points to a scalar property"); 408 } 409 String index = null; 410 if (segment.endsWith("]")) { 411 int p = segment.lastIndexOf('['); 412 if (p == -1) { 413 throw new PropertyNotFoundException(path.toString(), "Parse error: no matching '[' was found"); 414 } 415 index = segment.substring(p + 1, segment.length() - 1); 416 segment = segment.substring(0, p); 417 } 418 if (index == null) { 419 property = property.get(segment); 420 if (property == null) { 421 throw new PropertyNotFoundException(path.toString(), "segment " + segments[i] 422 + " cannot be resolved"); 423 } 424 } else { 425 property = property.get(index); 426 } 427 } 428 return property; 429 } 430 431 @Override 432 public Serializable normalize(Object value) throws PropertyConversionException { 433 if (isNormalized(value)) { 434 return (Serializable) value; 435 } 436 throw new PropertyConversionException(value.getClass(), Serializable.class, getPath()); 437 } 438 439 @Override 440 public boolean isNormalized(Object value) { 441 return value == null || value instanceof Serializable; 442 } 443 444 @Override 445 public <T> T convertTo(Serializable value, Class<T> toType) throws PropertyConversionException { 446 // TODO FIXME XXX make it abstract at this level 447 throw new UnsupportedOperationException("Not implemented"); 448 } 449 450 @Override 451 public boolean validateType(Class<?> type) { 452 return true; // TODO XXX FIXME 453 } 454 455 @Override 456 public Object newInstance() { 457 return null; // TODO XXX FIXME 458 } 459 460 @Override 461 public String toString() { 462 return getClass().getSimpleName() + '(' + getPath() + ')'; 463 } 464 465 @Override 466 public PropertyObjectResolver getObjectResolver() { 467 ObjectResolver resolver = getType().getObjectResolver(); 468 if (resolver != null) { 469 return new PropertyObjectResolverImpl(this, resolver); 470 } 471 return null; 472 } 473 474 @Override 475 public boolean isForceDirty() { 476 return forceDirty; 477 } 478 479 @Override 480 public void setForceDirty(boolean forceDirty) { 481 this.forceDirty = forceDirty; 482 } 483 484}