001/* 002 * (C) Copyright 2006-2011 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 * Nuxeo - initial API and implementation 018 * 019 * $Id$ 020 */ 021 022package org.nuxeo.ecm.core.api.model.impl; 023 024import java.io.Serializable; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.HashMap; 028import java.util.Iterator; 029import java.util.Map; 030import java.util.Set; 031 032import org.nuxeo.ecm.core.api.PropertyException; 033import org.nuxeo.ecm.core.api.model.InvalidPropertyValueException; 034import org.nuxeo.ecm.core.api.model.Property; 035import org.nuxeo.ecm.core.api.model.PropertyConversionException; 036import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 037import org.nuxeo.ecm.core.api.model.PropertyVisitor; 038import org.nuxeo.ecm.core.api.model.ReadOnlyPropertyException; 039import org.nuxeo.ecm.core.schema.types.ComplexType; 040import org.nuxeo.ecm.core.schema.types.Field; 041 042/** 043 * A scalar property that is linked to a schema field 044 * 045 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 046 */ 047public abstract class ComplexProperty extends AbstractProperty implements Map<String, Property> { 048 049 private static final long serialVersionUID = 1L; 050 051 protected Map<String, Property> children; 052 053 protected ComplexProperty(Property parent) { 054 super(parent); 055 children = new HashMap<>(); 056 } 057 058 protected ComplexProperty(Property parent, int flags) { 059 super(parent, flags); 060 children = new HashMap<>(); 061 } 062 063 /** 064 * Gets the property given its name. If the property was not set, returns null. 065 * <p> 066 * This method will always be called using a valid property name (a property specified by the schema). The returned 067 * property will be cached by its parent so the next time it is needed, it will be reused from the cache. That means 068 * this method servers as a initializer for properties - usually you create a new property and return it - you don't 069 * need to cache created properties. 070 * <p> 071 * If you want to change the way a property is fetched / stored, you must override this method. 072 * 073 * @return the child. Cannot return null 074 */ 075 protected Property internalGetChild(Field field) { 076 return null; // we don't store property that are not in the cache 077 } 078 079 @Override 080 public abstract ComplexType getType(); 081 082 @Override 083 public boolean isNormalized(Object value) { 084 return value == null || value instanceof Map; 085 } 086 087 @Override 088 public Serializable normalize(Object value) throws PropertyConversionException { 089 if (isNormalized(value)) { 090 return (Serializable) value; 091 } 092 throw new PropertyConversionException(value.getClass(), Map.class, getXPath()); 093 } 094 095 @Override 096 public Property get(int index) { 097 throw new UnsupportedOperationException("accessing children by index is not allowed for complex properties"); 098 } 099 100 public final Property getNonPhantomChild(Field field) { 101 String name = field.getName().getPrefixedName(); 102 Property property = children.get(name); 103 if (property == null) { 104 property = internalGetChild(field); 105 if (property == null) { 106 return null; 107 } 108 children.put(name, property); 109 } 110 return property; 111 } 112 113 public final Property getChild(Field field) { 114 Property property = getNonPhantomChild(field); 115 if (property == null) { 116 property = getRoot().createProperty(this, field, IS_PHANTOM); 117 children.put(property.getName(), property); // cache it 118 } 119 return property; 120 } 121 122 public final Collection<Property> getNonPhantomChildren() { 123 ComplexType type = getType(); 124 if (children.size() < type.getFieldsCount()) { // populate with 125 // unloaded props only 126 // if needed 127 for (Field field : type.getFields()) { 128 getNonPhantomChild(field); // force loading non phantom props 129 } 130 } 131 return Collections.unmodifiableCollection(children.values()); 132 } 133 134 @Override 135 public Collection<Property> getChildren() { 136 ComplexType type = getType(); 137 if (children.size() < type.getFieldsCount()) { // populate with 138 // phantoms if needed 139 for (Field field : type.getFields()) { 140 getChild(field); // force loading all props including 141 // phantoms 142 } 143 } 144 return Collections.unmodifiableCollection(children.values()); 145 } 146 147 @Override 148 public Property get(String name) throws PropertyNotFoundException { 149 Field field = getType().getField(name); 150 if (field == null) { 151 return computeRemovedProperty(name); 152 } 153 return getChild(field); 154 } 155 156 @Override 157 public void set(String name, Property property) throws PropertyNotFoundException { 158 Field field = getType().getField(name); 159 if (field == null) { 160 Property removedProperty = computeRemovedProperty(name); 161 if (removedProperty != null) { 162 removedProperty.set(name, property); 163 } 164 return; 165 } 166 children.put(property.getName(), property); 167 setIsModified(); 168 } 169 170 @Override 171 public Serializable internalGetValue() throws PropertyException { 172 var map = new HashMap<String, Serializable>(); 173 for (Property property : getChildren()) { 174 map.put(property.getName(), property.getValue()); 175 } 176 return map; 177 } 178 179 @Override 180 public Serializable getValueForWrite() throws PropertyException { 181 if (isPhantom() || isRemoved()) { 182 return getDefaultValue(); 183 } 184 HashMap<String, Serializable> map = new HashMap<>(); 185 for (Property property : getChildren()) { 186 map.put(property.getName(), property.getValueForWrite()); 187 } 188 return map; 189 } 190 191 @Override 192 @SuppressWarnings("unchecked") 193 public void init(Serializable value) throws PropertyException { 194 if (value == null) { // IGNORE null values - properties will be 195 // considered PHANTOMS 196 return; 197 } 198 Map<String, Serializable> map = (Map<String, Serializable>) value; 199 for (Entry<String, Serializable> entry : map.entrySet()) { 200 Property property = get(entry.getKey()); 201 property.init(entry.getValue()); 202 } 203 removePhantomFlag(); 204 } 205 206 @Override 207 protected Serializable getDefaultValue() { 208 return new HashMap<String, Serializable>(); 209 } 210 211 @Override 212 @SuppressWarnings("unchecked") 213 public void setValue(Object value) throws PropertyException { 214 if (!isContainer()) { // if not a container use default setValue() 215 super.setValue(value); 216 return; 217 } 218 if (isReadOnly() || isSecuredForContext()) { 219 throw new ReadOnlyPropertyException( 220 String.format("Cannot set the value of property: %s since it is readonly", getXPath())); 221 } 222 if (value == null) { 223 remove(); 224 // completly clear this property 225 for (Property child : children.values()) { 226 child.remove(); 227 } 228 return; // TODO how to treat nulls? 229 } 230 if (!(value instanceof Map)) { 231 throw new InvalidPropertyValueException(getXPath()); 232 } 233 234 if (getRoot().getClearComplexPropertyBeforeSet()) { 235 // completely clear this property before adding new values 236 for (Property child : children.values()) { 237 child.remove(); 238 } 239 children.clear(); 240 } 241 242 Map<String, Object> map = (Map<String, Object>) value; 243 for (Entry<String, Object> entry : map.entrySet()) { 244 Property property = get(entry.getKey()); 245 if (property.isPhantom() && this.isNew()) { 246 // make sure complex list elements are rewritten 247 property.setForceDirty(true); 248 } 249 property.setValue(entry.getValue()); 250 } 251 setValueDeprecation(value, false); 252 } 253 254 @Override 255 public Property addValue(Object value) { 256 throw new UnsupportedOperationException("add(value) operation not supported on map properties"); 257 } 258 259 @Override 260 public Property addValue(int index, Object value) { 261 throw new UnsupportedOperationException("add(value, index) operation not supported on map properties"); 262 } 263 264 @Override 265 public Property addEmpty() { 266 throw new UnsupportedOperationException("add() operation not supported on map properties"); 267 } 268 269 public void visitChildren(PropertyVisitor visitor, Object arg) throws PropertyException { 270 boolean includePhantoms = visitor.acceptPhantoms(); 271 if (includePhantoms) { 272 for (Property property : getChildren()) { 273 property.accept(visitor, arg); 274 } 275 } else { 276 for (Field field : getType().getFields()) { 277 Property property = getNonPhantomChild(field); 278 if (property == null) { 279 continue; // a phantom property not yet initialized 280 } else if (property.isPhantom()) { 281 continue; // a phantom property 282 } else { 283 property.accept(visitor, arg); 284 } 285 } 286 } 287 } 288 289 /** 290 * Should be used by container properties. Non container props must overwrite this. 291 */ 292 @Override 293 public boolean isSameAs(Property property) throws PropertyException { 294 if (!(property instanceof ComplexProperty)) { 295 return false; 296 } 297 ComplexProperty cp = (ComplexProperty) property; 298 if (isContainer()) { 299 if (!cp.isContainer()) { 300 return false; 301 } 302 Collection<Property> c1 = getNonPhantomChildren(); 303 Collection<Property> c2 = cp.getNonPhantomChildren(); 304 if (c1.size() != c2.size()) { 305 return false; 306 } 307 for (Property p : c1) { 308 Property child = cp.getNonPhantomChild(p.getField()); 309 if (child == null) { 310 return false; 311 } 312 if (!p.isSameAs(child)) { 313 return false; 314 } 315 } 316 } 317 return true; 318 } 319 320 @Override 321 public Iterator<Property> getDirtyChildren() { 322 if (!isContainer()) { 323 throw new UnsupportedOperationException("Cannot iterate over children of scalar properties"); 324 } 325 return new DirtyPropertyIterator(children.values().iterator()); 326 } 327 328 /** 329 * Throws UnsupportedOperationException, added to implement Map<String, Property> interface. 330 */ 331 @Override 332 public void clear() { 333 throw new UnsupportedOperationException(); 334 } 335 336 /** 337 * Throws UnsupportedOperationException, added to implement Map<String, Property> interface. 338 */ 339 @Override 340 public boolean containsKey(Object key) { 341 throw new UnsupportedOperationException(); 342 } 343 344 /** 345 * Throws UnsupportedOperationException, added to implement Map<String, Property> interface. 346 */ 347 @Override 348 public boolean containsValue(Object value) { 349 throw new UnsupportedOperationException(); 350 } 351 352 @Override 353 public Set<Entry<String, Property>> entrySet() { 354 return children.entrySet(); 355 } 356 357 @Override 358 public Property get(Object key) { 359 return children.get(key); 360 } 361 362 @Override 363 public boolean isEmpty() { 364 return children.isEmpty(); 365 } 366 367 @Override 368 public Set<String> keySet() { 369 return children.keySet(); 370 } 371 372 /** 373 * Throws UnsupportedOperationException, added to implement Map<String, Property> interface. 374 */ 375 @Override 376 public Property put(String key, Property value) { 377 throw new UnsupportedOperationException(); 378 } 379 380 /** 381 * Throws UnsupportedOperationException, added to implement Map<String, Property> interface. 382 */ 383 @Override 384 public void putAll(Map<? extends String, ? extends Property> t) { 385 throw new UnsupportedOperationException(); 386 } 387 388 /** 389 * Throws UnsupportedOperationException, added to implement Map<String, Property> interface. 390 */ 391 @Override 392 public Property remove(Object key) { 393 throw new UnsupportedOperationException(); 394 } 395 396 @Override 397 public Collection<Property> values() { 398 return children.values(); 399 } 400 401 @Override 402 public void clearDirtyFlags() { 403 // even makes child properties not dirty 404 super.clearDirtyFlags(); 405 for (Property child : children.values()) { 406 if (!child.isRemoved() && !child.isPhantom()) { 407 child.clearDirtyFlags(); 408 } 409 } 410 } 411 412}