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