    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 *      https://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}