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<String, Property>(); 056 } 057 058 protected ComplexProperty(Property parent, int flags) { 059 super(parent, flags); 060 children = new HashMap<String, Property>(); 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 * @throws UnsupportedOperationException 075 */ 076 protected Property internalGetChild(Field field) { 077 return null; // we don't store property that are not in the cache 078 } 079 080 @Override 081 public abstract ComplexType getType(); 082 083 @Override 084 public boolean isNormalized(Object value) { 085 return value == null || value instanceof Map; 086 } 087 088 @Override 089 public Serializable normalize(Object value) throws PropertyConversionException { 090 if (isNormalized(value)) { 091 return (Serializable) value; 092 } 093 throw new PropertyConversionException(value.getClass(), Map.class, getXPath()); 094 } 095 096 @Override 097 public Property get(int index) { 098 throw new UnsupportedOperationException("accessing children by index is not allowed for complex properties"); 099 } 100 101 public final Property getNonPhantomChild(Field field) { 102 String name = field.getName().getPrefixedName(); 103 Property property = children.get(name); 104 if (property == null) { 105 property = internalGetChild(field); 106 if (property == null) { 107 return null; 108 } 109 children.put(name, property); 110 } 111 return property; 112 } 113 114 public final Property getChild(Field field) { 115 Property property = getNonPhantomChild(field); 116 if (property == null) { 117 property = getRoot().createProperty(this, field, IS_PHANTOM); 118 children.put(property.getName(), property); // cache it 119 } 120 return property; 121 } 122 123 public final Collection<Property> getNonPhantomChildren() { 124 ComplexType type = getType(); 125 if (children.size() < type.getFieldsCount()) { // populate with 126 // unloaded props only 127 // if needed 128 for (Field field : type.getFields()) { 129 getNonPhantomChild(field); // force loading non phantom props 130 } 131 } 132 return Collections.unmodifiableCollection(children.values()); 133 } 134 135 @Override 136 public Collection<Property> getChildren() { 137 ComplexType type = getType(); 138 if (children.size() < type.getFieldsCount()) { // populate with 139 // phantoms if needed 140 for (Field field : type.getFields()) { 141 getChild(field); // force loading all props including 142 // phantoms 143 } 144 } 145 return Collections.unmodifiableCollection(children.values()); 146 } 147 148 @Override 149 public Property get(String name) throws PropertyNotFoundException { 150 Field field = getType().getField(name); 151 if (field == null) { 152 return computeRemovedProperty(name); 153 } 154 return getChild(field); 155 } 156 157 @Override 158 public Serializable internalGetValue() throws PropertyException { 159 // noinspection CollectionDeclaredAsConcreteClass 160 HashMap<String, Serializable> map = new HashMap<String, Serializable>(); 161 for (Property property : getChildren()) { 162 map.put(property.getName(), property.getValue()); 163 } 164 return map; 165 } 166 167 @Override 168 public Serializable getValueForWrite() throws PropertyException { 169 if (isPhantom() || isRemoved()) { 170 return getDefaultValue(); 171 } 172 HashMap<String, Serializable> map = new HashMap<String, Serializable>(); 173 for (Property property : getChildren()) { 174 map.put(property.getName(), property.getValueForWrite()); 175 } 176 return map; 177 } 178 179 @Override 180 @SuppressWarnings("unchecked") 181 public void init(Serializable value) throws PropertyException { 182 if (value == null) { // IGNORE null values - properties will be 183 // considered PHANTOMS 184 return; 185 } 186 Map<String, Serializable> map = (Map<String, Serializable>) value; 187 for (Entry<String, Serializable> entry : map.entrySet()) { 188 Property property = get(entry.getKey()); 189 property.init(entry.getValue()); 190 } 191 removePhantomFlag(); 192 } 193 194 @Override 195 protected Serializable getDefaultValue() { 196 return new HashMap<String, Serializable>(); 197 } 198 199 @Override 200 @SuppressWarnings("unchecked") 201 public void setValue(Object value) throws PropertyException { 202 if (!isContainer()) { // if not a container use default setValue() 203 super.setValue(value); 204 return; 205 } 206 if (isReadOnly()) { 207 throw new ReadOnlyPropertyException(getXPath()); 208 } 209 if (value == null) { 210 remove(); 211 // completly clear this property 212 for (Property child : children.values()) { 213 child.remove(); 214 } 215 return; // TODO how to treat nulls? 216 } 217 if (!(value instanceof Map)) { 218 throw new InvalidPropertyValueException(getXPath()); 219 } 220 221 if (getRoot().getClearComplexPropertyBeforeSet()) { 222 // completely clear this property before adding new values 223 for (Property child : children.values()) { 224 child.remove(); 225 } 226 children.clear(); 227 } 228 229 Map<String, Object> map = (Map<String, Object>) value; 230 for (Entry<String, Object> entry : map.entrySet()) { 231 Property property = get(entry.getKey()); 232 if (property.isPhantom() && this.isNew()) { 233 // make sure complex list elements are rewritten 234 property.setForceDirty(true); 235 } 236 property.setValue(entry.getValue()); 237 } 238 setValueDeprecation(value, false); 239 } 240 241 @Override 242 public Property addValue(Object value) { 243 throw new UnsupportedOperationException("add(value) operation not supported on map properties"); 244 } 245 246 @Override 247 public Property addValue(int index, Object value) { 248 throw new UnsupportedOperationException("add(value, index) operation not supported on map properties"); 249 } 250 251 @Override 252 public Property addEmpty() { 253 throw new UnsupportedOperationException("add() operation not supported on map properties"); 254 } 255 256 public void visitChildren(PropertyVisitor visitor, Object arg) throws PropertyException { 257 boolean includePhantoms = visitor.acceptPhantoms(); 258 if (includePhantoms) { 259 for (Property property : getChildren()) { 260 property.accept(visitor, arg); 261 } 262 } else { 263 for (Field field : getType().getFields()) { 264 Property property = getNonPhantomChild(field); 265 if (property == null) { 266 continue; // a phantom property not yet initialized 267 } else if (property.isPhantom()) { 268 continue; // a phantom property 269 } else { 270 property.accept(visitor, arg); 271 } 272 } 273 } 274 } 275 276 /** 277 * Should be used by container properties. Non container props must overwrite this. 278 */ 279 @Override 280 public boolean isSameAs(Property property) throws PropertyException { 281 if (!(property instanceof ComplexProperty)) { 282 return false; 283 } 284 ComplexProperty cp = (ComplexProperty) property; 285 if (isContainer()) { 286 if (!cp.isContainer()) { 287 return false; 288 } 289 Collection<Property> c1 = getNonPhantomChildren(); 290 Collection<Property> c2 = cp.getNonPhantomChildren(); 291 if (c1.size() != c2.size()) { 292 return false; 293 } 294 for (Property p : c1) { 295 Property child = cp.getNonPhantomChild(p.getField()); 296 if (child == null) { 297 return false; 298 } 299 if (!p.isSameAs(child)) { 300 return false; 301 } 302 } 303 } 304 return true; 305 } 306 307 @Override 308 public Iterator<Property> getDirtyChildren() { 309 if (!isContainer()) { 310 throw new UnsupportedOperationException("Cannot iterate over children of scalar properties"); 311 } 312 return new DirtyPropertyIterator(children.values().iterator()); 313 } 314 315 /** 316 * Throws UnsupportedOperationException, added to implement List<Property> interface 317 */ 318 @Override 319 public void clear() { 320 throw new UnsupportedOperationException(); 321 } 322 323 /** 324 * Throws UnsupportedOperationException, added to implement List<Property> interface 325 */ 326 @Override 327 public boolean containsKey(Object key) { 328 throw new UnsupportedOperationException(); 329 } 330 331 /** 332 * Throws UnsupportedOperationException, added to implement List<Property> interface 333 */ 334 @Override 335 public boolean containsValue(Object value) { 336 throw new UnsupportedOperationException(); 337 } 338 339 @Override 340 public Set<Entry<String, Property>> entrySet() { 341 return children.entrySet(); 342 } 343 344 @Override 345 public Property get(Object key) { 346 return children.get(key); 347 } 348 349 @Override 350 public boolean isEmpty() { 351 return children.isEmpty(); 352 } 353 354 @Override 355 public Set<String> keySet() { 356 return children.keySet(); 357 } 358 359 /** 360 * Throws UnsupportedOperationException, added to implement List<Property> interface 361 */ 362 @Override 363 public Property put(String key, Property value) { 364 throw new UnsupportedOperationException(); 365 } 366 367 /** 368 * Throws UnsupportedOperationException, added to implement List<Property> interface 369 */ 370 @Override 371 public void putAll(Map<? extends String, ? extends Property> t) { 372 throw new UnsupportedOperationException(); 373 } 374 375 /** 376 * Throws UnsupportedOperationException, added to implement List<Property> interface 377 */ 378 @Override 379 public Property remove(Object key) { 380 throw new UnsupportedOperationException(); 381 } 382 383 @Override 384 public Collection<Property> values() { 385 return children.values(); 386 } 387 388 @Override 389 public void clearDirtyFlags() { 390 // even makes child properties not dirty 391 super.clearDirtyFlags(); 392 for (Property child : children.values()) { 393 if (!child.isRemoved() && !child.isPhantom()) { 394 child.clearDirtyFlags(); 395 } 396 } 397 } 398 399}