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 */
016 package org.joda.beans.impl.flexi;
017
018 import java.io.Serializable;
019 import java.util.Collections;
020 import java.util.HashMap;
021 import java.util.Map;
022 import java.util.NoSuchElementException;
023 import java.util.Set;
024
025 import org.joda.beans.DynamicBean;
026 import org.joda.beans.MetaBean;
027 import org.joda.beans.Property;
028 import org.joda.beans.impl.BasicBean;
029 import 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 */
040 public 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);
049 /** The underlying data. */
050 volatile Map<String, Object> data = Collections.emptyMap();
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 propertyName the property name, not empty
114 * @param type the type to cast to, not null
115 * @return the value of the property, may be null
116 */
117 @SuppressWarnings("unchecked")
118 public <T> T get(String propertyName, Class<T> type) {
119 return (T) get(propertyName);
120 }
121
122 /**
123 * Gets the value of the property as a {@code String}.
124 * This will use {@link Object#toString()}.
125 *
126 * @param propertyName the property name, not empty
127 * @return the value of the property, may be null
128 */
129 public String getString(String propertyName) {
130 Object obj = get(propertyName);
131 return obj != null ? obj.toString() : null;
132 }
133
134 /**
135 * Gets the value of the property as a {@code boolean}.
136 *
137 * @param propertyName the property name, not empty
138 * @return the value of the property
139 * @throws ClassCastException if the value is not compatible
140 */
141 public boolean getBoolean(String propertyName) {
142 return (Boolean) get(propertyName);
143 }
144
145 /**
146 * Gets the value of the property as a {@code int}.
147 *
148 * @param propertyName the property name, not empty
149 * @return the value of the property
150 * @throws ClassCastException if the value is not compatible
151 */
152 public int getInt(String propertyName) {
153 return ((Number) get(propertyName)).intValue();
154 }
155
156 /**
157 * Gets the value of the property as a {@code int} using a default value.
158 *
159 * @param propertyName the property name, not empty
160 * @param defaultValue the default value for null
161 * @return the value of the property
162 * @throws ClassCastException if the value is not compatible
163 */
164 public int getInt(String propertyName, int defaultValue) {
165 Object obj = get(propertyName);
166 return obj != null ? ((Number) get(propertyName)).intValue() : defaultValue;
167 }
168
169 /**
170 * Gets the value of the property as a {@code long}.
171 *
172 * @param propertyName the property name, not empty
173 * @return the value of the property
174 * @throws ClassCastException if the value is not compatible
175 */
176 public long getLong(String propertyName) {
177 return ((Number) get(propertyName)).longValue();
178 }
179
180 /**
181 * Gets the value of the property as a {@code long} using a default value.
182 *
183 * @param propertyName the property name, not empty
184 * @param defaultValue the default value for null
185 * @return the value of the property
186 * @throws ClassCastException if the value is not compatible
187 */
188 public long getLong(String propertyName, long defaultValue) {
189 Object obj = get(propertyName);
190 return obj != null ? ((Number) get(propertyName)).longValue() : defaultValue;
191 }
192
193 /**
194 * Gets the value of the property as a {@code double}.
195 *
196 * @param propertyName the property name, not empty
197 * @return the value of the property
198 * @throws ClassCastException if the value is not compatible
199 */
200 public double getDouble(String propertyName) {
201 return ((Number) get(propertyName)).doubleValue();
202 }
203
204 /**
205 * Gets the value of the property as a {@code double} using a default value.
206 *
207 * @param propertyName the property name, not empty
208 * @param defaultValue the default value for null
209 * @return the value of the property
210 * @throws ClassCastException if the value is not compatible
211 */
212 public double getDouble(String propertyName, double defaultValue) {
213 Object obj = get(propertyName);
214 return obj != null ? ((Number) get(propertyName)).doubleValue() : defaultValue;
215 }
216
217 //-----------------------------------------------------------------------
218 /**
219 * Adds or updates a property returning {@code this} for chaining.
220 *
221 * @param propertyName the property name, not empty
222 * @param newValue the new value, may be null
223 * @return {@code this} for chaining, not null
224 */
225 public FlexiBean append(String propertyName, Object newValue) {
226 dataWritable().put(propertyName, newValue);
227 return this;
228 }
229
230 /**
231 * Adds or updates a property.
232 *
233 * @param propertyName the property name, not empty
234 * @param newValue the new value, may be null
235 */
236 public void set(String propertyName, Object newValue) {
237 dataWritable().put(propertyName, newValue);
238 }
239
240 /**
241 * Puts the property into this bean.
242 *
243 * @param propertyName the property name, not empty
244 * @param newValue the new value, may be null
245 * @return the old value of the property, may be null
246 */
247 public Object put(String propertyName, Object newValue) {
248 return dataWritable().put(propertyName, newValue);
249 }
250
251 /**
252 * Puts the properties in the specified map into this bean.
253 *
254 * @param map the map of properties to add, not null
255 */
256 public void putAll(Map<String, Object> map) {
257 if (map.size() > 0) {
258 if (data == Collections.EMPTY_MAP) {
259 data = new HashMap<String, Object>(map);
260 } else {
261 data.putAll(map);
262 }
263 }
264 }
265
266 /**
267 * Puts the properties in the specified bean into this bean.
268 *
269 * @param other the map of properties to add, not null
270 */
271 public void putAll(FlexiBean other) {
272 if (other.size() > 0) {
273 if (data == Collections.EMPTY_MAP) {
274 data = new HashMap<String, Object>(other.data);
275 } else {
276 data.putAll(other.data);
277 }
278 }
279 }
280
281 /**
282 * Removes a property.
283 * @param propertyName the property name, not empty
284 */
285 public void remove(String propertyName) {
286 propertyRemove(propertyName);
287 }
288
289 /**
290 * Removes all properties.
291 */
292 public void clear() {
293 if (data != Collections.EMPTY_MAP) {
294 data.clear();
295 }
296 }
297
298 //-----------------------------------------------------------------------
299 /**
300 * Checks if the property exists.
301 *
302 * @param propertyName the property name, not empty
303 * @return true if the property exists
304 */
305 public boolean propertyExists(String propertyName) {
306 return data.containsKey(propertyName);
307 }
308
309 /**
310 * Gets the value of the property.
311 *
312 * @param propertyName the property name, not empty
313 * @return the value of the property, may be null
314 */
315 public Object propertyGet(String propertyName) {
316 if (propertyExists(propertyName) == false) {
317 throw new NoSuchElementException("Unknown property: " + propertyName);
318 }
319 return data.get(propertyName);
320 }
321
322 /**
323 * Sets the value of the property.
324 *
325 * @param propertyName the property name, not empty
326 * @param newValue the new value of the property, may be null
327 */
328 public void propertySet(String propertyName, Object newValue) {
329 dataWritable().put(propertyName, newValue);
330 }
331
332 //-----------------------------------------------------------------------
333 @Override
334 public MetaBean metaBean() {
335 return metaBean;
336 }
337
338 @Override
339 public Property<Object> property(String name) {
340 if (propertyExists(name) == false) {
341 throw new NoSuchElementException("Unknown property: " + name);
342 }
343 return BasicProperty.of(this, FlexiMetaProperty.of(metaBean, name));
344 }
345
346 @Override
347 public Set<String> propertyNames() {
348 return data.keySet();
349 }
350
351 @Override
352 public void propertyDefine(String propertyName, Class<?> propertyType) {
353 // no need to define
354 }
355
356 @Override
357 public void propertyRemove(String propertyName) {
358 if (data != Collections.EMPTY_MAP) {
359 data.remove(propertyName);
360 }
361 }
362
363 //-----------------------------------------------------------------------
364 /**
365 * Returns a map representing the contents of the bean.
366 *
367 * @return a map representing the contents of the bean, not null
368 */
369 public Map<String, Object> toMap() {
370 if (size() == 0) {
371 return Collections.emptyMap();
372 }
373 return Collections.unmodifiableMap(new HashMap<String, Object>(data));
374 }
375
376 //-----------------------------------------------------------------------
377 /**
378 * Compares this bean to another based on the property names and content.
379 *
380 * @param obj the object to compare to, null returns false
381 * @return true if equal
382 */
383 @Override
384 public boolean equals(Object obj) {
385 if (obj == this) {
386 return true;
387 }
388 if (obj instanceof FlexiBean) {
389 FlexiBean other = (FlexiBean) obj;
390 return this.data.equals(other.data);
391 }
392 return super.equals(obj);
393 }
394
395 /**
396 * Returns a suitable hash code.
397 *
398 * @return a hash code
399 */
400 @Override
401 public int hashCode() {
402 return data.hashCode();
403 }
404
405 /**
406 * Returns a string that summarises the bean.
407 * <p>
408 * The string contains the class name and properties.
409 *
410 * @return a summary string, not null
411 */
412 @Override
413 public String toString() {
414 return getClass().getSimpleName() + data.toString();
415 }
416
417 }