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}