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