View Javadoc
1   /**
2    * Copyright 2005-2014 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 = "lightTableGroup-bean", parent = "Uif-LightTableGroup"),
85          @BeanTag(name = "lightTableSection-bean", parent = "Uif-LightTableSection"),
86          @BeanTag(name = "lightTableSubSection-bean", 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 
296         Object collectionValue = ObjectPropertyUtils.getPropertyValue(model, bindingInfo.getBindingPath());
297 
298         //set emptyTable true if null, empty, or not valid collection
299         if (collectionValue == null || !(collectionValue instanceof Collection) ||
300                 ((Collection<?>) collectionValue).isEmpty()) {
301             emptyTable = true;
302         }
303     }
304 
305     /**
306      * Build the rows from the rowTemplate passed in.  This method uses regex to locate pieces of the row that need
307      * to be replaced with row specific content per row.
308      *
309      * @param view the view instance the table is being built within
310      * @param rowTemplate the first row of the collection in html generated from the ftl
311      * @param model the model
312      */
313     public void buildRows(View view, String rowTemplate, UifFormBase model) {
314         if (StringUtils.isBlank(rowTemplate)) {
315             return;
316         }
317 
318         rowTemplate = StringUtils.removeEnd(rowTemplate, ",");
319         rowTemplate = rowTemplate.replace("\n", "");
320         rowTemplate = rowTemplate.replace("\r", "");
321 
322         StringBuffer rows = new StringBuffer();
323         List<Object> collectionObjects = ObjectPropertyUtils.getPropertyValue(model, bindingInfo.getBindingPath());
324 
325         //uncheck any checked checkboxes globally for this row
326         rowTemplate = rowTemplate.replace("checked=\"checked\"", "");
327 
328         //init token patterns
329         Pattern idPattern = Pattern.compile(ID_TOKEN + "(.*?)" + ID_TOKEN);
330         Pattern expressionPattern = Pattern.compile(EXPRESSION_TOKEN + "(.*?)" + EXPRESSION_TOKEN);
331 
332         ExpressionEvaluator expressionEvaluator = ViewLifecycle.getExpressionEvaluator();
333         expressionEvaluator.initializeEvaluationContext(model);
334 
335         int lineIndex = 0;
336         for (Object obj : collectionObjects) {
337             //add line index to all ids
338             String row = idPattern.matcher(rowTemplate).replaceAll("$1" + UifConstants.IdSuffixes.LINE + lineIndex);
339 
340             //create the expanded context
341             Map<String, Object> expandedContext = new HashMap<String, Object>();
342             expandedContext.put(UifConstants.ContextVariableNames.LINE, obj);
343             expandedContext.put(UifConstants.ContextVariableNames.INDEX, lineIndex);
344             expandedContext.put(UifConstants.ContextVariableNames.VIEW, view);
345 
346             currentColumnValue = "";
347 
348             int itemIndex = 0;
349             for (Component item : this.getItems()) {
350                 //determine original id for this component
351                 String originalId = initialComponentIds.get(itemIndex);
352 
353                 //special DataField handling
354                 row = handleDataFieldInRow(item, obj, row, lineIndex, originalId);
355 
356                 //special InputField handling
357                 row = handleInputFieldInRow(item, obj, row, lineIndex, originalId);
358 
359                 //add item context
360                 if (item.getContext() != null) {
361                     expandedContext.putAll(item.getContext());
362                 }
363 
364                 //evaluate expressions found by the pattern
365                 row = evaluateAndReplaceExpressionValues(row, lineIndex, model, expandedContext, expressionPattern,
366                         expressionEvaluator);
367 
368                 if (currentColumnValue == null) {
369                     currentColumnValue = "";
370                 }
371 
372                 row = row.replace(SORT_VALUE + itemIndex + A_TOKEN, currentColumnValue);
373 
374                 itemIndex++;
375             }
376 
377             // get rowCss class
378             boolean isOdd = lineIndex % 2 == 0;
379             String rowCss = KRADUtils.generateRowCssClassString(conditionalRowCssClasses, lineIndex, isOdd,
380                     expandedContext, expressionEvaluator);
381 
382             row = row.replace("\"", "\\\"");
383             row = row.replace(ROW_CLASS, rowCss);
384             row = "{" + row + "},";
385 
386             //special render property expression handling
387             row = evaluateRenderExpressions(row, lineIndex, model, expandedContext, expressionEvaluator);
388 
389             //append row
390             rows.append(row);
391             lineIndex++;
392         }
393 
394         StringBuffer tableToolsColumnOptions = new StringBuffer("[");
395         for (int index = 0; index < this.getItems().size(); index++) {
396             String colOptions = richTable.constructTableColumnOptions(index, true, false, String.class, null);
397             tableToolsColumnOptions.append(colOptions + " , ");
398         }
399 
400         String aoColumnDefs = StringUtils.removeEnd(tableToolsColumnOptions.toString(), " , ") + "]";
401         Map<String, String> rtTemplateOptions = richTable.getTemplateOptions();
402         
403         if (rtTemplateOptions == null) {
404             richTable.setTemplateOptions(rtTemplateOptions = new HashMap<String, String>());
405         }
406         
407         rtTemplateOptions.put(UifConstants.TableToolsKeys.AO_COLUMN_DEFS, aoColumnDefs);
408 
409         // construct aaData option to set data in dataTable options (speed enhancement)
410         String aaData = StringUtils.removeEnd(rows.toString(), ",");
411         aaData = "[" + aaData + "]";
412         aaData = aaData.replace(KRADConstants.QUOTE_PLACEHOLDER, "\"");
413 
414         //set the aaData option on datatable for faster rendering
415         rtTemplateOptions.put(UifConstants.TableToolsKeys.AA_DATA, aaData);
416 
417         //make sure deferred rendering is forced whether set or not
418         rtTemplateOptions.put(UifConstants.TableToolsKeys.DEFER_RENDER,
419                 UifConstants.TableToolsValues.TRUE);
420     }
421 
422     /**
423      * Evaluate expressions and replace content found by the expressionPattern in the row
424      *
425      * @param row the row being modified
426      * @param index the line index
427      * @param model the model
428      * @param expandedContext the context to evaluate expressions against
429      * @param expressionPattern the expression pattern used to find expression tokens for value replacement
430      * @param expressionEvaluator the expression service to use for evaluation
431      * @return the modified row
432      */
433     protected String evaluateAndReplaceExpressionValues(String row, int index, Object model,
434             Map<String, Object> expandedContext, Pattern expressionPattern, ExpressionEvaluator expressionEvaluator) {
435 
436         Matcher matcher = expressionPattern.matcher(row);
437 
438         while (matcher.find()) {
439             String matchingGroup = matcher.group(1);
440             String expression = expressionConversionMap.get(matchingGroup);
441 
442             //adjust prefix for evaluation
443             expression = expression.replace(UifConstants.LINE_PATH_BIND_ADJUST_PREFIX,
444                     this.getBindingInfo().getBindingPath() + "[" + index + "].");
445 
446             //get expression result
447             Object value = expressionEvaluator.evaluateExpressionTemplate(expandedContext, expression);
448 
449             if (value != null) {
450                 row = row.replace(matcher.group(), value.toString());
451             } else {
452                 row = row.replace(matcher.group(), "");
453             }
454         }
455 
456         return row;
457     }
458 
459     /**
460      * Evaluates the render expressions for the row and removes the content if render is evaluated false
461      *
462      * @param row the row being modified
463      * @param index the line index
464      * @param model the model
465      * @param expandedContext the context to evaluate expressions against
466      * @param expressionEvaluator the expression service to use for evaluation
467      * @return the modified row
468      */
469     protected String evaluateRenderExpressions(String row, int index, Object model, Map<String, Object> expandedContext,
470             ExpressionEvaluator expressionEvaluator) {
471         for (String id : renderIdExpressionMap.keySet()) {
472             String expression = renderIdExpressionMap.get(id);
473 
474             //adjust prefix for evaluation
475             expression = expression.replace(UifConstants.LINE_PATH_BIND_ADJUST_PREFIX,
476                     this.getBindingInfo().getBindingPath() + "[" + index + "].");
477 
478             //get expression result
479             Object value = expressionEvaluator.evaluateExpressionTemplate(expandedContext, expression);
480 
481             String wrap = A_TOKEN + RENDER + A_TOKEN + id + A_TOKEN;
482 
483             if (value != null && value instanceof String && Boolean.parseBoolean((String) value) == false) {
484                 //do not render this component - remove content between render wrappers
485                 row = row.replaceAll(wrap + "(.|\\s)*?" + wrap, "");
486             } else {
487                 //remove render wrappers only - keep content
488                 row = row.replaceAll(wrap, "");
489             }
490         }
491 
492         return row;
493     }
494 
495     /**
496      * Special handling of the DataField in the row, replaces necessary content with row specific content
497      *
498      * @param item the item being processed
499      * @param obj the row's object model
500      * @param row the row in html
501      * @param index the current row index
502      * @param originalId the original id of the component item
503      * @return the updated row
504      */
505     protected String handleDataFieldInRow(Component item, Object obj, String row, int index, String originalId) {
506         if (!(item instanceof DataField)) {
507             return row;
508         }
509 
510         Object currentValue = ObjectPropertyUtils.getPropertyValue(obj, ((DataField) item).getPropertyName());
511 
512         if (currentValue == null) {
513             currentValue = "";
514         }
515 
516         //for readOnly DataFields replace the value marked with the value on the current object
517         // TODO: this needs to go through registered property editors
518         row = row.replaceAll(VALUE_TOKEN + originalId + VALUE_TOKEN, currentValue.toString());
519         currentColumnValue = currentValue.toString();
520 
521         Inquiry dataFieldInquiry = ((DataField) item).getInquiry();
522         if (dataFieldInquiry != null && dataFieldInquiry.getInquiryParameters() != null
523                 && dataFieldInquiry.getInquiryLink() != null) {
524 
525             String inquiryLinkId = dataFieldInquiry.getInquiryLink().getId().replace(ID_TOKEN, "")
526                     + UifConstants.IdSuffixes.LINE + index;
527 
528             // process each Inquiry link parameter by replacing each in the inquiry url with their current value
529             for (String key : dataFieldInquiry.getInquiryParameters().keySet()) {
530                 String name = dataFieldInquiry.getInquiryParameters().get(key);
531 
532                 //omit the binding prefix fromt he key to get the path relative to the current object
533                 key = key.replace(((DataField) item).getBindingInfo().getBindByNamePrefix() + ".", "");
534 
535                 if (ObjectPropertyUtils.isReadableProperty(obj, key)) {
536                     String value = ObjectPropertyUtils.getPropertyValue(obj, key);
537                     row = row.replaceFirst("(" + inquiryLinkId + "(.|\\s)*?" + name + ")=.*?([&|\"])",
538                             "$1=" + value + "$3");
539                 }
540             }
541         }
542 
543         return row;
544     }
545 
546     /**
547      * Special handling of the InputField in the row, replaces necessary content with row specific content
548      *
549      * @param item the item being processed
550      * @param obj the row's object model
551      * @param row the row in html
552      * @param index the current row index
553      * @param originalId the original id of the component item
554      * @return the updated row
555      */
556     protected String handleInputFieldInRow(Component item, Object obj, String row, int index, String originalId) {
557         if (!(item instanceof InputField) || ((InputField) item).getControl() == null) {
558             return row;
559         }
560 
561         Control control = ((InputField) item).getControl();
562 
563         //updates the name path to the current path for any instance this item's propertyName with
564         //a collection binding prefix
565         row = row.replace("name=\"" + ((InputField) item).getBindingInfo().getBindingPath() + "\"",
566                 "name=\"" + this.getBindingInfo().getBindingPath() + "[" + index + "]." + ((InputField) item)
567                         .getPropertyName() + "\"");
568 
569         Object value = ObjectPropertyUtils.getPropertyValue(obj, ((InputField) item).getPropertyName());
570         String stringValue = "";
571 
572         if (value == null) {
573             stringValue = "";
574         } else if (value.getClass().isAssignableFrom(boolean.class)) {
575             stringValue = "" + value;
576         } else if (!(value instanceof Collection)) {
577             stringValue = value.toString();
578         }
579 
580         String controlId = originalId + "_line" + index + UifConstants.IdSuffixes.CONTROL;
581 
582         if (control instanceof CheckboxControl && stringValue.equalsIgnoreCase("true")) {
583             //CheckboxControl handling - only replace if true with a checked attribute appended
584             row = row.replaceAll("(id(\\s)*?=(\\s)*?\"" + controlId + "\")", "$1 checked=\"checked\" ");
585         } else if (control instanceof TextControl) {
586             //TextControl handling - replace with
587             row = row.replaceAll("(id(\\s)*?=(\\s)*?\"" + controlId + "\"(.|\\s)*?value=\")(.|\\s)*?\"",
588                     "$1" + stringValue + "\"");
589         } else if (control instanceof SelectControl && !((SelectControl) control).isMultiple()) {
590             //SelectControl handling (single item only)
591             Pattern pattern = Pattern.compile("<select(\\s)*?id(\\s)*?=(\\s)*?\"" + controlId + "\"(.|\\s)*?</select>");
592             Matcher matcher = pattern.matcher(row);
593             String replacement = "";
594 
595             if (matcher.find()) {
596                 //remove selected from select options
597                 String selected = "selected=\"selected\"";
598                 replacement = matcher.group().replace(selected, "");
599 
600                 //put selected on only the selected option
601                 String selectedValue = "value=\"" + stringValue + "\"";
602                 replacement = replacement.replace(selectedValue, selectedValue + " " + selected);
603             }
604 
605             //replace the old select tag with the old one
606             if (StringUtils.isNotBlank(replacement)) {
607                 row = matcher.replaceAll(replacement);
608             }
609         }
610 
611         currentColumnValue = stringValue;
612 
613         return row;
614     }
615 
616     /**
617      * The propertyName of the list backing this collection
618      *
619      * @return the propertyName of this collection
620      */
621     @BeanTagAttribute(name = "propertyName")
622     public String getPropertyName() {
623         return propertyName;
624     }
625 
626     /**
627      * Set the propertyName
628      *
629      * @param propertyName
630      */
631     public void setPropertyName(String propertyName) {
632         this.propertyName = propertyName;
633     }
634 
635     /**
636      * The bindingInfo for this collection table, containg the property path and other options
637      *
638      * @return the bindingInfo
639      */
640     @BeanTagAttribute(name = "bindingInfo", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
641     public BindingInfo getBindingInfo() {
642         return bindingInfo;
643     }
644 
645     /**
646      * Set the bindingInfo
647      *
648      * @param bindingInfo
649      */
650     public void setBindingInfo(BindingInfo bindingInfo) {
651         this.bindingInfo = bindingInfo;
652     }
653 
654     /**
655      * The labels for the header derived from the items of this collection (the fields)
656      *
657      * @return the header labels
658      */
659     public List<Label> getHeaderLabels() {
660         return headerLabels;
661     }
662 
663     /**
664      * The richTable widget definition for this table for setting dataTable javascript options
665      *
666      * @return the RichTable widget
667      */
668     @BeanTagAttribute(name = "richTable", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
669     public RichTable getRichTable() {
670         return richTable;
671     }
672 
673     /**
674      * Set the richTable widget
675      *
676      * @param richTable
677      */
678     public void setRichTable(RichTable richTable) {
679         this.richTable = richTable;
680     }
681 
682     /**
683      * The row css classes for the rows of this layout
684      *
685      * <p>To set a css class on all rows, use "all" as a key.  To set a
686      * class for even rows, use "even" as a key, for odd rows, use "odd".
687      * Use a one-based index to target a specific row by index.  SpringEL can be
688      * used as a key and the expression will be evaluated; if evaluated to true, the
689      * class(es) specified will be applied.</p>
690      *
691      * @return a map which represents the css classes of the rows of this layout
692      */
693     @BeanTagAttribute(name = "conditionalRowCssClasses", type = BeanTagAttribute.AttributeType.MAPVALUE)
694     public Map<String, String> getConditionalRowCssClasses() {
695         return conditionalRowCssClasses;
696     }
697 
698     /**
699      * Set the conditionalRowCssClasses
700      *
701      * @param conditionalRowCssClasses
702      */
703     public void setConditionalRowCssClasses(Map<String, String> conditionalRowCssClasses) {
704         this.conditionalRowCssClasses = conditionalRowCssClasses;
705     }
706 
707     /**
708      * True if this table is empty, false otherwise
709      *
710      * @return true if the collection backing this table is empty
711      */
712     public boolean isEmptyTable() {
713         return emptyTable;
714     }
715 
716     public void setHeaderLabels(List<Label> headerLabels) {
717         this.headerLabels = headerLabels;
718     }
719 
720     public void setExpressionConversionMap(Map<String, String> expressionConversionMap) {
721         this.expressionConversionMap = expressionConversionMap;
722     }
723 
724     public Map<String, String> getExpressionConversionMap() {
725         return expressionConversionMap;
726     }
727 
728     public List<String> getInitialComponentIds() {
729         return initialComponentIds;
730     }
731 
732     public Map<String, String> getRenderIdExpressionMap() {
733         return renderIdExpressionMap;
734     }
735 
736     public void setInitialComponentIds(List<String> initialComponentIds) {
737         this.initialComponentIds = initialComponentIds;
738     }
739 
740     public void setRenderIdExpressionMap(Map<String, String> renderIdExpressionMap) {
741         this.renderIdExpressionMap = renderIdExpressionMap;
742     }
743 
744     public void setEmptyTable(boolean emptyTable) {
745         this.emptyTable = emptyTable;
746     }
747 
748     /**
749      *
750      * @return the current column value
751      */
752     @BeanTagAttribute(name = "currentColumnValue")
753     protected String getCurrentColumnValue() {
754         return currentColumnValue;
755     }
756 
757     /**
758      * Set the current column value
759      *
760      * @param currentColumnValue
761      */
762     protected void setCurrentColumnValue(String currentColumnValue) {
763         this.currentColumnValue = currentColumnValue;
764     }
765 }