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    }