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 */
017package org.apache.commons.beanutils.expression;
018
019/**
020 * Default Property Name Expression {@link Resolver} Implementation.
021 * <p>
022 * This class assists in resolving property names in the following five formats,
023 * with the layout of an identifying String in parentheses:
024 * <ul>
025 * <li><strong>Simple (<code>name</code>)</strong> - The specified
026 *     <code>name</code> identifies an individual property of a particular
027 *     JavaBean.  The name of the actual getter or setter method to be used
028 *     is determined using standard JavaBeans instrospection, so that (unless
029 *     overridden by a <code>BeanInfo</code> class, a property named "xyz"
030 *     will have a getter method named <code>getXyz()</code> or (for boolean
031 *     properties only) <code>isXyz()</code>, and a setter method named
032 *     <code>setXyz()</code>.</li>
033 * <li><strong>Nested (<code>name1.name2.name3</code>)</strong> The first
034 *     name element is used to select a property getter, as for simple
035 *     references above.  The object returned for this property is then
036 *     consulted, using the same approach, for a property getter for a
037 *     property named <code>name2</code>, and so on.  The property value that
038 *     is ultimately retrieved or modified is the one identified by the
039 *     last name element.</li>
040 * <li><strong>Indexed (<code>name[index]</code>)</strong> - The underlying
041 *     property value is assumed to be an array, or this JavaBean is assumed
042 *     to have indexed property getter and setter methods.  The appropriate
043 *     (zero-relative) entry in the array is selected.  <code>List</code>
044 *     objects are now also supported for read/write.  You simply need to define
045 *     a getter that returns the <code>List</code></li>
046 * <li><strong>Mapped (<code>name(key)</code>)</strong> - The JavaBean
047 *     is assumed to have an property getter and setter methods with an
048 *     additional attribute of type <code>java.lang.String</code>.</li>
049 * <li><strong>Combined (<code>name1.name2[index].name3(key)</code>)</strong> -
050 *     Combining mapped, nested, and indexed references is also
051 *     supported.</li>
052 * </ul>
053 *
054 * @since 1.8.0
055 */
056public class DefaultResolver implements Resolver {
057
058    private static final char NESTED        = '.';
059    private static final char MAPPED_START  = '(';
060    private static final char MAPPED_END    = ')';
061    private static final char INDEXED_START = '[';
062    private static final char INDEXED_END   = ']';
063
064    /**
065     * Constructs a new instance.
066     */
067    public DefaultResolver() {
068    }
069
070    /**
071     * Return the index value from the property expression or -1.
072     *
073     * @param expression The property expression
074     * @return The index value or -1 if the property is not indexed
075     * @throws IllegalArgumentException If the indexed property is illegally
076     * formed or has an invalid (non-numeric) value.
077     */
078    @Override
079    public int getIndex(final String expression) {
080        if (expression == null || expression.length() == 0) {
081            return -1;
082        }
083        for (int i = 0; i < expression.length(); i++) {
084            final char c = expression.charAt(i);
085            if (c == NESTED || c == MAPPED_START) {
086                return -1;
087            }
088            if (c == INDEXED_START) {
089                final int end = expression.indexOf(INDEXED_END, i);
090                if (end < 0) {
091                    throw new IllegalArgumentException("Missing End Delimiter");
092                }
093                final String value = expression.substring(i + 1, end);
094                if (value.length() == 0) {
095                    throw new IllegalArgumentException("No Index Value");
096                }
097                int index = 0;
098                try {
099                    index = Integer.parseInt(value, 10);
100                } catch (final Exception e) {
101                    throw new IllegalArgumentException("Invalid index value '"
102                            + value + "'");
103                }
104                return index;
105            }
106        }
107        return -1;
108    }
109
110    /**
111     * Return the map key from the property expression or <code>null</code>.
112     *
113     * @param expression The property expression
114     * @return The index value
115     * @throws IllegalArgumentException If the mapped property is illegally formed.
116     */
117    @Override
118    public String getKey(final String expression) {
119        if (expression == null || expression.length() == 0) {
120            return null;
121        }
122        for (int i = 0; i < expression.length(); i++) {
123            final char c = expression.charAt(i);
124            if (c == NESTED || c == INDEXED_START) {
125                return null;
126            }
127            if (c == MAPPED_START) {
128                final int end = expression.indexOf(MAPPED_END, i);
129                if (end < 0) {
130                    throw new IllegalArgumentException("Missing End Delimiter");
131                }
132                return expression.substring(i + 1, end);
133            }
134        }
135        return null;
136    }
137
138    /**
139     * Return the property name from the property expression.
140     *
141     * @param expression The property expression
142     * @return The property name
143     */
144    @Override
145    public String getProperty(final String expression) {
146        if (expression == null || expression.length() == 0) {
147            return expression;
148        }
149        for (int i = 0; i < expression.length(); i++) {
150            final char c = expression.charAt(i);
151            if (c == NESTED || c == MAPPED_START || c == INDEXED_START) {
152                return expression.substring(0, i);
153            }
154        }
155        return expression;
156    }
157
158    /**
159     * Indicates whether or not the expression
160     * contains nested property expressions or not.
161     *
162     * @param expression The property expression
163     * @return The next property expression
164     */
165    @Override
166    public boolean hasNested(final String expression) {
167        if (expression == null || expression.length() == 0) {
168            return false;
169        }
170        return remove(expression) != null;
171    }
172
173    /**
174     * Indicate whether the expression is for an indexed property or not.
175     *
176     * @param expression The property expression
177     * @return <code>true</code> if the expresion is indexed,
178     *  otherwise <code>false</code>
179     */
180    @Override
181    public boolean isIndexed(final String expression) {
182        if (expression == null || expression.length() == 0) {
183            return false;
184        }
185        for (int i = 0; i < expression.length(); i++) {
186            final char c = expression.charAt(i);
187            if (c == NESTED || c == MAPPED_START) {
188                return false;
189            }
190            if (c == INDEXED_START) {
191                return true;
192            }
193        }
194        return false;
195    }
196
197    /**
198     * Indicate whether the expression is for a mapped property or not.
199     *
200     * @param expression The property expression
201     * @return <code>true</code> if the expresion is mapped,
202     *  otherwise <code>false</code>
203     */
204    @Override
205    public boolean isMapped(final String expression) {
206        if (expression == null || expression.length() == 0) {
207            return false;
208        }
209        for (int i = 0; i < expression.length(); i++) {
210            final char c = expression.charAt(i);
211            if (c == NESTED || c == INDEXED_START) {
212                return false;
213            }
214            if (c == MAPPED_START) {
215                return true;
216            }
217        }
218        return false;
219    }
220
221    /**
222     * Extract the next property expression from the
223     * current expression.
224     *
225     * @param expression The property expression
226     * @return The next property expression
227     */
228    @Override
229    public String next(final String expression) {
230        if (expression == null || expression.length() == 0) {
231            return null;
232        }
233        boolean indexed = false;
234        boolean mapped  = false;
235        for (int i = 0; i < expression.length(); i++) {
236            final char c = expression.charAt(i);
237            if (indexed) {
238                if (c == INDEXED_END) {
239                    return expression.substring(0, i + 1);
240                }
241            } else if (mapped) {
242                if (c == MAPPED_END) {
243                    return expression.substring(0, i + 1);
244                }
245            } else {
246                switch (c) {
247                case NESTED:
248                    return expression.substring(0, i);
249                case MAPPED_START:
250                    mapped = true;
251                    break;
252                case INDEXED_START:
253                    indexed = true;
254                    break;
255                default:
256                    break;
257                }
258            }
259        }
260        return expression;
261    }
262
263    /**
264     * Remove the last property expresson from the
265     * current expression.
266     *
267     * @param expression The property expression
268     * @return The new expression value, with first property
269     * expression removed - null if there are no more expressions
270     */
271    @Override
272    public String remove(final String expression) {
273        if (expression == null || expression.length() == 0) {
274            return null;
275        }
276        final String property = next(expression);
277        if (expression.length() == property.length()) {
278            return null;
279        }
280        int start = property.length();
281        if (expression.charAt(start) == NESTED) {
282            start++;
283        }
284        return expression.substring(start);
285    }
286}