package com.joe.jsf.view;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import javax.faces.event.ValueChangeEvent;
import javax.faces.model.SelectItem;
import org.apache.commons.beanutils.PropertyUtils;
import com.icesoft.faces.component.selectinputtext.SelectInputText;
/***
* Abstract bean for the icefaces autocomplete control. This can be embedded
* inside of a controller, once extended, to provide the necessary capabilities
* to do autocomplete querying, and state tracking.
*
* @author minger
*
* @param <ResultType>
*/
public abstract class AbstractAutocompleteBean<ResultType> {
private int querySize = 10;
private List<SelectItem> queryMatches;
private String selectedValueString = "";
private ResultType selectedValue;
private boolean emptySelectedValueValid = false;
private static final String NO_RESULTS_FOUND = "No Results Found";
private AutocompleteBeanListener<ResultType> listener;
/***
* Default constructor.
*/
protected AbstractAutocompleteBean() {
super();
}
protected AbstractAutocompleteBean( boolean emptySelectedValueValid) {
super();
this.emptySelectedValueValid=emptySelectedValueValid;
}
public void setListener(AutocompleteBeanListener<ResultType> listener) {
this.listener = listener;
}
/***
* Query for a particular partial string that was entered in the
* text box.
*
* @param partial The string that was entered in the text box.
* @param querySize The number of results to query for.
* @return The list of results which match the query. The list should
* be no larger than the requested querySize.
*/
protected abstract List<ResultType> query(String partial, int querySize);
/***
* Create a {@link SelectItem} from a given result.
*
* @param result
* @return
*/
protected abstract SelectItem createSelectItem(ResultType result);
public boolean isEmptySelectedValueValid() {
return emptySelectedValueValid;
}
public void setEmptySelectedValueValid(boolean emptySelectedValueValid) {
this.emptySelectedValueValid = emptySelectedValueValid;
}
/***
* The value change listener which handles the logic for when something is
* typed in the text box.
*
* @param event
*/
@SuppressWarnings("unchecked")
public void selectInputValueChanged(ValueChangeEvent event) {
if (event.getComponent() instanceof SelectInputText) {
// clear out the old list of matches
if(this.queryMatches != null) {
queryMatches.clear();
queryMatches = null;
}
// get the number of displayable records from the component
SelectInputText autoComplete =
(SelectInputText) event.getComponent();
// get the new value typed by component user.
String newWord = (String) event.getNewValue();
String oldWord = (String) event.getOldValue();
// grab the row size to limit the queries
if (autoComplete.getRows() > 0) {
this.querySize = autoComplete.getRows();
}
// check entered text few new matching values
if ("".equals(newWord)) {
// user cleared field, so present an empty selectbox
queryMatches = new ArrayList<SelectItem>(0);
if (listener != null) {
listener.autocompleteValueBlank();
}
}
else {
// get a new query using the new text entered
List<ResultType> matches = query(newWord, querySize);
queryMatches = new ArrayList<SelectItem>(matches.size());
for (ResultType result : matches) {
queryMatches.add(createSelectItem(result));
}
// if empty, then no results found...put a placeholder
if (queryMatches.isEmpty()) {
SelectItem noResult = new SelectItem("", NO_RESULTS_FOUND);
queryMatches.add(noResult);
}
}
// if there is a selected item and the value has changed to something different
// then find the object of the same name
if (autoComplete.getSelectedItem() != null) {
if (!autoComplete.getSelectedItem().getValue().equals("")) {
selectedValue = getQueryMatch(newWord);
if (listener != null) {
listener.autocompleteValueSelected(selectedValue);
}
}
else {
autoComplete.setValue("");
this.selectedValue = null;
this.selectedValueString = "";
queryMatches.clear();
queryMatches = null;
if (listener != null) {
listener.autocompleteValueSelectedNoResultsFound();
}
}
}
// if there was no selection we still want to see if a proper
// city was typed and update our selectedCity instance.
else {
ResultType tmp = getQueryMatch(newWord);
if (tmp != null){
selectedValue = tmp;
if (listener != null) {
listener.autocompleteValueSelected(selectedValue);
}
} else {
selectedValue = null;
}
}
// if there is a valid selectedValue, and the
if (selectedValue != null && newWord.equalsIgnoreCase(oldWord)) {
queryMatches.clear();
queryMatches = null;
}
}
}
/***
* The value change listener which handles the logic for when something is
* typed in the text box. Forces the showing and hiding of the autocomplete
* box. This method is for use with older Alineo pages that have stylesheet
* conflicts causing the autocomplete box to react badly and not hide when
* it is supposed to.
*
* @param event
*/
@SuppressWarnings("unchecked")
public void selectInputValueChangedForceHide(ValueChangeEvent event) {
if (event.getComponent() instanceof SelectInputText) {
// get the number of displayable records from the component
SelectInputText autoComplete =
(SelectInputText) event.getComponent();
// complete hack to get autocomplete elements in existing
// Alineo jspx pages to act correctly. Whenever any
// action is taken on the autocomplete component we
// want to set the display to block and make it so
// icefaces cannot overwrite it.
autoComplete.setStyle("display:block!important;");
// call the normal value change event
selectInputValueChanged(event);
// set the new list of matches unless a selected value,
// in which case we can ignore the dropdown altogether.
if (queryMatches == null || queryMatches.size() == 0 ||
((queryMatches.size() == 1) &&
(!NO_RESULTS_FOUND.equalsIgnoreCase(queryMatches.get(0).getLabel())))) {
// complete hack to get autocomplete elements in existing
// Alineo jspx pages to act correctly. When the user
// selects an item or types an element that matches one
// item, we can hide the autocomplete box.
autoComplete.setStyle("display:none;");
queryMatches = null;
}
}
}
/***
* Looks in the previously queried data for a match.
*
* @param label
* @return
*/
@SuppressWarnings("unchecked")
private ResultType getQueryMatch(String label) {
if (queryMatches != null) {
SelectItem selectItem;
for(int i = 0, max = queryMatches.size(); i < max; i++){
selectItem = (SelectItem)queryMatches.get(i);
if (selectItem.getLabel().compareToIgnoreCase(label) == 0 &&
!NO_RESULTS_FOUND.equalsIgnoreCase(selectItem.getLabel())) {
return (ResultType) selectItem.getValue();
}
}
}
return null;
}
/***
* See if the selected value string is a valid value. If it is, you can rely on calling
* {@link #getSelectedValue()} method after this to retrieve that value.
*
* @param nestedPropertyPath
* @return
*/
public boolean isSelectedValueStringValid(String nestedPropertyPath) {
if (this.emptySelectedValueValid && selectedValueString.trim().length()==0){
selectedValue=null;
return true;
}
List<ResultType> results = query(selectedValueString, 1);
if (results.size() == 1) {
ResultType r0 = results.get(0);
try {
String val = (String) PropertyUtils.getNestedProperty(r0, nestedPropertyPath);
if (val != null && !val.equalsIgnoreCase(NO_RESULTS_FOUND) && val.equalsIgnoreCase(selectedValueString)) {
selectedValue = r0;
return true;
}
}
catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
return false;
}
public String getSelectedValueString() {
return selectedValueString;
}
public void setSelectedValueString(String selectedValueString) {
this.selectedValueString = selectedValueString;
}
public ResultType getSelectedValue() {
return selectedValue;
}
public void setSelectedValue(ResultType selectedValue) {
this.selectedValue = selectedValue;
}
public List<SelectItem> getQueryMatches() {
return queryMatches;
}
public void reset() {
queryMatches = null;
selectedValueString = "";
selectedValue = null;
}
}