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