1 /*
2 * Copyright 2001-2013 Stephen Colebourne
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.joda.beans.impl.flexi;
17
18 import java.io.Serializable;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.Map;
22 import java.util.NoSuchElementException;
23 import java.util.Set;
24
25 import org.joda.beans.DynamicBean;
26 import org.joda.beans.MetaBean;
27 import org.joda.beans.Property;
28 import org.joda.beans.impl.BasicBean;
29 import org.joda.beans.impl.BasicProperty;
30
31 /**
32 * Implementation of a fully dynamic {@code Bean}.
33 * <p>
34 * Properties are dynamic, and can be added and removed at will from the map.
35 * The internal storage is created lazily to allow a flexi-bean to be used as
36 * a lightweight extension to another bean.
37 *
38 * @author Stephen Colebourne
39 */
40 public final class FlexiBean extends BasicBean implements DynamicBean, Serializable {
41 // Alternate way to implement this would be to create a list/map of real property
42 // objects which could then be properly typed
43
44 /** Serialization version. */
45 private static final long serialVersionUID = 1L;
46
47 /** The meta-bean. */
48 final FlexiMetaBean metaBean = new FlexiMetaBean(this); // CSIGNORE
49 /** The underlying data. */
50 volatile Map<String, Object> data = Collections.emptyMap();// CSIGNORE
51
52 /**
53 * Constructor.
54 */
55 public FlexiBean() {
56 }
57
58 /**
59 * Constructor that copies all the data entries from the specified bean.
60 *
61 * @param copyFrom the bean to copy from, not null
62 */
63 public FlexiBean(FlexiBean copyFrom) {
64 putAll(copyFrom.data);
65 }
66
67 //-----------------------------------------------------------------------
68 /**
69 * Gets the internal data map.
70 *
71 * @return the data, not null
72 */
73 private Map<String, Object> dataWritable() {
74 if (data == Collections.EMPTY_MAP) {
75 data = new HashMap<String, Object>();
76 }
77 return data;
78 }
79
80 //-----------------------------------------------------------------------
81 /**
82 * Gets the number of properties.
83 *
84 * @return the number of properties
85 */
86 public int size() {
87 return data.size();
88 }
89
90 /**
91 * Checks if the bean contains a specific property.
92 *
93 * @param propertyName the property name, null returns false
94 * @return true if the bean contains the property
95 */
96 public boolean contains(String propertyName) {
97 return propertyExists(propertyName);
98 }
99
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 }