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.jxpath.ri.model.beans;
019
020import java.beans.IndexedPropertyDescriptor;
021import java.beans.PropertyDescriptor;
022
023import org.apache.commons.jxpath.JXPathBeanInfo;
024import org.apache.commons.jxpath.JXPathContext;
025import org.apache.commons.jxpath.JXPathInvalidAccessException;
026import org.apache.commons.jxpath.ri.model.NodePointer;
027import org.apache.commons.jxpath.util.ValueUtils;
028
029/**
030 * Pointer to a property of a JavaBean.
031 */
032public class BeanPropertyPointer extends PropertyPointer {
033
034    private static final long serialVersionUID = -6008991447676468786L;
035
036    /**
037     * Uninitialized object marker.
038     */
039    private static final Object UNINITIALIZED = new Object();
040
041    /**
042     * The name of the currently selected property.
043     */
044    private String propertyName;
045
046    /**
047     * JavaBean info.
048     */
049    private final JXPathBeanInfo beanInfo;
050
051    /**
052     * The value of the currently selected property..
053     */
054    private Object baseValue = UNINITIALIZED;
055
056    /**
057     * If index == WHOLE_COLLECTION, the value of the property, otherwise the value of the index'th element of the collection represented by the property. If
058     * the property is not a collection, index should be zero and the value will be the property itself.
059     */
060    private Object value = UNINITIALIZED;
061
062    /**
063     * The names of all properties, sorted alphabetically.
064     */
065    private transient String[] names;
066
067    /**
068     * All PropertyDescriptors.
069     */
070    private transient PropertyDescriptor[] propertyDescriptors;
071
072    /**
073     * The property descriptor corresponding to the current property index.
074     */
075    private transient PropertyDescriptor propertyDescriptor;
076
077    /**
078     * Constructs a new BeanPropertyPointer.
079     *
080     * @param parent   parent pointer
081     * @param beanInfo describes the target property/ies.
082     */
083    public BeanPropertyPointer(final NodePointer parent, final JXPathBeanInfo beanInfo) {
084        super(parent);
085        this.beanInfo = beanInfo;
086    }
087
088    @Override
089    public NodePointer createPath(final JXPathContext context) {
090        if (getImmediateNode() == null) {
091            super.createPath(context);
092            baseValue = UNINITIALIZED;
093            value = UNINITIALIZED;
094        }
095        return this;
096    }
097
098    /**
099     * Gets the value of the currently selected property.
100     *
101     * @return Object value
102     */
103    @Override
104    public Object getBaseValue() {
105        if (baseValue == UNINITIALIZED) {
106            final PropertyDescriptor pd = getPropertyDescriptor();
107            if (pd == null) {
108                return null;
109            }
110            baseValue = ValueUtils.getValue(getBean(), pd);
111        }
112        return baseValue;
113    }
114
115    /**
116     * If index == WHOLE_COLLECTION, the value of the property, otherwise the value of the index'th element of the collection represented by the property. If
117     * the property is not a collection, index should be zero and the value will be the property itself.
118     *
119     * @return Object
120     */
121    @Override
122    public Object getImmediateNode() {
123        if (value == UNINITIALIZED) {
124            if (index == WHOLE_COLLECTION) {
125                value = ValueUtils.getValue(getBaseValue());
126            } else {
127                final PropertyDescriptor pd = getPropertyDescriptor();
128                if (pd == null) {
129                    value = null;
130                } else {
131                    value = ValueUtils.getValue(getBean(), pd, index);
132                }
133            }
134        }
135        return value;
136    }
137
138    /**
139     * If the property contains a collection, then the length of that collection, otherwise - 1.
140     *
141     * @return int length
142     */
143    @Override
144    public int getLength() {
145        final PropertyDescriptor pd = getPropertyDescriptor();
146        if (pd == null) {
147            return 1;
148        }
149        if (pd instanceof IndexedPropertyDescriptor) {
150            return ValueUtils.getIndexedPropertyLength(getBean(), (IndexedPropertyDescriptor) pd);
151        }
152        final int hint = ValueUtils.getCollectionHint(pd.getPropertyType());
153        if (hint == -1) {
154            return 1;
155        }
156        return super.getLength();
157    }
158
159    @Override
160    public int getPropertyCount() {
161        if (beanInfo.isAtomic()) {
162            return 0;
163        }
164        return getPropertyDescriptors().length;
165    }
166
167    /**
168     * Gets the property descriptor corresponding to the current property index.
169     *
170     * @return PropertyDescriptor
171     */
172    private PropertyDescriptor getPropertyDescriptor() {
173        if (propertyDescriptor == null) {
174            final int inx = getPropertyIndex();
175            if (inx == UNSPECIFIED_PROPERTY) {
176                propertyDescriptor = beanInfo.getPropertyDescriptor(propertyName);
177            } else {
178                final PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors();
179                if (inx >= 0 && inx < propertyDescriptors.length) {
180                    propertyDescriptor = propertyDescriptors[inx];
181                } else {
182                    propertyDescriptor = null;
183                }
184            }
185        }
186        return propertyDescriptor;
187    }
188
189    /**
190     * Gets all PropertyDescriptors.
191     *
192     * @return PropertyDescriptor[]
193     */
194    protected synchronized PropertyDescriptor[] getPropertyDescriptors() {
195        if (propertyDescriptors == null) {
196            propertyDescriptors = beanInfo.getPropertyDescriptors();
197        }
198        return propertyDescriptors;
199    }
200
201    /**
202     * Gets the name of the currently selected property.
203     *
204     * @return String property name
205     */
206    @Override
207    public String getPropertyName() {
208        if (propertyName == null) {
209            final PropertyDescriptor pd = getPropertyDescriptor();
210            if (pd != null) {
211                propertyName = pd.getName();
212            }
213        }
214        return propertyName != null ? propertyName : "*";
215    }
216
217    /**
218     * Gets the names of all properties, sorted alphabetically.
219     *
220     * @return String[]
221     */
222    @Override
223    public String[] getPropertyNames() {
224        if (names == null) {
225            final PropertyDescriptor[] pds = getPropertyDescriptors();
226            names = new String[pds.length];
227            for (int i = 0; i < names.length; i++) {
228                names[i] = pds[i].getName();
229            }
230        }
231        return names;
232    }
233
234    @Override
235    protected boolean isActualProperty() {
236        return getPropertyDescriptor() != null;
237    }
238
239    @Override
240    public boolean isCollection() {
241        final PropertyDescriptor pd = getPropertyDescriptor();
242        if (pd == null) {
243            return false;
244        }
245        if (pd instanceof IndexedPropertyDescriptor) {
246            return true;
247        }
248        final int hint = ValueUtils.getCollectionHint(pd.getPropertyType());
249        if (hint == -1) {
250            return false;
251        }
252        if (hint == 1) {
253            return true;
254        }
255        final Object value = getBaseValue();
256        return value != null && ValueUtils.isCollection(value);
257    }
258
259    /**
260     * This type of node is auxiliary.
261     *
262     * @return true
263     */
264    @Override
265    public boolean isContainer() {
266        return true;
267    }
268
269    @Override
270    public void remove() {
271        if (index == WHOLE_COLLECTION) {
272            setValue(null);
273        } else if (isCollection()) {
274            final Object o = getBaseValue();
275            final Object collection = ValueUtils.remove(getBaseValue(), index);
276            if (collection != o) {
277                ValueUtils.setValue(getBean(), getPropertyDescriptor(), collection);
278            }
279        } else if (index == 0) {
280            index = WHOLE_COLLECTION;
281            setValue(null);
282        }
283    }
284
285    @Override
286    public void setIndex(final int index) {
287        if (this.index == index) {
288            return;
289        }
290        // When dealing with a scalar, index == 0 is equivalent to
291        // WHOLE_COLLECTION, so do not change it.
292        if (this.index != WHOLE_COLLECTION || index != 0 || isCollection()) {
293            super.setIndex(index);
294            value = UNINITIALIZED;
295        }
296    }
297
298    /**
299     * Selects a property by its offset in the alphabetically sorted list.
300     *
301     * @param index property index
302     */
303    @Override
304    public void setPropertyIndex(final int index) {
305        if (propertyIndex != index) {
306            super.setPropertyIndex(index);
307            propertyName = null;
308            propertyDescriptor = null;
309            baseValue = UNINITIALIZED;
310            value = UNINITIALIZED;
311        }
312    }
313
314    /**
315     * Select a property by name.
316     *
317     * @param propertyName String name
318     */
319    @Override
320    public void setPropertyName(final String propertyName) {
321        setPropertyIndex(UNSPECIFIED_PROPERTY);
322        this.propertyName = propertyName;
323    }
324
325    /**
326     * If index == WHOLE_COLLECTION, change the value of the property, otherwise change the value of the index'th element of the collection represented by the
327     * property.
328     *
329     * @param value value to set
330     */
331    @Override
332    public void setValue(final Object value) {
333        final PropertyDescriptor pd = getPropertyDescriptor();
334        if (pd == null) {
335            throw new JXPathInvalidAccessException("Cannot set property: " + asPath() + " - no such property");
336        }
337        if (index == WHOLE_COLLECTION) {
338            ValueUtils.setValue(getBean(), pd, value);
339        } else {
340            ValueUtils.setValue(getBean(), pd, index, value);
341        }
342        this.value = value;
343    }
344}