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