001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.beanutils;
019
020import java.io.IOException;
021import java.io.ObjectInputStream;
022import java.io.ObjectOutputStream;
023import java.io.Serializable;
024import java.io.StreamCorruptedException;
025import java.util.List;
026import java.util.Map;
027
028/**
029 * <p>The metadata describing an individual property of a DynaBean.</p>
030 *
031 * <p>The meta contains an <em>optional</em> content type property ({@link #getContentType})
032 * for use by mapped and iterated properties.
033 * A mapped or iterated property may choose to indicate the type it expects.
034 * The DynaBean implementation may choose to enforce this type on its entries.
035 * Alternatively, an implementatin may choose to ignore this property.
036 * All keys for maps must be of type String so no meta data is needed for map keys.</p>
037 *
038 */
039
040public class DynaProperty implements Serializable {
041
042    private static final long serialVersionUID = 1L;
043    /*
044     * There are issues with serializing primitive class types on certain JVM versions
045     * (including java 1.3).
046     * This class uses a custom serialization implementation that writes an integer
047     * for these primitive class.
048     * This list of constants are the ones used in serialization.
049     * If these values are changed, then older versions will no longer be read correctly
050     */
051    private static final int BOOLEAN_TYPE = 1;
052    private static final int BYTE_TYPE = 2;
053    private static final int CHAR_TYPE = 3;
054    private static final int DOUBLE_TYPE = 4;
055    private static final int FLOAT_TYPE = 5;
056    private static final int INT_TYPE = 6;
057    private static final int LONG_TYPE = 7;
058    private static final int SHORT_TYPE = 8;
059
060    /** Property name */
061    protected String name;
062
063    /** Property type */
064    protected transient Class<?> type;
065
066    /** The <em>(optional)</em> type of content elements for indexed <code>DynaProperty</code> */
067    protected transient Class<?> contentType;
068
069    /**
070     * Construct a property that accepts any data type.
071     *
072     * @param name Name of the property being described
073     */
074    public DynaProperty(final String name) {
075
076        this(name, Object.class);
077
078    }
079    /**
080     * Construct a property of the specified data type.
081     *
082     * @param name Name of the property being described
083     * @param type Java class representing the property data type
084     */
085    public DynaProperty(final String name, final Class<?> type) {
086
087        this.name = name;
088        this.type = type;
089        if (type != null && type.isArray()) {
090            this.contentType = type.getComponentType();
091        }
092
093    }
094
095    /**
096     * Construct an indexed or mapped <code>DynaProperty</code> that supports (pseudo)-introspection
097     * of the content type.
098     *
099     * @param name Name of the property being described
100     * @param type Java class representing the property data type
101     * @param contentType Class that all indexed or mapped elements are instances of
102     */
103    public DynaProperty(final String name, final Class<?> type, final Class<?> contentType) {
104
105        this.name = name;
106        this.type = type;
107        this.contentType = contentType;
108
109    }
110    /**
111     * Checks this instance against the specified Object for equality. Overrides the
112     * default refererence test for equality provided by {@link java.lang.Object#equals(Object)}
113     * @param obj The object to compare to
114     * @return <code>true</code> if object is a dyna property with the same name
115     * type and content type, otherwise <code>false</code>
116     * @since 1.8.0
117     */
118    @Override
119    public boolean equals(final Object obj) {
120
121        boolean result = false;
122
123        result = obj == this;
124
125        if (!result && obj instanceof DynaProperty) {
126            final DynaProperty that = (DynaProperty) obj;
127            result =
128               (this.name == null ? that.name == null : this.name.equals(that.name)) &&
129               (this.type == null ? that.type == null : this.type.equals(that.type)) &&
130               (this.contentType == null ? that.contentType == null : this.contentType.equals(that.contentType));
131        }
132
133        return result;
134    }
135
136    /**
137     * Gets the <em>(optional)</em> type of the indexed content for <code>DynaProperty</code>'s
138     * that support this feature.
139     *
140     * <p>There are issues with serializing primitive class types on certain JVM versions
141     * (including java 1.3).
142     * Therefore, this field <strong>must not be serialized using the standard methods</strong>.</p>
143     *
144     * @return the Class for the content type if this is an indexed <code>DynaProperty</code>
145     * and this feature is supported. Otherwise null.
146     */
147    public Class<?> getContentType() {
148        return contentType;
149    }
150    /**
151     * Get the name of this property.
152     * @return the name of the property
153     */
154    public String getName() {
155        return this.name;
156    }
157
158    /**
159     * <p>Gets the Java class representing the data type of the underlying property
160     * values.</p>
161     *
162     * <p>There are issues with serializing primitive class types on certain JVM versions
163     * (including java 1.3).
164     * Therefore, this field <strong>must not be serialized using the standard methods</strong>.</p>
165     *
166     * <p><strong>Please leave this field as <code>transient</code></strong></p>
167     *
168     * @return the property type
169     */
170    public Class<?> getType() {
171        return this.type;
172    }
173
174    /**
175     * @return the hashcode for this dyna property
176     * @see java.lang.Object#hashCode
177     * @since 1.8.0
178     */
179    @Override
180    public int hashCode() {
181
182       int result = 1;
183
184       result = result * 31 + (name == null ? 0 : name.hashCode());
185       result = result * 31 + (type == null ? 0 : type.hashCode());
186       return result * 31 + (contentType == null ? 0 : contentType.hashCode());
187    }
188
189    /**
190     * Does this property represent an indexed value (ie an array or List)?
191     *
192     * @return <code>true</code> if the property is indexed (i.e. is a List or
193     * array), otherwise <code>false</code>
194     */
195    public boolean isIndexed() {
196
197        if (type == null) {
198            return false;
199        }
200        if (type.isArray() || List.class.isAssignableFrom(type)) {
201            return true;
202        }
203        return false;
204
205    }
206
207    /**
208     * Does this property represent a mapped value (ie a Map)?
209     *
210     * @return <code>true</code> if the property is a Map
211     * otherwise <code>false</code>
212     */
213    public boolean isMapped() {
214
215        if (type == null) {
216            return false;
217        }
218        return Map.class.isAssignableFrom(type);
219
220    }
221
222    /**
223     * Reads a class using safe encoding to workaround java 1.3 serialization bug.
224     */
225    private Class<?> readAnyClass(final ObjectInputStream in) throws IOException, ClassNotFoundException {
226        // read back type class safely
227        if (!in.readBoolean()) {
228            // it's another class
229            return (Class<?>) in.readObject();
230        }
231        // it's a type constant
232        switch (in.readInt()) {
233
234            case BOOLEAN_TYPE: return   Boolean.TYPE;
235            case BYTE_TYPE:    return      Byte.TYPE;
236            case CHAR_TYPE:    return Character.TYPE;
237            case DOUBLE_TYPE:  return    Double.TYPE;
238            case FLOAT_TYPE:   return     Float.TYPE;
239            case INT_TYPE:     return   Integer.TYPE;
240            case LONG_TYPE:    return      Long.TYPE;
241            case SHORT_TYPE:   return     Short.TYPE;
242            default:
243                // something's gone wrong
244                throw new StreamCorruptedException(
245                    "Invalid primitive type. "
246                    + "Check version of beanutils used to serialize is compatible.");
247
248        }
249    }
250
251    /**
252     * Reads field values for this object safely. There are issues with serializing primitive class types on certain JVM versions (including java 1.3). This
253     * method provides a workaround.
254     *
255     * @param in the content source.
256     * @throws IOException            when the stream data values are outside expected range
257     * @throws ClassNotFoundException Class of a serialized object cannot be found.
258     */
259    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
260        this.type = readAnyClass(in);
261        if (isMapped() || isIndexed()) {
262            this.contentType = readAnyClass(in);
263        }
264        // read other values
265        in.defaultReadObject();
266    }
267
268    /**
269     * Return a String representation of this Object.
270     *
271     * @return a String representation of the dyna property
272     */
273    @Override
274    public String toString() {
275        final StringBuilder sb = new StringBuilder("DynaProperty[name=");
276        sb.append(this.name);
277        sb.append(",type=");
278        sb.append(this.type);
279        if (isMapped() || isIndexed()) {
280            sb.append(" <").append(this.contentType).append(">");
281        }
282        sb.append("]");
283        return sb.toString();
284
285    }
286
287    /**
288     * Writes a class using safe encoding to workaround java 1.3 serialization bug.
289     *
290     * @throws IOException if I/O errors occur while writing to the underlying stream.
291     */
292    private void writeAnyClass(final Class<?> clazz, final ObjectOutputStream out) throws IOException {
293        // safely write out any class
294        int primitiveType = 0;
295        if (Boolean.TYPE.equals(clazz)) {
296            primitiveType = BOOLEAN_TYPE;
297        } else if (Byte.TYPE.equals(clazz)) {
298            primitiveType = BYTE_TYPE;
299        } else if (Character.TYPE.equals(clazz)) {
300            primitiveType = CHAR_TYPE;
301        } else if (Double.TYPE.equals(clazz)) {
302            primitiveType = DOUBLE_TYPE;
303        } else if (Float.TYPE.equals(clazz)) {
304            primitiveType = FLOAT_TYPE;
305        } else if (Integer.TYPE.equals(clazz)) {
306            primitiveType = INT_TYPE;
307        } else if (Long.TYPE.equals(clazz)) {
308            primitiveType = LONG_TYPE;
309        } else if (Short.TYPE.equals(clazz)) {
310            primitiveType = SHORT_TYPE;
311        }
312        if (primitiveType == 0) {
313            // then it's not a primitive type
314            out.writeBoolean(false);
315            out.writeObject(clazz);
316        } else {
317            // we'll write out a constant instead
318            out.writeBoolean(true);
319            out.writeInt(primitiveType);
320        }
321    }
322
323    /**
324     * Writes this object safely. There are issues with serializing primitive class types on certain JVM versions (including java 1.3). This method provides a
325     * workaround.
326     *
327     * @param out Where to write.
328     * @throws IOException if I/O errors occur while writing to the underlying stream.
329     */
330    private void writeObject(final ObjectOutputStream out) throws IOException {
331        writeAnyClass(this.type,out);
332        if (isMapped() || isIndexed()) {
333            writeAnyClass(this.contentType,out);
334        }
335        // write out other values
336        out.defaultWriteObject();
337    }
338}