blob: 745d0c4eca7654de9cf2ea12ed9da81fa14645d7 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.beanutils2.expression;
/**
* Default Property Name Expression {@link Resolver} Implementation.
* <p>
* This class assists in resolving property names in the following five formats,
* with the layout of an identifying String in parentheses:
* <ul>
* <li><strong>Simple ({@code name})</strong> - The specified
* {@code name} identifies an individual property of a particular
* JavaBean. The name of the actual getter or setter method to be used
* is determined using standard JavaBeans introspection, so that (unless
* overridden by a {@code BeanInfo} class, a property named "xyz"
* will have a getter method named {@code getXyz()} or (for boolean
* properties only) {@code isXyz()}, and a setter method named
* {@code setXyz()}.</li>
* <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 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 property value that
* is ultimately retrieved or modified is the one identified by the
* last name element.</li>
* <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 property getter and setter methods. The appropriate
* (zero-relative) entry in the array is selected. {@code List}
* objects are now also supported for read/write. You simply need to define
* a getter that returns the {@code List}</li>
* <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 {@code java.lang.String}.</li>
* <li><strong>Combined ({@code name1.name2[index].name3(key)})</strong> -
* Combining mapped, nested, and indexed references is also
* supported.</li>
* </ul>
*
* @since 1.8.0
*/
public class DefaultResolver implements Resolver {
private static final char NESTED = '.';
private static final char MAPPED_START = '(';
private static final char MAPPED_END = ')';
private static final char INDEXED_START = '[';
private static final char INDEXED_END = ']';
/**
* Default Constructor.
*/
public DefaultResolver() {
}
/**
* Return the index value from the property expression or -1.
*
* @param expression The property expression
* @return The index value or -1 if the property is not indexed
* @throws IllegalArgumentException If the indexed property is illegally
* formed or has an invalid (non-numeric) value.
*/
@Override
public int getIndex(final String expression) {
if (expression == null || expression.length() == 0) {
return -1;
}
for (int i = 0; i < expression.length(); i++) {
final char c = expression.charAt(i);
if (c == NESTED || c == MAPPED_START) {
return -1;
} else if (c == INDEXED_START) {
final int end = expression.indexOf(INDEXED_END, i);
if (end < 0) {
throw new IllegalArgumentException("Missing End Delimiter");
}
final String value = expression.substring(i + 1, end);
if (value.length() == 0) {
throw new IllegalArgumentException("No Index Value");
}
int index = 0;
try {
index = Integer.parseInt(value, 10);
} catch (final Exception e) {
throw new IllegalArgumentException("Invalid index value '"
+ value + "'");
}
return index;
}
}
return -1;
}
/**
* Return the map key from the property expression or {@code null}.
*
* @param expression The property expression
* @return The index value
* @throws IllegalArgumentException If the mapped property is illegally formed.
*/
@Override
public String getKey(final String expression) {
if (expression == null || expression.length() == 0) {
return null;
}
for (int i = 0; i < expression.length(); i++) {
final char c = expression.charAt(i);
if (c == NESTED || c == INDEXED_START) {
return null;
} else if (c == MAPPED_START) {
final int end = expression.indexOf(MAPPED_END, i);
if (end < 0) {
throw new IllegalArgumentException("Missing End Delimiter");
}
return expression.substring(i + 1, end);
}
}
return null;
}
/**
* Return the property name from the property expression.
*
* @param expression The property expression
* @return The property name
*/
@Override
public String getProperty(final String expression) {
if (expression == null || expression.length() == 0) {
return expression;
}
for (int i = 0; i < expression.length(); i++) {
final char c = expression.charAt(i);
if (c == NESTED) {
return expression.substring(0, i);
} else if (c == MAPPED_START || c == INDEXED_START) {
return expression.substring(0, i);
}
}
return expression;
}
/**
* Indicates whether or not the expression
* contains nested property expressions or not.
*
* @param expression The property expression
* @return The next property expression
*/
@Override
public boolean hasNested(final String expression) {
if (expression == null || expression.length() == 0) {
return false;
}
return remove(expression) != null;
}
/**
* Indicate whether the expression is for an indexed property or not.
*
* @param expression The property expression
* @return {@code true} if the expression is indexed,
* otherwise {@code false}
*/
@Override
public boolean isIndexed(final String expression) {
if (expression == null || expression.length() == 0) {
return false;
}
for (int i = 0; i < expression.length(); i++) {
final char c = expression.charAt(i);
if (c == NESTED || c == MAPPED_START) {
return false;
} else if (c == INDEXED_START) {
return true;
}
}
return false;
}
/**
* Indicate whether the expression is for a mapped property or not.
*
* @param expression The property expression
* @return {@code true} if the expression is mapped,
* otherwise {@code false}
*/
@Override
public boolean isMapped(final String expression) {
if (expression == null || expression.length() == 0) {
return false;
}
for (int i = 0; i < expression.length(); i++) {
final char c = expression.charAt(i);
if (c == NESTED || c == INDEXED_START) {
return false;
} else if (c == MAPPED_START) {
return true;
}
}
return false;
}
/**
* Extract the next property expression from the
* current expression.
*
* @param expression The property expression
* @return The next property expression
*/
@Override
public String next(final String expression) {
if (expression == null || expression.length() == 0) {
return null;
}
boolean indexed = false;
boolean mapped = false;
for (int i = 0; i < expression.length(); i++) {
final char c = expression.charAt(i);
if (indexed) {
if (c == INDEXED_END) {
return expression.substring(0, i + 1);
}
} else if (mapped) {
if (c == MAPPED_END) {
return expression.substring(0, i + 1);
}
} else {
if (c == NESTED) {
return expression.substring(0, i);
} else if (c == MAPPED_START) {
mapped = true;
} else if (c == INDEXED_START) {
indexed = true;
}
}
}
return expression;
}
/**
* Remove the last property expression from the
* current expression.
*
* @param expression The property expression
* @return The new expression value, with first property
* expression removed - null if there are no more expressions
*/
@Override
public String remove(final String expression) {
if (expression == null || expression.length() == 0) {
return null;
}
final String property = next(expression);
if (expression.length() == property.length()) {
return null;
}
int start = property.length();
if (expression.charAt(start) == NESTED) {
start++;
}
return expression.substring(start);
}
}