View Javadoc
1   /**
2    * Copyright 2005-2015 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.container;
17  
18  import java.util.ArrayList;
19  import java.util.Collection;
20  import java.util.HashMap;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.regex.Matcher;
24  import java.util.regex.Pattern;
25  
26  import org.apache.commons.lang.StringUtils;
27  import org.kuali.rice.krad.datadictionary.parse.BeanTag;
28  import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
29  import org.kuali.rice.krad.datadictionary.parse.BeanTags;
30  import org.kuali.rice.krad.uif.UifConstants;
31  import org.kuali.rice.krad.uif.component.BindingInfo;
32  import org.kuali.rice.krad.uif.component.Component;
33  import org.kuali.rice.krad.uif.component.DataBinding;
34  import org.kuali.rice.krad.uif.control.CheckboxControl;
35  import org.kuali.rice.krad.uif.control.Control;
36  import org.kuali.rice.krad.uif.control.SelectControl;
37  import org.kuali.rice.krad.uif.control.TextControl;
38  import org.kuali.rice.krad.uif.element.Action;
39  import org.kuali.rice.krad.uif.element.Image;
40  import org.kuali.rice.krad.uif.element.Label;
41  import org.kuali.rice.krad.uif.element.Link;
42  import org.kuali.rice.krad.uif.element.Message;
43  import org.kuali.rice.krad.uif.field.DataField;
44  import org.kuali.rice.krad.uif.field.Field;
45  import org.kuali.rice.krad.uif.field.FieldGroup;
46  import org.kuali.rice.krad.uif.field.InputField;
47  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
48  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleUtils;
49  import org.kuali.rice.krad.uif.util.ComponentUtils;
50  import org.kuali.rice.krad.uif.util.LifecycleElement;
51  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
52  import org.kuali.rice.krad.uif.view.ExpressionEvaluator;
53  import org.kuali.rice.krad.uif.view.View;
54  import org.kuali.rice.krad.uif.widget.Inquiry;
55  import org.kuali.rice.krad.uif.widget.RichTable;
56  import org.kuali.rice.krad.uif.widget.Tooltip;
57  import org.kuali.rice.krad.util.KRADConstants;
58  import org.kuali.rice.krad.util.KRADUtils;
59  import org.kuali.rice.krad.web.form.UifFormBase;
60  
61  /**
62   * LightTable is a light-weight collection table implementation that supports a subset of features,
63   * Current known supported features are:
64   *
65   * <ul>
66   * <li>DataField</li>
67   * <li>InputField with TextControl, CheckboxControl, or single SelectControl</li>
68   * <li>MessageField</li>
69   * <li>LinkField</li>
70   * <li>ActionField</li>
71   * <li>ImageField</li>
72   * <li>most RichTable options</li>
73   * <li>FieldGroup containing only Actions, Image, Messages, or Links</li>
74   * <li>SpringEL for String properties on supported components only</li>
75   * <li>SpringEL specifically for the render property</li>
76   * </ul>
77   *
78   * Other features are not guaranteed to work, but may work at your own risk.  The intent of this table is to be a
79   * light-weight alternative to the fully featured table already available in KRAD and it is more suited to displaying
80   * large sets of simple data to the user.
81   *
82   * @author Kuali Rice Team (rice.collab@kuali.org)
83   */
84  @BeanTags({@BeanTag(name = "lightTable", parent = "Uif-LightTableGroup"),
85          @BeanTag(name = "lightTableSection", parent = "Uif-LightTableSection"),
86          @BeanTag(name = "lightTableSubSection", parent = "Uif-LightTableSubSection")})
87  public class LightTable extends GroupBase implements DataBinding {
88      private static final long serialVersionUID = -8930885219866835711L;
89  
90      private static final String VALUE_TOKEN = "@v@";
91      private static final String EXPRESSION_TOKEN = "@e@";
92      private static final String RENDER = "render";
93      private static final String ID_TOKEN = "@id@";
94      private static final String A_TOKEN = "@";
95      private static final String ROW_CLASS = "@rowClass@";
96      private static final String SORT_VALUE = "@sortVal";
97      private static final String SEPARATOR = "@@@";
98  
99      private String propertyName;
100     private BindingInfo bindingInfo;
101     private List<Label> headerLabels;
102     private RichTable richTable;
103     private Map<String, String> conditionalRowCssClasses;
104 
105     private Map<String, String> expressionConversionMap;
106     private List<String> initialComponentIds;
107     private Map<String, String> renderIdExpressionMap;
108     private boolean emptyTable;
109     private String currentColumnValue;
110 
111     /**
112      * LightTable constructor
113      */
114     public LightTable() {
115         expressionConversionMap = new HashMap<String, String>();
116         initialComponentIds = new ArrayList<String>();
117         renderIdExpressionMap = new HashMap<String, String>();
118     }
119 
120     /**
121      * Initialization override that sets up DataField value placeholders for parsing and populates the
122      * expressionConversionMap
123      */
124     @Override
125     public void performInitialization(Object model) {
126         super.performInitialization(model);
127         richTable.setForceLocalJsonData(true);
128 
129         //init binding info
130         if (bindingInfo != null) {
131             bindingInfo.setDefaults(ViewLifecycle.getView(), getPropertyName());
132         }
133         
134         List<? extends Component> items = getItems();
135         
136         ComponentUtils.clearAndAssignIds(items);
137 
138         //iterate over this collections items to initialize
139         for (Component item : this.getItems()) {
140             initialComponentIds.add(item.getId());
141 
142             //if data field, setup a forced placeholder value
143             if (item instanceof DataField) {
144                 ((DataField) item).setForcedValue(VALUE_TOKEN + item.getId() + VALUE_TOKEN);
145             }
146 
147             ///populate expression map
148             expressionConversionMap = buildExpressionMap(item, expressionConversionMap);
149         }
150     }
151 
152     /**
153      * Builds the expression map which contains "propertyName@@@id" and the expression.  Also fills the
154      * renderIdExpressionMap which contains all the component ids and expressions for render conditions, and overrides
155      * ids with a placeholder id.  This method is recursive for child components which match certain supported types.
156      *
157      * @param item the item to iterate on
158      * @param expressionMap the map holding the expressions for the items of this collection
159      * @return the expressionMap with expressions populated
160      */
161     protected Map<String, String> buildExpressionMap(Component item, Map<String, String> expressionMap) {
162         if (item == null) {
163             return expressionMap;
164         }
165 
166         List<String> toRemove = new ArrayList<String>();
167 
168         if (item.getExpressionGraph() != null && !item.getExpressionGraph().isEmpty()) {
169             for (String name : item.getExpressionGraph().keySet()) {
170                 processExpression(name, item, expressionMap, toRemove);
171             }
172         }
173 
174         //id placeholder
175         item.setId(ID_TOKEN + item.getId() + ID_TOKEN);
176 
177         if (item instanceof Group) {
178             ((Group) item).getLayoutManager().setId(ID_TOKEN + ((Group) item).getLayoutManager().getId() + ID_TOKEN);
179         }
180 
181         expressionMap = addChildExpressions(ViewLifecycleUtils.getElementsForLifecycle(item).values(), expressionMap);
182 
183         for (String name : toRemove) {
184             item.getExpressionGraph().remove(name);
185         }
186 
187         return expressionMap;
188     }
189 
190     /**
191      * Process the expression for the item by putting placeholder values in for String properties and adding markers
192      * for render expressions to the component; adds the original expression to the expressionMap
193      *
194      * @param name the property name
195      * @param item the component this expressio is on
196      * @param expressionMap the map to add expressions to
197      * @param toRemove the property name is added this map to be removed later
198      */
199     public void processExpression(String name, Component item, Map<String, String> expressionMap,
200             List<String> toRemove) {
201         Class<?> clazz = ObjectPropertyUtils.getPropertyType(item, name);
202         if (clazz == null) {
203             return;
204         }
205 
206         if (clazz.isAssignableFrom(String.class)) {
207             //add expressions for string properties only
208             expressionMap.put(name + SEPARATOR + item.getId(), item.getExpressionGraph().get(name));
209             toRemove.add(name);
210             ObjectPropertyUtils.setPropertyValue(item, name,
211                     EXPRESSION_TOKEN + name + SEPARATOR + item.getId() + EXPRESSION_TOKEN);
212 
213         } else if (name.endsWith(RENDER) && clazz.isAssignableFrom(boolean.class)) {
214             //setup render tokens to be able to determine where to remove content for render false, if needed
215             Component renderComponent = item;
216 
217             //check for nested render (child element)
218             if (!name.equals(RENDER)) {
219                 renderComponent = ObjectPropertyUtils.getPropertyValue(item, StringUtils.removeEnd(name, ".render"));
220             }
221 
222             //add render expression to the map
223             renderIdExpressionMap.put(renderComponent.getId(), item.getExpressionGraph().get(name));
224             toRemove.add(name);
225 
226             String renderMarker = A_TOKEN + RENDER + A_TOKEN + renderComponent.getId() + A_TOKEN;
227 
228             //setup pre render content token
229             String pre = renderComponent.getPreRenderContent() == null ? "" : renderComponent.getPreRenderContent();
230             renderComponent.setPreRenderContent(renderMarker + pre);
231 
232             //setup post render content token
233             String post = renderComponent.getPostRenderContent() == null ? "" : renderComponent.getPostRenderContent();
234             renderComponent.setPostRenderContent(post + renderMarker);
235 
236             //force render to true
237             ObjectPropertyUtils.setPropertyValue(item, name, true);
238         }
239     }
240 
241     /**
242      * Add expressions to the expression map for nested components of specific types
243      *
244      * @param components the child components
245      * @param expressionMap the map to add expressions to
246      * @return the map with child component expressions added
247      */
248     protected Map<String, String> addChildExpressions(Collection<? extends LifecycleElement> components,
249             Map<String, String> expressionMap) {
250         for (LifecycleElement comp : components) {
251             if (comp != null && (comp instanceof Action
252                     || comp instanceof Image
253                     || comp instanceof Message
254                     || comp instanceof Link
255                     || comp instanceof Inquiry
256                     || comp instanceof Group
257                     || comp instanceof Tooltip
258                     || comp instanceof InputField
259                     || comp instanceof CheckboxControl
260                     || comp instanceof TextControl
261                     || comp instanceof SelectControl)) {
262                 expressionMap = buildExpressionMap((Component) comp, expressionMap);
263             }
264         }
265 
266         return expressionMap;
267     }
268 
269     /**
270      * performFinalize override corrects the binding path for the DataFields and turns off rendering on some components
271      */
272     @Override
273     public void performFinalize(Object model, LifecycleElement parent) {
274         super.performFinalize(model, parent);
275 
276         headerLabels = new ArrayList<Label>();
277         for (Component item : this.getItems()) {
278             //get the header labels
279             if (item instanceof Field) {
280                 headerLabels.add(ComponentUtils.copy(((Field) item).getFieldLabel()));
281                 ((Field) item).getFieldLabel().setRender(false);
282             } else {
283                 headerLabels.add(null);
284             }
285 
286             if (item instanceof FieldGroup) {
287                 ((FieldGroup) item).getGroup().setValidationMessages(null);
288 
289             }
290 
291             if (item instanceof DataField) {
292                 ((DataField) item).getBindingInfo().setBindByNamePrefix(this.getBindingInfo().getBindingPath() + "[0]");
293             }
294 
295             if (item instanceof InputField) {
296                 ViewLifecycle.getViewPostMetadata().addAccessibleBindingPath(this.getBindingInfo().getBindingPath() + "[*]." + ((DataField) item).getPropertyName());
297             }
298         }
299 
300         Object collectionValue = ObjectPropertyUtils.getPropertyValue(model, bindingInfo.getBindingPath());
301 
302         //set emptyTable true if null, empty, or not valid collection
303         if (collectionValue == null || !(collectionValue instanceof Collection) ||
304                 ((Collection<?>) collectionValue).isEmpty()) {
305             emptyTable = true;
306         }
307     }
308 
309     /**
310      * Build the rows from the rowTemplate passed in.  This method uses regex to locate pieces of the row that need
311      * to be replaced with row specific content per row.
312      *
313      * @param view the view instance the table is being built within
314      * @param rowTemplate the first row of the collection in html generated from the ftl
315      * @param model the model
316      */
317     public void buildRows(View view, String rowTemplate, UifFormBase model) {
318         if (StringUtils.isBlank(rowTemplate)) {
319             return;
320         }
321 
322         rowTemplate = StringUtils.removeEnd(rowTemplate, ",");
323         rowTemplate = rowTemplate.replace("\n", "");
324         rowTemplate = rowTemplate.replace("\r", "");
325 
326         StringBuffer rows = new StringBuffer();
327         List<Object> collectionObjects = ObjectPropertyUtils.getPropertyValue(model, bindingInfo.getBindingPath());
328 
329         //uncheck any checked checkboxes globally for this row
330         rowTemplate = rowTemplate.replace("checked=\"checked\"", "");
331 
332         //init token patterns
333         Pattern idPattern = Pattern.compile(ID_TOKEN + "(.*?)" + ID_TOKEN);
334         Pattern expressionPattern = Pattern.compile(EXPRESSION_TOKEN + "(.*?)" + EXPRESSION_TOKEN);
335 
336         ExpressionEvaluator expressionEvaluator = ViewLifecycle.getExpressionEvaluator();
337         expressionEvaluator.initializeEvaluationContext(model);
338 
339         int lineIndex = 0;
340         for (Object obj : collectionObjects) {
341             //add line index to all ids
342             String row = idPattern.matcher(rowTemplate).replaceAll("$1" + UifConstants.IdSuffixes.LINE + lineIndex);
343 
344             //create the expanded context
345             Map<String, Object> expandedContext = new HashMap<String, Object>();
346             expandedContext.put(UifConstants.ContextVariableNames.LINE, obj);
347             expandedContext.put(UifConstants.ContextVariableNames.INDEX, lineIndex);
348             expandedContext.put(UifConstants.ContextVariableNames.VIEW, view);
349 
350             currentColumnValue = "";
351 
352             int itemIndex = 0;
353             for (Component item : this.getItems()) {
354                 //determine original id for this component
355                 String originalId = initialComponentIds.get(itemIndex);
356 
357                 //special DataField handling
358                 row = handleDataFieldInRow(item, obj, row, lineIndex, originalId);
359 
360                 //special InputField handling
361                 row = handleInputFieldInRow(item, obj, row, lineIndex, originalId);
362 
363                 //add item context
364                 if (item.getContext() != null) {
365                     expandedContext.putAll(item.getContext());
366                 }
367 
368                 //evaluate expressions found by the pattern
369                 row = evaluateAndReplaceExpressionValues(row, lineIndex, model, expandedContext, expressionPattern,
370                         expressionEvaluator);
371 
372                 if (currentColumnValue == null) {
373                     currentColumnValue = "";
374                 }
375 
376                 row = row.replace(SORT_VALUE + itemIndex + A_TOKEN, currentColumnValue);
377 
378                 itemIndex++;
379             }
380 
381             // get rowCss class
382             boolean isOdd = lineIndex % 2 == 0;
383             String rowCss = KRADUtils.generateRowCssClassString(conditionalRowCssClasses, lineIndex, isOdd,
384                     expandedContext, expressionEvaluator);
385 
386             row = row.replace("\"", "\\\"");
387             row = row.replace(ROW_CLASS, rowCss);
388             row = "{" + row + "},";
389 
390             //special render property expression handling
391             row = evaluateRenderExpressions(row, lineIndex, model, expandedContext, expressionEvaluator);
392 
393             //append row
394             rows.append(row);
395             lineIndex++;
396         }
397 
398         StringBuffer tableToolsColumnOptions = new StringBuffer("[");
399         for (int index = 0; index < this.getItems().size(); index++) {
400             String colOptions = richTable.constructTableColumnOptions(index, true, false, String.class, null);
401             tableToolsColumnOptions.append(colOptions + " , ");
402         }
403 
404         String aoColumnDefs = StringUtils.removeEnd(tableToolsColumnOptions.toString(), " , ") + "]";
405         Map<String, String> rtTemplateOptions = richTable.getTemplateOptions();
406         
407         if (rtTemplateOptions == null) {
408             richTable.setTemplateOptions(rtTemplateOptions = new HashMap<String, String>());
409         }
410         
411         rtTemplateOptions.put(UifConstants.TableToolsKeys.AO_COLUMN_DEFS, aoColumnDefs);
412 
413         // construct aaData option to set data in dataTable options (speed enhancement)
414         String aaData = StringUtils.removeEnd(rows.toString(), ",");
415         aaData = "[" + aaData + "]";
416         aaData = aaData.replace(KRADConstants.QUOTE_PLACEHOLDER, "\"");
417 
418         //set the aaData option on datatable for faster rendering
419         rtTemplateOptions.put(UifConstants.TableToolsKeys.AA_DATA, aaData);
420 
421         //make sure deferred rendering is forced whether set or not
422         rtTemplateOptions.put(UifConstants.TableToolsKeys.DEFER_RENDER,
423                 UifConstants.TableToolsValues.TRUE);
424     }
425 
426     /**
427      * Evaluate expressions and replace content found by the expressionPattern in the row
428      *
429      * @param row the row being modified
430      * @param index the line index
431      * @param model the model
432      * @param expandedContext the context to evaluate expressions against
433      * @param expressionPattern the expression pattern used to find expression tokens for value replacement
434      * @param expressionEvaluator the expression service to use for evaluation
435      * @return the modified row
436      */
437     protected String evaluateAndReplaceExpressionValues(String row, int index, Object model,
438             Map<String, Object> expandedContext, Pattern expressionPattern, ExpressionEvaluator expressionEvaluator) {
439 
440         Matcher matcher = expressionPattern.matcher(row);
441 
442         while (matcher.find()) {
443             String matchingGroup = matcher.group(1);
444             String expression = expressionConversionMap.get(matchingGroup);
445 
446             //adjust prefix for evaluation
447             expression = expression.replace(UifConstants.LINE_PATH_BIND_ADJUST_PREFIX,
448                     this.getBindingInfo().getBindingPath() + "[" + index + "].");
449 
450             //get expression result
451             Object value = expressionEvaluator.evaluateExpressionTemplate(expandedContext, expression);
452 
453             if (value != null) {
454                 row = row.replace(matcher.group(), value.toString());
455             } else {
456                 row = row.replace(matcher.group(), "");
457             }
458         }
459 
460         return row;
461     }
462 
463     /**
464      * Evaluates the render expressions for the row and removes the content if render is evaluated false
465      *
466      * @param row the row being modified
467      * @param index the line index
468      * @param model the model
469      * @param expandedContext the context to evaluate expressions against
470      * @param expressionEvaluator the expression service to use for evaluation
471      * @return the modified row
472      */
473     protected String evaluateRenderExpressions(String row, int index, Object model, Map<String, Object> expandedContext,
474             ExpressionEvaluator expressionEvaluator) {
475         for (String id : renderIdExpressionMap.keySet()) {
476             String expression = renderIdExpressionMap.get(id);
477 
478             //adjust prefix for evaluation
479             expression = expression.replace(UifConstants.LINE_PATH_BIND_ADJUST_PREFIX,
480                     this.getBindingInfo().getBindingPath() + "[" + index + "].");
481 
482             //get expression result
483             Object value = expressionEvaluator.evaluateExpressionTemplate(expandedContext, expression);
484 
485             String wrap = A_TOKEN + RENDER + A_TOKEN + id + A_TOKEN;
486 
487             if (value != null && value instanceof String && Boolean.parseBoolean((String) value) == false) {
488                 //do not render this component - remove content between render wrappers
489                 row = row.replaceAll(wrap + "(.|\\s)*?" + wrap, "");
490             } else {
491                 //remove render wrappers only - keep content
492                 row = row.replaceAll(wrap, "");
493             }
494         }
495 
496         return row;
497     }
498 
499     /**
500      * Special handling of the DataField in the row, replaces necessary content with row specific content
501      *
502      * @param item the item being processed
503      * @param obj the row's object model
504      * @param row the row in html
505      * @param index the current row index
506      * @param originalId the original id of the component item
507      * @return the updated row
508      */
509     protected String handleDataFieldInRow(Component item, Object obj, String row, int index, String originalId) {
510         if (!(item instanceof DataField)) {
511             return row;
512         }
513 
514         String currentValue = ObjectPropertyUtils.getPropertyValueAsText(obj, ((DataField) item).getPropertyName());
515 
516         if (currentValue == null) {
517             currentValue = "";
518         }
519 
520         //for readOnly DataFields replace the value marked with the value on the current object
521         row = row.replaceAll(VALUE_TOKEN + originalId + VALUE_TOKEN, currentValue);
522         currentColumnValue = currentValue;
523 
524         Inquiry dataFieldInquiry = ((DataField) item).getInquiry();
525         if (dataFieldInquiry != null && dataFieldInquiry.getInquiryParameters() != null
526                 && dataFieldInquiry.getInquiryLink() != null) {
527 
528             String inquiryLinkId = dataFieldInquiry.getInquiryLink().getId().replace(ID_TOKEN, "")
529                     + UifConstants.IdSuffixes.LINE + index;
530 
531             // process each Inquiry link parameter by replacing each in the inquiry url with their current value
532             for (String key : dataFieldInquiry.getInquiryParameters().keySet()) {
533                 String name = dataFieldInquiry.getInquiryParameters().get(key);
534 
535                 //omit the binding prefix from the key to get the path relative to the current object
536                 key = key.replace(((DataField) item).getBindingInfo().getBindByNamePrefix() + ".", "");
537 
538                 if (ObjectPropertyUtils.isReadableProperty(obj, key)) {
539                     String value = ObjectPropertyUtils.getPropertyValueAsText(obj, key);
540                     row = row.replaceFirst("(" + inquiryLinkId + "(.|\\s)*?" + name + ")=.*?([&|\"])",
541                             "$1=" + value + "$3");
542                 }
543             }
544         }
545 
546         return row;
547     }
548 
549     /**
550      * Special handling of the InputField in the row, replaces necessary content with row specific content
551      *
552      * @param item the item being processed
553      * @param obj the row's object model
554      * @param row the row in html
555      * @param index the current row index
556      * @param originalId the original id of the component item
557      * @return the updated row
558      */
559     protected String handleInputFieldInRow(Component item, Object obj, String row, int index, String originalId) {
560         if (!(item instanceof InputField) || ((InputField) item).getControl() == null) {
561             return row;
562         }
563 
564         Control control = ((InputField) item).getControl();
565 
566         //updates the name path to the current path for any instance this item's propertyName with
567         //a collection binding prefix
568         row = row.replace("name=\"" + ((InputField) item).getBindingInfo().getBindingPath() + "\"",
569                 "name=\"" + this.getBindingInfo().getBindingPath() + "[" + index + "]." + ((InputField) item)
570                         .getPropertyName() + "\"");
571 
572         Object value = ObjectPropertyUtils.getPropertyValue(obj, ((InputField) item).getPropertyName());
573         String stringValue = "";
574 
575         if (value == null) {
576             stringValue = "";
577         } else if (value.getClass().isAssignableFrom(boolean.class)) {
578             stringValue = "" + value;
579         } else if (!(value instanceof Collection)) {
580             stringValue = ObjectPropertyUtils.getPropertyValueAsText(obj, ((InputField) item).getPropertyName());
581         }
582 
583         String controlId = originalId + "_line" + index + UifConstants.IdSuffixes.CONTROL;
584 
585         if (control instanceof CheckboxControl && stringValue.equalsIgnoreCase("true")) {
586             //CheckboxControl handling - only replace if true with a checked attribute appended
587             row = row.replaceAll("(id(\\s)*?=(\\s)*?\"" + controlId + "\")", "$1 checked=\"checked\" ");
588         } else if (control instanceof TextControl) {
589             //TextControl handling - replace with
590             row = row.replaceAll("(id(\\s)*?=(\\s)*?\"" + controlId + "\"(.|\\s)*?value=\")(.|\\s)*?\"",
591                     "$1" + stringValue + "\"");
592         } else if (control instanceof SelectControl && !((SelectControl) control).isMultiple()) {
593             //SelectControl handling (single item only)
594             Pattern pattern = Pattern.compile("<select(\\s)*?id(\\s)*?=(\\s)*?\"" + controlId + "\"(.|\\s)*?</select>");
595             Matcher matcher = pattern.matcher(row);
596             String replacement = "";
597 
598             if (matcher.find()) {
599                 //remove selected from select options
600                 String selected = "selected=\"selected\"";
601                 replacement = matcher.group().replace(selected, "");
602 
603                 //put selected on only the selected option
604                 String selectedValue = "value=\"" + stringValue + "\"";
605                 replacement = replacement.replace(selectedValue, selectedValue + " " + selected);
606             }
607 
608             //replace the old select tag with the old one
609             if (StringUtils.isNotBlank(replacement)) {
610                 row = matcher.replaceAll(replacement);
611             }
612         }
613 
614         currentColumnValue = stringValue;
615 
616         return row;
617     }
618 
619     /**
620      * The propertyName of the list backing this collection
621      *
622      * @return the propertyName of this collection
623      */
624     @BeanTagAttribute
625     public String getPropertyName() {
626         return propertyName;
627     }
628 
629     /**
630      * Set the propertyName
631      *
632      * @param propertyName
633      */
634     public void setPropertyName(String propertyName) {
635         this.propertyName = propertyName;
636     }
637 
638     /**
639      * The bindingInfo for this collection table, containg the property path and other options
640      *
641      * @return the bindingInfo
642      */
643     @BeanTagAttribute
644     public BindingInfo getBindingInfo() {
645         return bindingInfo;
646     }
647 
648     /**
649      * Set the bindingInfo
650      *
651      * @param bindingInfo
652      */
653     public void setBindingInfo(BindingInfo bindingInfo) {
654         this.bindingInfo = bindingInfo;
655     }
656 
657     /**
658      * The labels for the header derived from the items of this collection (the fields)
659      *
660      * @return the header labels
661      */
662     public List<Label> getHeaderLabels() {
663         return headerLabels;
664     }
665 
666     /**
667      * The richTable widget definition for this table for setting dataTable javascript options
668      *
669      * @return the RichTable widget
670      */
671     @BeanTagAttribute
672     public RichTable getRichTable() {
673         return richTable;
674     }
675 
676     /**
677      * Set the richTable widget
678      *
679      * @param richTable
680      */
681     public void setRichTable(RichTable richTable) {
682         this.richTable = richTable;
683     }
684 
685     /**
686      * The row css classes for the rows of this layout
687      *
688      * <p>To set a css class on all rows, use "all" as a key.  To set a
689      * class for even rows, use "even" as a key, for odd rows, use "odd".
690      * Use a one-based index to target a specific row by index.  SpringEL can be
691      * used as a key and the expression will be evaluated; if evaluated to true, the
692      * class(es) specified will be applied.</p>
693      *
694      * @return a map which represents the css classes of the rows of this layout
695      */
696     @BeanTagAttribute
697     public Map<String, String> getConditionalRowCssClasses() {
698         return conditionalRowCssClasses;
699     }
700 
701     /**
702      * Set the conditionalRowCssClasses
703      *
704      * @param conditionalRowCssClasses
705      */
706     public void setConditionalRowCssClasses(Map<String, String> conditionalRowCssClasses) {
707         this.conditionalRowCssClasses = conditionalRowCssClasses;
708     }
709 
710     /**
711      * True if this table is empty, false otherwise
712      *
713      * @return true if the collection backing this table is empty
714      */
715     public boolean isEmptyTable() {
716         return emptyTable;
717     }
718 
719     public void setHeaderLabels(List<Label> headerLabels) {
720         this.headerLabels = headerLabels;
721     }
722 
723     public void setExpressionConversionMap(Map<String, String> expressionConversionMap) {
724         this.expressionConversionMap = expressionConversionMap;
725     }
726 
727     public Map<String, String> getExpressionConversionMap() {
728         return expressionConversionMap;
729     }
730 
731     public List<String> getInitialComponentIds() {
732         return initialComponentIds;
733     }
734 
735     public Map<String, String> getRenderIdExpressionMap() {
736         return renderIdExpressionMap;
737     }
738 
739     public void setInitialComponentIds(List<String> initialComponentIds) {
740         this.initialComponentIds = initialComponentIds;
741     }
742 
743     public void setRenderIdExpressionMap(Map<String, String> renderIdExpressionMap) {
744         this.renderIdExpressionMap = renderIdExpressionMap;
745     }
746 
747     public void setEmptyTable(boolean emptyTable) {
748         this.emptyTable = emptyTable;
749     }
750 
751     /**
752      *
753      * @return the current column value
754      */
755     @BeanTagAttribute(name = "currentColumnValue")
756     protected String getCurrentColumnValue() {
757         return currentColumnValue;
758     }
759 
760     /**
761      * Set the current column value
762      *
763      * @param currentColumnValue
764      */
765     protected void setCurrentColumnValue(String currentColumnValue) {
766         this.currentColumnValue = currentColumnValue;
767     }
768 }