/** * (not-IBM-owned-copyright) * * 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: * * 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. * * --- * The first version of this file in this source control project * was contributed through * https://github.com/OpenNTF/XPagesExtensionLibrary/pull/14 * by Paul S Withers (https://github.com/paulswithers) * It was previously located in the other project: * https://github.com/OpenNTF/org.openntf.domino */ package com.ibm.xsp.extlib.component.picker.data; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import javax.faces.context.FacesContext; import javax.faces.el.ValueBinding; import com.ibm.commons.util.StringUtil; import com.ibm.xsp.complex.ValueBindingObjectImpl; import com.ibm.xsp.context.FacesContextEx; import com.ibm.xsp.util.DataPublisher; import com.ibm.xsp.util.DataPublisher.ShadowedObject; /** * @author Paul Withers * * MapValuePickerData, for use with the ValuePicker control */ public class MapValuePickerData extends ValueBindingObjectImpl implements IValuePickerData { private String searchType; private String searchRange; private Boolean caseInsensitive; private Map<String, String> options; private Boolean preventFiltering; /** * Enum for easy and consistent access to search type options * * @since org.openntf.domino.xsp 5.0.0 */ private static enum SearchType { SEARCH_STARTSWITH("startsWith"), SEARCH_EQUALS("equals"), SEARCH_CONTAINS("contains"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ private final String value_; private SearchType(final String value) { value_ = value; } public String getValue() { return value_; } } /** * Enum for easy access to the search styles - jumpTo and restrictToSearch */ private static enum SearchRange { SEARCH_RESTRICTTOSEARCH("restrictToSearch"), SEARCH_JUMPTO("jumpTo"); //$NON-NLS-1$ //$NON-NLS-2$ private final String value_; private SearchRange(final String value) { value_ = value; } public String getValue() { return value_; } } public MapValuePickerData() { } /** * Gets the options for the Value Picker, from the "options" property * * @return Map<String, String> of values * @since org.openntf.domino.xsp 4.5.0 */ @SuppressWarnings("unchecked") //$NON-NLS-1$ public Map<String, String> getOptions() { if (options != null) { return options; } ValueBinding vb = getValueBinding("options"); //$NON-NLS-1$ if (vb != null) { Object vbVal = vb.getValue(getFacesContext()); if( null != vbVal ){ return (Map<String, String>) vbVal; } } return null; } /** * By default delegates to {@link #getOptions()}, but available to override in subclasses. * * @return Map<String, String> of values */ public Map<String, String> getOptionsMap() { return getOptions(); } /** * Loads the options for the Value Picker * * @param options * Map<String, String> * @since org.openntf.domino.xsp 4.5.0 */ public void setOptions(final Map<String, String> options) { this.options = options; } /** * Gets the search type for the picker, from the "searchType" property * * @return String search type * @since org.openntf.domino.xsp 5.0.0 */ public String getSearchType() { if (searchType != null) { return searchType; } ValueBinding vb = getValueBinding("searchType"); //$NON-NLS-1$ if (vb != null) { return (String) vb.getValue(getFacesContext()); } return null; } /** * Loads the search type * * @param searchType * String search type * @since org.openntf.domino.xsp 5.0.0 */ public void setSearchType(final String searchType) { this.searchType = searchType; } /** * Gets the search style, from the "searchRange" property * * @return String search style * @since org.openntf.domino.xsp 5.0.0 */ public String getSearchRange() { if (searchRange != null) { return searchRange; } ValueBinding vb = getValueBinding("searchRange"); //$NON-NLS-1$ if (vb != null) { return (String) vb.getValue(getFacesContext()); } return null; } /** * Loads the search style * * @param searchStyle * String search style * @since org.openntf.domino.xsp 5.0.0 */ public void setSearchRange(final String searchStyle) { this.searchRange = searchStyle; } /** * Whether the options should be searched case insensitive or not * * @return boolean whether case insensitive * @since org.openntf.domino.xsp 5.0.0 */ public boolean isCaseInsensitive() { if (caseInsensitive != null) { return caseInsensitive; } ValueBinding vb = getValueBinding("caseInsensitive"); // $NON-NLS-1$ if (vb != null) { Boolean b = (Boolean) vb.getValue(getFacesContext()); if (b != null) { return b; } } return false; } /** * Loads whether the search should be done case inszensitive * * @param caseInsensitive * boolean * @since org.openntf.domino.xsp 5.0.0 */ public void setCaseInsensitive(final boolean caseInsensitive) { this.caseInsensitive = caseInsensitive; } /** * <p> * Return the value of the <code>preventFiltering</code> property. * </p> * <p> * Indicate if the list of values should be filtered with the value sent by the browser. * </p> * @designer.publicmethod */ public boolean isPreventFiltering() { if (null != preventFiltering) { return preventFiltering.booleanValue(); } ValueBinding binding = getValueBinding("preventFiltering"); // $NON-NLS-1$ if (binding != null) { Boolean result = (Boolean) binding.getValue(getFacesContext()); if( null != result ){ return result.booleanValue(); } } return false; } /** * <p> * Set the value of the <code>ignoreCase</code> property. * </p> * @designer.publicmethod */ public void setPreventFiltering(boolean preventFiltering) { this.preventFiltering = preventFiltering; } /* * (non-Javadoc) * * @see * com.ibm.xsp.extlib.component.picker.data.IPickerData#getSourceLabels() */ @Override public String[] getSourceLabels() { return null; } /* * (non-Javadoc) * * @see * com.ibm.xsp.extlib.component.picker.data.IPickerData#hasCapability(int) */ @Override public boolean hasCapability(final int capability) { if (capability == IValuePickerData.CAPABILITY_LABEL || capability == IValuePickerData.CAPABILITY_SEARCHBYKEY || capability == IValuePickerData.CAPABILITY_SEARCHLIST) return true; return false; } /* * This method appears to be the one that gets the entries for the picker * * (non-Javadoc) * * @see * com.ibm.xsp.extlib.component.picker.data.IPickerData#readEntries(com. * ibm.xsp.extlib.component.picker.data.IPickerOptions) */ @Override public IPickerResult readEntries(final IPickerOptions options) { String startKey = options.getStartKey(); String key = options.getKey(); int start = options.getStart(); int count = options.getCount(); int searchIndex = 0; LinkedHashMap<String, String> opts = filteredOptions(key, startKey, start, searchIndex); List<IPickerEntry> entries = new ArrayList<IPickerEntry>(); Iterator<String> it = opts.keySet().iterator(); while (it.hasNext()) { String mapKey = it.next(); entries.add(new SimplePickerResult.Entry(opts.get(mapKey), mapKey)); } return new SimplePickerResult(entries, count); } /** * Returns the filtered options, a subset of the options for the * MapValuePickerData * * @param key * String typeahead key * @param startKey * String search option * @param start * int not used * @param searchIndex * int not used * @return LinkedHashMap<String, String> of options filtered from the total * options * @since org.openntf.domino.xsp 4.5.0 */ private LinkedHashMap<String, String> filteredOptions(final String key, final String startKey, final int start, final int searchIndex) { boolean isPreventFiltering = isPreventFiltering(); String submittedKey = (null != key)? /*typeAhead submitted*/ key : /*PickerListSearch submitted*/startKey; LinkedHashMap<String, String> retVal = new LinkedHashMap<String, String>(); // Note, this pickerData is unusual in that most of them // do a startsWith filter for typeAheads but this is honoring the searchType option. if (!isPreventFiltering && StringUtil.isNotEmpty(submittedKey)) { // We've got a search key passed in, so search and add all remaining entries boolean isCaseInsensitive = isCaseInsensitive(); // Note, for case insensitive matching, it is not safe to do .toLowerCase // on the whole string, because of the Turkish dotless-i character. // Instead use the algorithm described in String.equalsIgnoreCase and // see http://www.i18nguy.com/unicode/turkish-i18n.html String computedSearchRange = getSearchRange(); // note, the org.openntf.domino.xsp implementation defaulted to searchTo, but this defaults to restrictToSearch SearchRange searchRangeEnum = SearchRange.SEARCH_JUMPTO.getValue().equals(computedSearchRange)? SearchRange.SEARCH_JUMPTO: /*default*/SearchRange.SEARCH_RESTRICTTOSEARCH; String searchType = getSearchType(); SearchType searchTypeEnum; if( SearchType.SEARCH_EQUALS.getValue().equals(searchType) ){ searchTypeEnum = SearchType.SEARCH_EQUALS; }else if(SearchType.SEARCH_CONTAINS.getValue().equals(searchType)){ searchTypeEnum = SearchType.SEARCH_CONTAINS; }else{ // startsWith (or old startsFrom value) searchTypeEnum = SearchType.SEARCH_STARTSWITH; } boolean doContainsLowerCaseCompare = false; String keyLowerCase = null; Pattern keyInsensitivePattern = null; if( isCaseInsensitive && SearchType.SEARCH_CONTAINS == searchTypeEnum){ // Note, defaulting to doing the slower Turkish-compliant search, // but there's an option to use the faster not-Turkish-compliant lowerCase compare. doContainsLowerCaseCompare = "false".equals( //$NON-NLS-1$ FacesContextEx.getCurrentInstance().getApplicationEx() .getProperty("xsp.picker.case_insensitive.locale.aware", /* defaultValue */"true")); //$NON-NLS-1$ //$NON-NLS-2$ if( doContainsLowerCaseCompare ){ keyLowerCase = submittedKey.toLowerCase(); }else{ // default String escapedKey = Pattern.quote(submittedKey); keyInsensitivePattern = Pattern.compile(escapedKey, Pattern.CASE_INSENSITIVE); } } Map<String, String> computedOptions = getOptionsMap(); Iterator<String> it = computedOptions.keySet().iterator(); boolean found = false; while (it.hasNext()) { String mapKey = it.next(); if (found) { retVal.put(mapKey, computedOptions.get(mapKey)); found = true; } else { boolean match; if (SearchType.SEARCH_EQUALS == searchTypeEnum ) { match = (!isCaseInsensitive)?StringUtil.equals(mapKey, submittedKey) : StringUtil.equalsIgnoreCase(mapKey, submittedKey); } else if (SearchType.SEARCH_CONTAINS == searchTypeEnum ) { if( !isCaseInsensitive ){ match = mapKey.contains(submittedKey); }else if( doContainsLowerCaseCompare ){ match = mapKey.toLowerCase().contains(keyLowerCase); }else{ // default case insensitive handling that can match the Turkish dotless-i match = keyInsensitivePattern.matcher(mapKey).find(); } } else { // SearchType.SEARCH_STARTFROM == searchTypeEnum match = (!isCaseInsensitive)? mapKey.startsWith(submittedKey) : mapKey.regionMatches(/* ignoreCase */true, 0, submittedKey,0,submittedKey.length()); } if (match) { retVal.put(mapKey, computedOptions.get(mapKey)); if (SearchRange.SEARCH_JUMPTO == searchRangeEnum ) { found = true; } } } } } else { // initial display without filtering, or filtering disabled List<ShadowedObject> shadowed = null; DataPublisher dataPublisher = null; if( isPreventFiltering ){ // computed options can reference requestScope.startValue to access the submittedKey. dataPublisher = FacesContextEx.getCurrentInstance().getDataPublisher(); shadowed = new ArrayList<DataPublisher.ShadowedObject>(); dataPublisher.pushObject(shadowed, "startValue", /*may be null*/submittedKey); //$NON-NLS-1$ } try{ Map<String, String> computedOptions = getOptionsMap(); retVal.putAll(computedOptions); }finally{ if( isPreventFiltering ){ dataPublisher.popObjects(shadowed); } } } return retVal; } /* * This method appears to be the one that is used for validation, to get an * entry based on a value or values in the relevant component. The ArrayList * only has values, so check values passed in and return those that exist in * the options * * (non-Javadoc) * * @see * com.ibm.xsp.extlib.component.picker.data.IPickerData#loadEntries(java * .lang.Object[], java.lang.String[]) */ @Override public List<IPickerEntry> loadEntries(final Object[] values, final String[] attributes) { // Note, this is method used by the PickerValidator, // and is checking against the values, not the keys, so it wouldn't be a caseInsensitive search. // Initially the list has a null entry for each of the values array, // and each index value will be replaced with an Entry if some map value // is found that matches that value. int length = (null == values)? 0 : values.length; List<IPickerEntry> entries = new ArrayList<IPickerEntry>(length); if (null != values) { Map<String, String> computedOptions = getOptionsMap(); for (int i = 0; i < values.length; i++) { String checkStr = values[i].toString(); entries.add(i, null); if (StringUtil.isNotEmpty(checkStr)) { Iterator<String> it = computedOptions.keySet().iterator(); while (it.hasNext()) { String mapKey = it.next(); String mapValue = computedOptions.get(mapKey); if (StringUtil.equals(checkStr, mapValue)) { entries.set(i, new SimplePickerResult.Entry(mapValue, mapKey)); break; // found for this value, continue the for loop to find for next value } } } } } return entries; } /* * (non-Javadoc) * * @see * com.ibm.xsp.complex.ValueBindingObjectImpl#restoreState(javax.faces.context * .FacesContext, java.lang.Object) */ @SuppressWarnings("unchecked") //$NON-NLS-1$ @Override public void restoreState(final FacesContext context, final Object state) { Object values[] = (Object[]) state; super.restoreState(context, values[0]); options = (Map<String, String>) values[1]; searchType = (String) values[2]; caseInsensitive = (Boolean) values[3]; searchRange = (String) values[4]; preventFiltering = (Boolean) values[5]; } /* * (non-Javadoc) * * @see * com.ibm.xsp.complex.ValueBindingObjectImpl#saveState(javax.faces.context * .FacesContext) */ @Override public Object saveState(final FacesContext context) { Object values[] = new Object[6]; values[0] = super.saveState(context); values[1] = options; values[2] = searchType; values[3] = caseInsensitive; values[4] = searchRange; values[5] = preventFiltering; return values; } }