View Javadoc

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 }