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, getPath()); 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 null; 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(getPath()); 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(getPath()); 219 } 220 Map<String, Object> map = (Map<String, Object>) value; 221 for (Entry<String, Object> entry : map.entrySet()) { 222 Property property = get(entry.getKey()); 223 if (property.isPhantom() && this.isNew()) { 224 // make sure complex list elements are rewritten 225 property.setForceDirty(true); 226 } 227 property.setValue(entry.getValue()); 228 } 229 } 230 231 @Override 232 public Property addValue(Object value) { 233 throw new UnsupportedOperationException("add(value) operation not supported on map properties"); 234 } 235 236 @Override 237 public Property addValue(int index, Object value) { 238 throw new UnsupportedOperationException("add(value, index) operation not supported on map properties"); 239 } 240 241 @Override 242 public Property addEmpty() { 243 throw new UnsupportedOperationException("add() operation not supported on map properties"); 244 } 245 246 public void visitChildren(PropertyVisitor visitor, Object arg) throws PropertyException { 247 boolean includePhantoms = visitor.acceptPhantoms(); 248 if (includePhantoms) { 249 for (Property property : getChildren()) { 250 property.accept(visitor, arg); 251 } 252 } else { 253 for (Field field : getType().getFields()) { 254 Property property = getNonPhantomChild(field); 255 if (property == null) { 256 continue; // a phantom property not yet initialized 257 } else if (property.isPhantom()) { 258 continue; // a phantom property 259 } else { 260 property.accept(visitor, arg); 261 } 262 } 263 } 264 } 265 266 /** 267 * Should be used by container properties. Non container props must overwrite this. 268 */ 269 @Override 270 public boolean isSameAs(Property property) throws PropertyException { 271 if (!(property instanceof ComplexProperty)) { 272 return false; 273 } 274 ComplexProperty cp = (ComplexProperty) property; 275 if (isContainer()) { 276 if (!cp.isContainer()) { 277 return false; 278 } 279 Collection<Property> c1 = getNonPhantomChildren(); 280 Collection<Property> c2 = cp.getNonPhantomChildren(); 281 if (c1.size() != c2.size()) { 282 return false; 283 } 284 for (Property p : c1) { 285 Property child = cp.getNonPhantomChild(p.getField()); 286 if (child == null) { 287 return false; 288 } 289 if (!p.isSameAs(child)) { 290 return false; 291 } 292 } 293 } 294 return true; 295 } 296 297 @Override 298 public Iterator<Property> getDirtyChildren() { 299 if (!isContainer()) { 300 throw new UnsupportedOperationException("Cannot iterate over children of scalar properties"); 301 } 302 return new DirtyPropertyIterator(children.values().iterator()); 303 } 304 305 /** 306 * Throws UnsupportedOperationException, added to implement List<Property> interface 307 */ 308 @Override 309 public void clear() { 310 throw new UnsupportedOperationException(); 311 } 312 313 /** 314 * Throws UnsupportedOperationException, added to implement List<Property> interface 315 */ 316 @Override 317 public boolean containsKey(Object key) { 318 throw new UnsupportedOperationException(); 319 } 320 321 /** 322 * Throws UnsupportedOperationException, added to implement List<Property> interface 323 */ 324 @Override 325 public boolean containsValue(Object value) { 326 throw new UnsupportedOperationException(); 327 } 328 329 @Override 330 public Set<Entry<String, Property>> entrySet() { 331 return children.entrySet(); 332 } 333 334 @Override 335 public Property get(Object key) { 336 return children.get(key); 337 } 338 339 @Override 340 public boolean isEmpty() { 341 return children.isEmpty(); 342 } 343 344 @Override 345 public Set<String> keySet() { 346 return children.keySet(); 347 } 348 349 /** 350 * Throws UnsupportedOperationException, added to implement List<Property> interface 351 */ 352 @Override 353 public Property put(String key, Property value) { 354 throw new UnsupportedOperationException(); 355 } 356 357 /** 358 * Throws UnsupportedOperationException, added to implement List<Property> interface 359 */ 360 @Override 361 public void putAll(Map<? extends String, ? extends Property> t) { 362 throw new UnsupportedOperationException(); 363 } 364 365 /** 366 * Throws UnsupportedOperationException, added to implement List<Property> interface 367 */ 368 @Override 369 public Property remove(Object key) { 370 throw new UnsupportedOperationException(); 371 } 372 373 @Override 374 public Collection<Property> values() { 375 return children.values(); 376 } 377 378 @Override 379 public void clearDirtyFlags() { 380 // even makes child properties not dirty 381 super.clearDirtyFlags(); 382 for (Property child : children.values()) { 383 if (!child.isRemoved() && !child.isPhantom()) { 384 child.clearDirtyFlags(); 385 } 386 } 387 } 388 389}