org.springframework.web.servlet.tags.form.SelectedValueComparator Maven / Gradle / Ivy
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.web.servlet.tags.form;
import java.beans.PropertyEditor;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.servlet.support.BindStatus;
/**
* Utility class for testing whether a candidate value matches a {@link BindStatus#getValue data bound value}.
* Eagerly attempts to prove a comparison through a number of avenues to deal with issues such as instance
* inequality, logical (String-representation-based) equality and {@link PropertyEditor}-based comparison.
*
* Full support is provided for comparing arrays, {@link Collection Collections} and {@link Map Maps}.
*
*
Equality Contract
* For single-valued objects equality is first tested using standard {@link Object#equals Java equality}. As
* such, user code should endeavour to implement {@link Object#equals} to speed up the comparison process. If
* {@link Object#equals} returns {@code false} then an attempt is made at an
* {@link #exhaustiveCompare exhaustive comparison} with the aim being to prove equality rather
* than disprove it.
*
* Next, an attempt is made to compare the {@code String} representations of both the candidate and bound
* values. This may result in {@code true} in a number of cases due to the fact both values will be represented
* as {@code Strings} when shown to the user.
*
*
Next, if the candidate value is a {@code String}, an attempt is made to compare the bound value to
* result of applying the corresponding {@link PropertyEditor} to the candidate. This comparison may be
* executed twice, once against the direct {@code String} instances, and then against the {@code String}
* representations if the first comparison results in {@code false}.
*
* @author Rob Harrop
* @author Juergen Hoeller
* @since 2.0
*/
abstract class SelectedValueComparator {
/**
* Returns {@code true} if the supplied candidate value is equal to the value bound to
* the supplied {@link BindStatus}. Equality in this case differs from standard Java equality and
* is described in more detail here.
*/
public static boolean isSelected(BindStatus bindStatus, @Nullable Object candidateValue) {
// Check obvious equality matches with the candidate first,
// both with the rendered value and with the original value.
Object boundValue = bindStatus.getValue();
if (ObjectUtils.nullSafeEquals(boundValue, candidateValue)) {
return true;
}
Object actualValue = bindStatus.getActualValue();
if (actualValue != null && actualValue != boundValue &&
ObjectUtils.nullSafeEquals(actualValue, candidateValue)) {
return true;
}
if (actualValue != null) {
boundValue = actualValue;
}
else if (boundValue == null) {
return false;
}
// Non-null value but no obvious equality with the candidate value:
// go into more exhaustive comparisons.
boolean selected = false;
if (candidateValue != null) {
if (boundValue.getClass().isArray()) {
selected = collectionCompare(CollectionUtils.arrayToList(boundValue), candidateValue, bindStatus);
}
else if (boundValue instanceof Collection) {
selected = collectionCompare((Collection) boundValue, candidateValue, bindStatus);
}
else if (boundValue instanceof Map) {
selected = mapCompare((Map) boundValue, candidateValue, bindStatus);
}
}
if (!selected) {
selected = exhaustiveCompare(boundValue, candidateValue, bindStatus.getEditor(), null);
}
return selected;
}
private static boolean collectionCompare(
Collection boundCollection, Object candidateValue, BindStatus bindStatus) {
try {
if (boundCollection.contains(candidateValue)) {
return true;
}
}
catch (ClassCastException ex) {
// Probably from a TreeSet - ignore.
}
return exhaustiveCollectionCompare(boundCollection, candidateValue, bindStatus);
}
private static boolean mapCompare(Map boundMap, Object candidateValue, BindStatus bindStatus) {
try {
if (boundMap.containsKey(candidateValue)) {
return true;
}
}
catch (ClassCastException ex) {
// Probably from a TreeMap - ignore.
}
return exhaustiveCollectionCompare(boundMap.keySet(), candidateValue, bindStatus);
}
private static boolean exhaustiveCollectionCompare(
Collection collection, Object candidateValue, BindStatus bindStatus) {
Map convertedValueCache = new HashMap<>();
PropertyEditor editor = null;
boolean candidateIsString = (candidateValue instanceof String);
if (!candidateIsString) {
editor = bindStatus.findEditor(candidateValue.getClass());
}
for (Object element : collection) {
if (editor == null && element != null && candidateIsString) {
editor = bindStatus.findEditor(element.getClass());
}
if (exhaustiveCompare(element, candidateValue, editor, convertedValueCache)) {
return true;
}
}
return false;
}
private static boolean exhaustiveCompare(@Nullable Object boundValue, @Nullable Object candidate,
@Nullable PropertyEditor editor, @Nullable Map convertedValueCache) {
String candidateDisplayString = ValueFormatter.getDisplayString(candidate, editor, false);
if (boundValue != null && boundValue.getClass().isEnum()) {
Enum boundEnum = (Enum) boundValue;
String enumCodeAsString = ObjectUtils.getDisplayString(boundEnum.name());
if (enumCodeAsString.equals(candidateDisplayString)) {
return true;
}
String enumLabelAsString = ObjectUtils.getDisplayString(boundEnum.toString());
if (enumLabelAsString.equals(candidateDisplayString)) {
return true;
}
}
else if (ObjectUtils.getDisplayString(boundValue).equals(candidateDisplayString)) {
return true;
}
if (editor != null && candidate instanceof String) {
// Try PE-based comparison (PE should *not* be allowed to escape creating thread)
String candidateAsString = (String) candidate;
Object candidateAsValue;
if (convertedValueCache != null && convertedValueCache.containsKey(editor)) {
candidateAsValue = convertedValueCache.get(editor);
}
else {
editor.setAsText(candidateAsString);
candidateAsValue = editor.getValue();
if (convertedValueCache != null) {
convertedValueCache.put(editor, candidateAsValue);
}
}
if (ObjectUtils.nullSafeEquals(boundValue, candidateAsValue)) {
return true;
}
}
return false;
}
}