001/* 002 * Copyright 2001-2013 Stephen Colebourne 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 */ 016package org.joda.beans.impl.flexi; 017 018import java.io.Serializable; 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.Map; 022import java.util.NoSuchElementException; 023import java.util.Set; 024 025import org.joda.beans.DynamicBean; 026import org.joda.beans.MetaBean; 027import org.joda.beans.Property; 028import org.joda.beans.impl.BasicBean; 029import org.joda.beans.impl.BasicProperty; 030 031/** 032 * Implementation of a fully dynamic {@code Bean}. 033 * <p> 034 * Properties are dynamic, and can be added and removed at will from the map. 035 * The internal storage is created lazily to allow a flexi-bean to be used as 036 * a lightweight extension to another bean. 037 * 038 * @author Stephen Colebourne 039 */ 040public final class FlexiBean extends BasicBean implements DynamicBean, Serializable { 041 // Alternate way to implement this would be to create a list/map of real property 042 // objects which could then be properly typed 043 044 /** Serialization version. */ 045 private static final long serialVersionUID = 1L; 046 047 /** The meta-bean. */ 048 final FlexiMetaBean metaBean = new FlexiMetaBean(this); // CSIGNORE 049 /** The underlying data. */ 050 volatile Map<String, Object> data = Collections.emptyMap();// CSIGNORE 051 052 /** 053 * Constructor. 054 */ 055 public FlexiBean() { 056 } 057 058 /** 059 * Constructor that copies all the data entries from the specified bean. 060 * 061 * @param copyFrom the bean to copy from, not null 062 */ 063 public FlexiBean(FlexiBean copyFrom) { 064 putAll(copyFrom.data); 065 } 066 067 //----------------------------------------------------------------------- 068 /** 069 * Gets the internal data map. 070 * 071 * @return the data, not null 072 */ 073 private Map<String, Object> dataWritable() { 074 if (data == Collections.EMPTY_MAP) { 075 data = new HashMap<String, Object>(); 076 } 077 return data; 078 } 079 080 //----------------------------------------------------------------------- 081 /** 082 * Gets the number of properties. 083 * 084 * @return the number of properties 085 */ 086 public int size() { 087 return data.size(); 088 } 089 090 /** 091 * Checks if the bean contains a specific property. 092 * 093 * @param propertyName the property name, null returns false 094 * @return true if the bean contains the property 095 */ 096 public boolean contains(String propertyName) { 097 return propertyExists(propertyName); 098 } 099 100 /** 101 * Gets the value of the property. 102 * 103 * @param propertyName the property name, not empty 104 * @return the value of the property, may be null 105 */ 106 public Object get(String propertyName) { 107 return data.get(propertyName); 108 } 109 110 /** 111 * Gets the value of the property cast to a specific type. 112 * 113 * @param <T> the value type 114 * @param propertyName the property name, not empty 115 * @param type the type to cast to, not null 116 * @return the value of the property, may be null 117 */ 118 @SuppressWarnings("unchecked") 119 public <T> T get(String propertyName, Class<T> type) { 120 return (T) get(propertyName); 121 } 122 123 /** 124 * Gets the value of the property as a {@code String}. 125 * This will use {@link Object#toString()}. 126 * 127 * @param propertyName the property name, not empty 128 * @return the value of the property, may be null 129 */ 130 public String getString(String propertyName) { 131 Object obj = get(propertyName); 132 return obj != null ? obj.toString() : null; 133 } 134 135 /** 136 * Gets the value of the property as a {@code boolean}. 137 * 138 * @param propertyName the property name, not empty 139 * @return the value of the property 140 * @throws ClassCastException if the value is not compatible 141 */ 142 public boolean getBoolean(String propertyName) { 143 return (Boolean) get(propertyName); 144 } 145 146 /** 147 * Gets the value of the property as a {@code int}. 148 * 149 * @param propertyName the property name, not empty 150 * @return the value of the property 151 * @throws ClassCastException if the value is not compatible 152 */ 153 public int getInt(String propertyName) { 154 return ((Number) get(propertyName)).intValue(); 155 } 156 157 /** 158 * Gets the value of the property as a {@code int} using a default value. 159 * 160 * @param propertyName the property name, not empty 161 * @param defaultValue the default value for null 162 * @return the value of the property 163 * @throws ClassCastException if the value is not compatible 164 */ 165 public int getInt(String propertyName, int defaultValue) { 166 Object obj = get(propertyName); 167 return obj != null ? ((Number) get(propertyName)).intValue() : defaultValue; 168 } 169 170 /** 171 * Gets the value of the property as a {@code long}. 172 * 173 * @param propertyName the property name, not empty 174 * @return the value of the property 175 * @throws ClassCastException if the value is not compatible 176 */ 177 public long getLong(String propertyName) { 178 return ((Number) get(propertyName)).longValue(); 179 } 180 181 /** 182 * Gets the value of the property as a {@code long} using a default value. 183 * 184 * @param propertyName the property name, not empty 185 * @param defaultValue the default value for null 186 * @return the value of the property 187 * @throws ClassCastException if the value is not compatible 188 */ 189 public long getLong(String propertyName, long defaultValue) { 190 Object obj = get(propertyName); 191 return obj != null ? ((Number) get(propertyName)).longValue() : defaultValue; 192 } 193 194 /** 195 * Gets the value of the property as a {@code double}. 196 * 197 * @param propertyName the property name, not empty 198 * @return the value of the property 199 * @throws ClassCastException if the value is not compatible 200 */ 201 public double getDouble(String propertyName) { 202 return ((Number) get(propertyName)).doubleValue(); 203 } 204 205 /** 206 * Gets the value of the property as a {@code double} using a default value. 207 * 208 * @param propertyName the property name, not empty 209 * @param defaultValue the default value for null 210 * @return the value of the property 211 * @throws ClassCastException if the value is not compatible 212 */ 213 public double getDouble(String propertyName, double defaultValue) { 214 Object obj = get(propertyName); 215 return obj != null ? ((Number) get(propertyName)).doubleValue() : defaultValue; 216 } 217 218 //----------------------------------------------------------------------- 219 /** 220 * Adds or updates a property returning {@code this} for chaining. 221 * 222 * @param propertyName the property name, not empty 223 * @param newValue the new value, may be null 224 * @return {@code this} for chaining, not null 225 */ 226 public FlexiBean append(String propertyName, Object newValue) { 227 dataWritable().put(propertyName, newValue); 228 return this; 229 } 230 231 /** 232 * Adds or updates a property. 233 * 234 * @param propertyName the property name, not empty 235 * @param newValue the new value, may be null 236 */ 237 public void set(String propertyName, Object newValue) { 238 dataWritable().put(propertyName, newValue); 239 } 240 241 /** 242 * Puts the property into this bean. 243 * 244 * @param propertyName the property name, not empty 245 * @param newValue the new value, may be null 246 * @return the old value of the property, may be null 247 */ 248 public Object put(String propertyName, Object newValue) { 249 return dataWritable().put(propertyName, newValue); 250 } 251 252 /** 253 * Puts the properties in the specified map into this bean. 254 * 255 * @param map the map of properties to add, not null 256 */ 257 public void putAll(Map<String, Object> map) { 258 if (map.size() > 0) { 259 if (data == Collections.EMPTY_MAP) { 260 data = new HashMap<String, Object>(map); 261 } else { 262 data.putAll(map); 263 } 264 } 265 } 266 267 /** 268 * Puts the properties in the specified bean into this bean. 269 * 270 * @param other the map of properties to add, not null 271 */ 272 public void putAll(FlexiBean other) { 273 if (other.size() > 0) { 274 if (data == Collections.EMPTY_MAP) { 275 data = new HashMap<String, Object>(other.data); 276 } else { 277 data.putAll(other.data); 278 } 279 } 280 } 281 282 /** 283 * Removes a property. 284 * @param propertyName the property name, not empty 285 */ 286 public void remove(String propertyName) { 287 propertyRemove(propertyName); 288 } 289 290 /** 291 * Removes all properties. 292 */ 293 public void clear() { 294 if (data != Collections.EMPTY_MAP) { 295 data.clear(); 296 } 297 } 298 299 //----------------------------------------------------------------------- 300 /** 301 * Checks if the property exists. 302 * 303 * @param propertyName the property name, not empty 304 * @return true if the property exists 305 */ 306 public boolean propertyExists(String propertyName) { 307 return data.containsKey(propertyName); 308 } 309 310 /** 311 * Gets the value of the property. 312 * 313 * @param propertyName the property name, not empty 314 * @return the value of the property, may be null 315 */ 316 public Object propertyGet(String propertyName) { 317 if (propertyExists(propertyName) == false) { 318 throw new NoSuchElementException("Unknown property: " + propertyName); 319 } 320 return data.get(propertyName); 321 } 322 323 /** 324 * Sets the value of the property. 325 * 326 * @param propertyName the property name, not empty 327 * @param newValue the new value of the property, may be null 328 */ 329 public void propertySet(String propertyName, Object newValue) { 330 dataWritable().put(propertyName, newValue); 331 } 332 333 //----------------------------------------------------------------------- 334 @Override 335 public MetaBean metaBean() { 336 return metaBean; 337 } 338 339 @Override 340 public Property<Object> property(String name) { 341 if (propertyExists(name) == false) { 342 throw new NoSuchElementException("Unknown property: " + name); 343 } 344 return BasicProperty.of(this, FlexiMetaProperty.of(metaBean, name)); 345 } 346 347 @Override 348 public Set<String> propertyNames() { 349 return data.keySet(); 350 } 351 352 @Override 353 public void propertyDefine(String propertyName, Class<?> propertyType) { 354 // no need to define 355 } 356 357 @Override 358 public void propertyRemove(String propertyName) { 359 if (data != Collections.EMPTY_MAP) { 360 data.remove(propertyName); 361 } 362 } 363 364 //----------------------------------------------------------------------- 365 /** 366 * Returns a map representing the contents of the bean. 367 * 368 * @return a map representing the contents of the bean, not null 369 */ 370 public Map<String, Object> toMap() { 371 if (size() == 0) { 372 return Collections.emptyMap(); 373 } 374 return Collections.unmodifiableMap(new HashMap<String, Object>(data)); 375 } 376 377 //----------------------------------------------------------------------- 378 /** 379 * Compares this bean to another based on the property names and content. 380 * 381 * @param obj the object to compare to, null returns false 382 * @return true if equal 383 */ 384 @Override 385 public boolean equals(Object obj) { 386 if (obj == this) { 387 return true; 388 } 389 if (obj instanceof FlexiBean) { 390 FlexiBean other = (FlexiBean) obj; 391 return this.data.equals(other.data); 392 } 393 return super.equals(obj); 394 } 395 396 /** 397 * Returns a suitable hash code. 398 * 399 * @return a hash code 400 */ 401 @Override 402 public int hashCode() { 403 return data.hashCode(); 404 } 405 406 /** 407 * Returns a string that summarises the bean. 408 * <p> 409 * The string contains the class name and properties. 410 * 411 * @return a summary string, not null 412 */ 413 @Override 414 public String toString() { 415 return getClass().getSimpleName() + data.toString(); 416 } 417 418}