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.dynabeans;
019
020import java.util.ArrayList;
021import java.util.Arrays;
022
023import org.apache.commons.beanutils.DynaBean;
024import org.apache.commons.beanutils.DynaClass;
025import org.apache.commons.beanutils.DynaProperty;
026import org.apache.commons.jxpath.JXPathTypeConversionException;
027import org.apache.commons.jxpath.ri.model.NodePointer;
028import org.apache.commons.jxpath.ri.model.beans.PropertyPointer;
029import org.apache.commons.jxpath.util.TypeUtils;
030import org.apache.commons.jxpath.util.ValueUtils;
031
032/**
033 * Pointer to a property of a {@link DynaBean}. If the target DynaBean is Serializable, so should this instance be.
034 */
035public class DynaBeanPropertyPointer extends PropertyPointer {
036
037    private static final String CLASS = "class";
038    private static final long serialVersionUID = 2094421509141267239L;
039
040    /**
041     * DynaBean.
042     */
043    private final DynaBean dynaBean;
044
045    /**
046     * The name of the currently selected property or "*" if none has been selected.
047     */
048    private String name;
049
050    /**
051     * The names of the included properties.
052     */
053    private String[] names;
054
055    /**
056     * Constructs a new DynaBeanPropertyPointer.
057     *
058     * @param parent   pointer
059     * @param dynaBean pointed
060     */
061    public DynaBeanPropertyPointer(final NodePointer parent, final DynaBean dynaBean) {
062        super(parent);
063        this.dynaBean = dynaBean;
064    }
065
066    /**
067     * Convert a value to the appropriate property type.
068     *
069     * @param value   to convert
070     * @param element whether this should be a collection element.
071     * @return conversion result
072     */
073    private Object convert(final Object value, final boolean element) {
074        final DynaClass dynaClass = dynaBean.getDynaClass();
075        final DynaProperty property = dynaClass.getDynaProperty(getPropertyName());
076        Class type = property.getType();
077        if (element) {
078            if (!type.isArray()) {
079                return value; // No need to convert
080            }
081            type = type.getComponentType();
082        }
083        try {
084            return TypeUtils.convert(value, type);
085        } catch (final Exception ex) {
086            final String string = value == null ? "null" : value.getClass().getName();
087            throw new JXPathTypeConversionException("Cannot convert value of class " + string + " to type " + type, ex);
088        }
089    }
090
091    @Override
092    public Object getBaseValue() {
093        return dynaBean.get(getPropertyName());
094    }
095
096    /**
097     * 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
098     * the property is not a collection, index should be zero and the value will be the property itself.
099     *
100     * @return Object
101     */
102    @Override
103    public Object getImmediateNode() {
104        final String name = getPropertyName();
105        if (name.equals("*")) {
106            return null;
107        }
108        Object value;
109        if (index == WHOLE_COLLECTION) {
110            value = ValueUtils.getValue(dynaBean.get(name));
111        } else if (isIndexedProperty()) {
112            // DynaClass at this point is not based on whether
113            // the property is indeed indexed, but rather on
114            // whether it is an array or List. Therefore
115            // the indexed set may fail.
116            try {
117                value = ValueUtils.getValue(dynaBean.get(name, index));
118            } catch (final ArrayIndexOutOfBoundsException ex) {
119                value = null;
120            } catch (final IllegalArgumentException ex) {
121                value = dynaBean.get(name);
122                value = ValueUtils.getValue(value, index);
123            }
124        } else {
125            value = dynaBean.get(name);
126            if (ValueUtils.isCollection(value)) {
127                value = ValueUtils.getValue(value, index);
128            } else if (index != 0) {
129                value = null;
130            }
131        }
132        return value;
133    }
134
135    @Override
136    public int getPropertyCount() {
137        return getPropertyNames().length;
138    }
139
140    /**
141     * Index of the currently selected property in the list of all properties sorted alphabetically.
142     *
143     * @return int
144     */
145    @Override
146    public int getPropertyIndex() {
147        if (propertyIndex == UNSPECIFIED_PROPERTY) {
148            final String[] names = getPropertyNames();
149            for (int i = 0; i < names.length; i++) {
150                if (names[i].equals(name)) {
151                    propertyIndex = i;
152                    name = null;
153                    break;
154                }
155            }
156        }
157        return super.getPropertyIndex();
158    }
159
160    /**
161     * Returns the name of the currently selected property or "*" if none has been selected.
162     *
163     * @return String
164     */
165    @Override
166    public String getPropertyName() {
167        if (name == null) {
168            final String[] names = getPropertyNames();
169            name = propertyIndex >= 0 && propertyIndex < names.length ? names[propertyIndex] : "*";
170        }
171        return name;
172    }
173
174    @Override
175    public String[] getPropertyNames() {
176        /* @todo do something about the sorting - LIKE WHAT? - MJB */
177        if (names == null) {
178            final DynaClass dynaClass = dynaBean.getDynaClass();
179            final DynaProperty[] dynaProperties = dynaClass.getDynaProperties();
180            final ArrayList<String> properties = new ArrayList<>(dynaProperties.length);
181            for (final DynaProperty element : dynaProperties) {
182                final String name = element.getName();
183                if (!CLASS.equals(name)) {
184                    properties.add(name);
185                }
186            }
187            names = properties.toArray(new String[properties.size()]);
188            Arrays.sort(names);
189        }
190        return names;
191    }
192
193    /**
194     * Returns true if the bean has the currently selected property.
195     *
196     * @return boolean
197     */
198    @Override
199    protected boolean isActualProperty() {
200        final DynaClass dynaClass = dynaBean.getDynaClass();
201        return dynaClass.getDynaProperty(getPropertyName()) != null;
202    }
203
204    /**
205     * This type of node is auxiliary.
206     *
207     * @return true
208     */
209    @Override
210    public boolean isContainer() {
211        return true;
212    }
213
214    /**
215     * Tests whether the property referenced is an indexed property.
216     *
217     * @return boolean
218     */
219    protected boolean isIndexedProperty() {
220        final DynaClass dynaClass = dynaBean.getDynaClass();
221        final DynaProperty property = dynaClass.getDynaProperty(name);
222        return property.isIndexed();
223    }
224
225    @Override
226    public void remove() {
227        if (index == WHOLE_COLLECTION) {
228            dynaBean.set(getPropertyName(), null);
229        } else if (isIndexedProperty()) {
230            dynaBean.set(getPropertyName(), index, null);
231        } else if (isCollection()) {
232            final Object collection = ValueUtils.remove(getBaseValue(), index);
233            dynaBean.set(getPropertyName(), collection);
234        } else if (index == 0) {
235            dynaBean.set(getPropertyName(), null);
236        }
237    }
238
239    /**
240     * Index a property by its index in the list of all properties sorted alphabetically.
241     *
242     * @param index to set
243     */
244    @Override
245    public void setPropertyIndex(final int index) {
246        if (propertyIndex != index) {
247            super.setPropertyIndex(index);
248            name = null;
249        }
250    }
251
252    /**
253     * Select a property by name.
254     *
255     * @param propertyName to select
256     */
257    @Override
258    public void setPropertyName(final String propertyName) {
259        setPropertyIndex(UNSPECIFIED_PROPERTY);
260        this.name = propertyName;
261    }
262
263    /**
264     * Sets an indexed value.
265     *
266     * @param index to change
267     * @param value to set
268     */
269    private void setValue(final int index, final Object value) {
270        if (index == WHOLE_COLLECTION) {
271            dynaBean.set(getPropertyName(), convert(value, false));
272        } else if (isIndexedProperty()) {
273            dynaBean.set(getPropertyName(), index, convert(value, true));
274        } else {
275            final Object baseValue = dynaBean.get(getPropertyName());
276            ValueUtils.setValue(baseValue, index, value);
277        }
278    }
279
280    /**
281     * 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
282     * property.
283     *
284     * @param value to set
285     */
286    @Override
287    public void setValue(final Object value) {
288        setValue(index, value);
289    }
290}