/** * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at the * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Initial code contributed and copyrighted by<br> * frentix GmbH, http://www.frentix.com * <p> */ package org.olat.core.gui.components.textboxlist; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang.StringUtils; import org.json.JSONArray; import org.olat.core.CoreSpringFactory; import org.olat.core.dispatcher.mapper.Mapper; import org.olat.core.dispatcher.mapper.MapperService; import org.olat.core.dispatcher.mapper.manager.MapperKey; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.form.flexible.impl.FormBaseComponentImpl; import org.olat.core.gui.control.JSAndCSSAdder; import org.olat.core.gui.media.JSONMediaResource; import org.olat.core.gui.media.MediaResource; import org.olat.core.gui.render.ValidationResult; import org.olat.core.gui.translator.Translator; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.StringHelper; import org.olat.core.util.filter.impl.OWASPAntiSamyXSSFilter; /** * Description:<br> * component to use the TextBoxList from * http://www.interiders.com/2008/02/18/protomultiselect-02/ a bugfixed-version * (the one used in OLAT) stays here: * http://github.com/thewebfellas/protomultiselect * * note: march 2012, strentini merged some bugfixes from * https://github.com/garrytan/protomultiselect as of march 2012, this is * intended to be used always within a flexiform. * * <P> * Initial Date: 23.07.2010 <br> * * * * @author Roman Haag, roman.haag@frentix.com, http://www.frentix.com */ public abstract class TextBoxListComponent extends FormBaseComponentImpl { // if changed, do so also in multiselect.js! public static final String MORE_RESULTS_INDICATOR = "....."; public static final String INPUT_SUFFIX = "textboxlistinput"; private String inputHint; /* * holds the initial items (keyString is the caption! valueString is the * value) */ private Map<String, String> initialItems; /* * holds the current Set of items */ private Map<String, String> currentItems; /* * if set to true, multiselect.js will allow new values (apart from the ones * in the autocompletion set) */ private boolean allowNewValues = true; private boolean allowDuplicates = false; private static final OLog logger = Tracing.createLoggerFor(TextBoxListComponent.class); /* * the autoCompletion map. Key-String in the map is the "caption", * Value-String is the "value" */ private Map<String, String> autoCompletionValues; private ResultMapProvider provider; private MapperKey mapperKey; /* * the number of maxResults shown in the auto-completion list */ private int maxResults; /** * * @param name * @param inputHint * i18n key for an input hint, displayed when no autocompletion * result are shown and the pointer is in the input field * @param initialItems * set the already existing items. Map is "Key, Value" where * value could be null. so returned value is same as key. If you * don't want to set any intial items, just pass null or an empty * map * */ public TextBoxListComponent(String name, String inputHint, Map<String, String> initialItems, Translator translator) { super(name, translator); this.inputHint = inputHint; this.initialItems = initialItems; // check for null values if (this.initialItems == null) { this.initialItems = new HashMap<String, String>(); } // copy the initialItems into the "currentItems" map this.currentItems = new HashMap<String, String>(); for (Entry<String, String> initialMapEntry : this.initialItems.entrySet()) { currentItems.put(initialMapEntry.getKey(), initialMapEntry.getValue()); } } /** * @see org.olat.core.gui.components.Component#doDispatchRequest(org.olat.core.gui.UserRequest) */ @Override protected void doDispatchRequest(UserRequest ureq) { String inputId = "textboxlistinput" + getFormDispatchId(); String cmd = ureq.getParameter(inputId); if(cmd == null){ return; } setCmd(ureq, cmd); } public void setCmd(UserRequest ureq, String cmd) { if(!StringHelper.containsNonWhitespace(cmd)) { return; } String[] splitted = cmd.split(","); List<String> cleanedItemValues = new ArrayList<String>(); for (String item : splitted) { if (!StringUtils.isBlank(item)) cleanedItemValues.add(item.trim()); } if (!isAllowDuplicates()) removeDuplicates(cleanedItemValues); // update our current items currentItems = new HashMap<String, String>(); String caption = ""; for (String itemValue : cleanedItemValues) { caption = getCaptionForKnownValue(itemValue); if ("".equals(caption)) { currentItems.put(itemValue, itemValue); } else { currentItems.put(caption, itemValue); } } if (logger.isDebug()) logger.debug("doDispatchRequest --> firing textBoxListEvent with current items: " + cleanedItemValues); fireEvent(ureq, new TextBoxListEvent(cleanedItemValues)); } /** * * @param itemValue * @return */ private String getCaptionForKnownValue(String itemValue) { String caption = getInitialItemCaptionByValue(itemValue); if ("".equals(caption)) caption = getAutoCompletionItemCaptionByValue(itemValue); return caption; } /** * * @return */ private String getInitialItemCaptionByValue(String itemValue) { String initialItemCaption = ""; for (Entry<String, String> initialItemEntry : this.initialItems.entrySet()) { if (initialItemEntry.getValue().equals(itemValue)) initialItemCaption = initialItemEntry.getKey(); } return initialItemCaption; } /** * * @param itemValue * @return */ private String getAutoCompletionItemCaptionByValue(String itemValue) { String autoCompletionItemCaption = ""; Map<String,String> content = getAutoCompleteContent(); if (content == null) { return autoCompletionItemCaption; } for (Entry<String, String> autoCompletionItemEntry : content.entrySet()) { if (autoCompletionItemEntry.getValue().equals(itemValue)) { autoCompletionItemCaption = autoCompletionItemEntry.getKey(); } } return autoCompletionItemCaption; } /** * * @param arlList */ private static void removeDuplicates(List<String> arlList) { HashSet<String> h = new HashSet<String>(arlList); arlList.clear(); arlList.addAll(h); } /** * returns the input-hint <br /> * (the text that is displayed within the input-field on rendering) * * @return */ public String getInputHint() { return inputHint; } /** * @return Returns the provider. */ public ResultMapProvider getProvider() { return provider; } /** * returns the set of initial items * * @return */ public Map<String, String> getInitialItems() { return initialItems; } /** * returns the current Set of items in the textBoxList<br /> * (aka the current "bits"). The returned map contains the captions (as key) * and the values * * @return the current Items/"bits" of the TextBoxListComponent */ public Map<String, String> getCurrentItems() { return currentItems; } /** * returns the current List of item-values (without the captions) * * @return */ public List<String> getCurrentItemValues() { return new ArrayList<String>(currentItems.values()); } public void validate(UserRequest ureq, ValidationResult vr) { super.validate(ureq, vr); JSAndCSSAdder jsa = vr.getJsAndCSSAdder(); jsa.addRequiredStaticJsFile("js/jquery/tagsinput/bootstrap-tagsinput.min.js"); if (provider != null) { jsa.addRequiredStaticJsFile("js/jquery/typeahead/typeahead.bundle.min.js"); setMapper(ureq); } } /** * registers a OpenOLAT Mapper for this textBoxListComponent * * @param ureq */ private void setMapper(UserRequest ureq) { Mapper mapper = new Mapper() { public MediaResource handle(String relPath, HttpServletRequest request) { String lastInput = request.getParameter("term"); if (lastInput != null && lastInput.length() > 2) { Map<String, String> autoCContLoc = new HashMap<String, String>(); provider.getAutoCompleteContent(lastInput, autoCContLoc); setAutoCompleteContent(autoCContLoc); } JSONArray jsonResult = getAutoCompleteJSON(); return new JSONMediaResource(jsonResult, "UTF-8"); } }; mapperKey = CoreSpringFactory.getImpl(MapperService.class).register(ureq.getUserSession(), mapper); } /** * @param allowNewValues * if set to false, no values outside the autocompletion-result * are allowed to be entered. default is true */ public void setAllowNewValues(boolean allowNewValues) { this.allowNewValues = allowNewValues; } /** * @return Returns true if its allowed to enter new values */ public boolean isAllowNewValues() { return allowNewValues; } /** * @return Returns the allowDuplicates. */ public boolean isAllowDuplicates() { return allowDuplicates; } /** * @param allowDuplicates * if set to false (default) duplicates will be filtered * automatically */ public void setAllowDuplicates(boolean allowDuplicates) { this.allowDuplicates = allowDuplicates; } /** * @param autoCompletionValues * set a Map to use for autocompletion. Key in the map is the * "caption" */ public void setAutoCompleteContent(Map<String, String> autoCompletionValues) { this.autoCompletionValues = autoCompletionValues; } /** * set a Set of auto-completion values. ( caption will be equal to value, * use setAutoCompleteContent(Map<String, String> autoCompleteContent) if * you want to set custom values and captions ) * * @param autoCompletionValues * the Set of autoCompletionValues to use in this * TextBoxListComponent */ public void setAutoCompleteContent(Set<String> autoCompletionValues) { Map<String, String> map = new HashMap<String, String>(autoCompletionValues.size()); for (String string : autoCompletionValues) { map.put(string, string); } setAutoCompleteContent(map); } /** * @return Returns the autoCompletionValues as Map, where the Key-String is * the caption, the Value-String the value of the * auto-Completion-item */ public Map<String, String> getAutoCompleteContent() { return autoCompletionValues; } /** * returns the AutoCompletionContent as JSON String.<br /> * it will contain the captions and values * * @return the autoCompletionContent as JSON */ protected JSONArray getAutoCompleteJSON() { JSONArray array = new JSONArray(); try { Map<String, String> autoCont = getAutoCompleteContent(); if (autoCont != null) { for (String item : autoCont.keySet()) { array.put(StringHelper.escapeHtml(autoCont.get(item))); } } } catch (Exception e) { logger.error("", e); } return array; } public void setMapperProvider(ResultMapProvider provider) { this.provider = provider; } /** * @return Returns the mapperUri. */ public String getMapperUri() { return mapperKey.getUrl(); } /** * @return Return the maximal number of results shown by the auto-completion * list */ public int getMaxResults() { return maxResults; } /** * set the maximal number of results that should be shown by the * auto-completion list * * @param maxResults */ public void setMaxResults(int maxResults) { this.maxResults = maxResults; } /** * returns a the initialItems as comma-separated list.<br /> * * @return An HTML escaped list of item */ protected String getInitialItemsAsString() { Map<String, String> content = getInitialItems(); if (content != null && content.size() != 0) { //antisamy + escaping to prevent issue with the javascript code OWASPAntiSamyXSSFilter filter = new OWASPAntiSamyXSSFilter(); List<String> filtered = new ArrayList<String>(); for(String item:content.keySet()) { String antiItem = filter.filter(item); if(StringHelper.containsNonWhitespace(antiItem)) { filtered.add(antiItem); } } return StringUtils.join(filtered, ", "); } else return ""; } }