View Javadoc
1   /*
2    * Copyright (c) 2011-2024 PrimeFaces Extensions
3    *
4    *  Permission is hereby granted, free of charge, to any person obtaining a copy
5    *  of this software and associated documentation files (the "Software"), to deal
6    *  in the Software without restriction, including without limitation the rights
7    *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8    *  copies of the Software, and to permit persons to whom the Software is
9    *  furnished to do so, subject to the following conditions:
10   *
11   *  The above copyright notice and this permission notice shall be included in
12   *  all copies or substantial portions of the Software.
13   *
14   *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15   *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16   *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17   *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18   *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19   *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20   *  THE SOFTWARE.
21   */
22  package org.primefaces.extensions.component.sheet;
23  
24  import java.util.Collection;
25  
26  import javax.faces.application.FacesMessage;
27  import javax.faces.component.UIComponent;
28  import javax.faces.component.UIInput;
29  import javax.faces.component.behavior.ClientBehaviorHolder;
30  import javax.faces.context.FacesContext;
31  import javax.faces.model.SelectItem;
32  import javax.faces.validator.Validator;
33  import javax.faces.validator.ValidatorException;
34  
35  /**
36   * JSF Component used to represent a column in the Sheet component.
37   *
38   * @author Mark Lassiter / Melloware
39   * @since 6.2
40   */
41  public class SheetColumn extends UIInput implements ClientBehaviorHolder {
42  
43      public static final String COMPONENT_TYPE = "org.primefaces.extensions.component.SheetColumn";
44      public static final String COMPONENT_FAMILY = "org.primefaces.extensions.component";
45      private static final String MESSAGE_REQUIRED = "A valid value for this column is required.";
46  
47      /**
48       * Properties that are tracked by state saving.
49       */
50      @SuppressWarnings("java:S115")
51      enum PropertyKeys {
52  
53          /**
54           * The width of the column
55           */
56          colWidth,
57  
58          /**
59           * The type of the column (text, numeric, etc)
60           */
61          colType,
62  
63          /**
64           * The header text to display for the column
65           */
66          headerText,
67  
68          /**
69           * Defines whether column should trim the whitespace at the beginning and the end of the cell contents. Default Value: true
70           */
71          trimWhitespace,
72  
73          /**
74           * Password mask asterisk * by default
75           */
76          passwordHashSymbol,
77  
78          /**
79           * Password fixed length
80           */
81          passwordHashLength,
82  
83          /**
84           * Numbers can be formatted to look like currency, percentages, times, or even plain old numbers with decimal places, thousands, and abbreviations.
85           */
86          numericPattern,
87  
88          /**
89           * Numbers such as currency can be configured for a Locale. Default to en_us.
90           */
91          numericLocale,
92  
93          /**
94           * Moment.js date format for colType="date". Default is "DD/MM/YYYY"
95           */
96          dateFormat,
97  
98          /**
99           * JSON config for coltype="date". DatePicker additional options (see https://github.com/dbushell/Pikaday#configuration)
100          */
101         datePickerConfig,
102 
103         /**
104          * Moment.js time format for colType="date". Default is "h:mm:ss a"
105          */
106         timeFormat,
107 
108         /**
109          * If true, the autocomplete cells will only accept values that are defined in the source array. Default true.
110          */
111         autoCompleteStrict,
112 
113         /**
114          * If true, allows manual input of value that does not exist in the source. In this case, the field background highlight becomes red and the selection
115          * advances to the next cell.
116          */
117         autoCompleteAllowInvalid,
118 
119         /**
120          * Number of rows visible in the autocomplete dropdown.
121          */
122         autoCompleteVisibleRows,
123 
124         /**
125          * If true, trims the dropdown to fit the cell size. Default to true.
126          */
127         autoCompleteTrimDropdown,
128 
129         /**
130          * List of values to display in colType="dropdown" or "autocomplete".
131          */
132         selectItems,
133 
134         /**
135          * Flag indicating whether or not the column is read only
136          */
137         readOnly,
138 
139         /**
140          * Flag indicating whether or not a cell is read only
141          */
142         readonlyCell,
143 
144         /**
145          * the style class for the cell
146          */
147         styleClass,
148 
149         /**
150          * Filter by value expression
151          */
152         filterBy,
153 
154         /**
155          * Filter options
156          */
157         filterOptions,
158 
159         /**
160          * The filter match mode: startsWith, endsWith, contains, exact
161          */
162         filterMatchMode,
163 
164         /**
165          * The submitted filtered value
166          */
167         filterValue,
168         /**
169          * Controls the visibilty of the column, default is true.
170          */
171         visible,
172         /**
173          * When set to true, the text of the cell content is wrapped if it does not fit in the fixed column width.
174          */
175         wordWrap,
176         /**
177          * A Javascript function, regular expression or a string, which will be used in the process of cell validation. If a function is used, be sure to
178          * execute the callback argument with either true (callback(true)) if the validation passed or with false (callback(false)), if the validation failed.
179          * Note, that this in the function points to the cellProperties object.
180          */
181         onvalidate
182     }
183 
184     private Object localValue;
185 
186     /**
187      * Sheet reference. Updated on decode.
188      */
189     private Sheet sheet;
190 
191     /**
192      * Default constructor
193      */
194     public SheetColumn() {
195         setRendererType(null);
196     }
197 
198     /*
199      * (non-Javadoc)
200      * @see javax.faces.component.UIComponent#getFamily()
201      */
202     @Override
203     public String getFamily() {
204         return COMPONENT_FAMILY;
205     }
206 
207     /**
208      * Updates the fixed columns count.
209      *
210      * @param value
211      */
212     public void setHeaderText(final String value) {
213         getStateHelper().put(PropertyKeys.headerText, value);
214     }
215 
216     /**
217      * The fixed column count.
218      *
219      * @return
220      */
221     public String getHeaderText() {
222         final Object result = getStateHelper().eval(PropertyKeys.headerText, null);
223         if (result == null) {
224             return null;
225         }
226         return result.toString();
227     }
228 
229     /**
230      * Updates the readOnly property
231      *
232      * @param value
233      */
234     public void setReadOnly(final boolean value) {
235         getStateHelper().put(PropertyKeys.readOnly, value);
236     }
237 
238     /**
239      * Flag indicating whether this column is read only.
240      *
241      * @return true if read only, otherwise false
242      */
243     public boolean isReadOnly() {
244         return Boolean.valueOf(getStateHelper().eval(PropertyKeys.readOnly, Boolean.FALSE).toString());
245     }
246 
247     /**
248      * Updates the readOnly property of the cell
249      *
250      * @param value
251      */
252     public void setReadonlyCell(final boolean value) {
253         getStateHelper().put(PropertyKeys.readonlyCell, value);
254     }
255 
256     /**
257      * Flag indicating whether this cell is read only. the var reference will be available.
258      *
259      * @return true if read only, otherwise false
260      */
261     public boolean isReadonlyCell() {
262         return Boolean.valueOf(getStateHelper().eval(PropertyKeys.readonlyCell, Boolean.FALSE).toString());
263     }
264 
265     /**
266      * Updates the column width
267      *
268      * @param value
269      */
270     public void setColWidth(final Integer value) {
271         getStateHelper().put(PropertyKeys.colWidth, value);
272     }
273 
274     /**
275      * The column width
276      *
277      * @return
278      */
279     public Integer getColWidth() {
280         final Object result = getStateHelper().eval(PropertyKeys.colWidth, null);
281         if (result == null) {
282             return null;
283         }
284         return Integer.valueOf(result.toString());
285     }
286 
287     /**
288      * Updates the column type. Possible values are: text, numeric, date, checkbox, autocomplete, handsontable.
289      *
290      * @param value
291      */
292     public void setColType(final String value) {
293         getStateHelper().put(PropertyKeys.colType, value);
294     }
295 
296     /**
297      * the Handsontable column type.
298      */
299     public String getColType() {
300         return getStateHelper().eval(PropertyKeys.colType, "text").toString();
301     }
302 
303     public boolean isAutoCompleteAllowInvalid() {
304         return Boolean.valueOf(getStateHelper().eval(PropertyKeys.autoCompleteAllowInvalid, Boolean.FALSE).toString());
305     }
306 
307     public void setAutoCompleteAllowInvalid(final boolean value) {
308         getStateHelper().put(PropertyKeys.autoCompleteAllowInvalid, value);
309     }
310 
311     public boolean isAutoCompleteStrict() {
312         return Boolean.valueOf(getStateHelper().eval(PropertyKeys.autoCompleteStrict, Boolean.TRUE).toString());
313     }
314 
315     public void setAutoCompleteStrict(final boolean value) {
316         getStateHelper().put(PropertyKeys.autoCompleteStrict, value);
317     }
318 
319     public boolean isAutoCompleteTrimDropdown() {
320         return Boolean.valueOf(getStateHelper().eval(PropertyKeys.autoCompleteTrimDropdown, Boolean.TRUE).toString());
321     }
322 
323     public void setAutoCompleteTrimDropdown(final boolean value) {
324         getStateHelper().put(PropertyKeys.autoCompleteTrimDropdown, value);
325     }
326 
327     public void setAutoCompleteVisibleRows(final Integer value) {
328         getStateHelper().put(PropertyKeys.autoCompleteVisibleRows, value);
329     }
330 
331     public Integer getAutoCompleteVisibleRows() {
332         return (Integer) getStateHelper().eval(PropertyKeys.autoCompleteVisibleRows, null);
333     }
334 
335     public Object getSelectItems() {
336         return getStateHelper().eval(PropertyKeys.selectItems, null);
337     }
338 
339     public void setSelectItems(final Object selectItems) {
340         getStateHelper().put(PropertyKeys.selectItems, selectItems);
341     }
342 
343     public Integer getPasswordHashLength() {
344         return (Integer) getStateHelper().eval(PropertyKeys.passwordHashLength, null);
345     }
346 
347     public void setPasswordHashLength(final Integer _passwordHashLength) {
348         getStateHelper().put(PropertyKeys.passwordHashLength, _passwordHashLength);
349     }
350 
351     public void setPasswordHashSymbol(final String _passwordHashSymbol) {
352         getStateHelper().put(PropertyKeys.passwordHashSymbol, _passwordHashSymbol);
353     }
354 
355     public String getPasswordHashSymbol() {
356         return (String) getStateHelper().eval(PropertyKeys.passwordHashSymbol, "*");
357     }
358 
359     public void setNumericPattern(final String _numericPattern) {
360         getStateHelper().put(PropertyKeys.numericPattern, _numericPattern);
361     }
362 
363     public String getNumericPattern() {
364         return (String) getStateHelper().eval(PropertyKeys.numericPattern, "0 a");
365     }
366 
367     public String getNumericLocale() {
368         return (String) getStateHelper().eval(PropertyKeys.numericLocale, "en-US");
369     }
370 
371     public void setNumericLocale(final String locale) {
372         getStateHelper().put(PropertyKeys.numericLocale, locale);
373     }
374 
375     public void setDateFormat(final String _dateFormat) {
376         getStateHelper().put(PropertyKeys.dateFormat, _dateFormat);
377     }
378 
379     public String getDateFormat() {
380         return (String) getStateHelper().eval(PropertyKeys.dateFormat, "DD/MM/YYYY");
381     }
382 
383     public void setDatePickerConfig(final String _datePickerConfig) {
384         getStateHelper().put(PropertyKeys.datePickerConfig, _datePickerConfig);
385     }
386 
387     public String getDatePickerConfig() {
388         return (String) getStateHelper().eval(PropertyKeys.datePickerConfig, null);
389     }
390 
391     public void setTimeFormat(final String _timeFormat) {
392         getStateHelper().put(PropertyKeys.timeFormat, _timeFormat);
393     }
394 
395     public String getTimeFormat() {
396         return (String) getStateHelper().eval(PropertyKeys.timeFormat, "h:mm:ss a");
397     }
398 
399     public void setOnvalidate(final String _onvalidate) {
400         getStateHelper().put(PropertyKeys.onvalidate, _onvalidate);
401     }
402 
403     public String getOnvalidate() {
404         return (String) getStateHelper().eval(PropertyKeys.onvalidate, null);
405     }
406 
407     public void setVisible(final boolean value) {
408         getStateHelper().put(PropertyKeys.visible, value);
409     }
410 
411     public boolean isVisible() {
412         return Boolean.valueOf(getStateHelper().eval(PropertyKeys.visible, Boolean.TRUE).toString());
413     }
414 
415     public void setWordWrap(final boolean value) {
416         getStateHelper().put(PropertyKeys.wordWrap, value);
417     }
418 
419     public Boolean isWordWrap() {
420         return Boolean.valueOf(getStateHelper().eval(PropertyKeys.wordWrap, Boolean.TRUE).toString());
421     }
422 
423     public void setTrimWhitespace(final boolean value) {
424         getStateHelper().put(PropertyKeys.trimWhitespace, value);
425     }
426 
427     public Boolean isTrimWhitespace() {
428         return Boolean.valueOf(getStateHelper().eval(PropertyKeys.trimWhitespace, Boolean.TRUE).toString());
429     }
430 
431     /**
432      * Update the style class for the cell.
433      *
434      * @param value
435      */
436     public void setStyleClass(final String value) {
437         getStateHelper().put(PropertyKeys.styleClass, value);
438     }
439 
440     /**
441      * The style class for the cell.
442      *
443      * @return
444      */
445     public String getStyleClass() {
446         final Object result = getStateHelper().eval(PropertyKeys.styleClass, null);
447         if (result == null) {
448             return null;
449         }
450         return result.toString();
451     }
452 
453     /**
454      * The filterBy expression
455      *
456      * @return
457      */
458     public Object getFilterBy() {
459         return getStateHelper().eval(PropertyKeys.filterBy, null);
460     }
461 
462     /**
463      * Update the filter by field
464      *
465      * @param filterBy
466      */
467     public void setFilterBy(final Object filterBy) {
468         getStateHelper().put(PropertyKeys.filterBy, filterBy);
469     }
470 
471     /**
472      * The filter value submitted by the user
473      *
474      * @return
475      */
476     public String getFilterValue() {
477         return (String) getStateHelper().eval(PropertyKeys.filterValue);
478     }
479 
480     /**
481      * Update the filter value for this column
482      *
483      * @param filterValue
484      */
485     public void setFilterValue(final String filterValue) {
486         getStateHelper().put(PropertyKeys.filterValue, filterValue);
487     }
488 
489     /**
490      * The filter match mode submitted by the user
491      *
492      * @return
493      */
494     public String getFilterMatchMode() {
495         return (String) getStateHelper().eval(PropertyKeys.filterMatchMode);
496     }
497 
498     /**
499      * Update the filter match mode for this column
500      *
501      * @param filterMatchMode
502      */
503     public void setFilterMatchMode(final String filterMatchMode) {
504         getStateHelper().put(PropertyKeys.filterMatchMode, filterMatchMode);
505     }
506 
507     /**
508      * The filterOptions expression
509      *
510      * @return
511      */
512     public Collection<SelectItem> getFilterOptions() {
513         return (Collection<SelectItem>) getStateHelper().eval(PropertyKeys.filterOptions, null);
514     }
515 
516     /**
517      * Update the filterOptions field
518      */
519     public void setFilterOptions(final Collection<SelectItem> filterOptions) {
520         getStateHelper().put(PropertyKeys.filterOptions, filterOptions);
521     }
522 
523     /**
524      * Get the parent sheet
525      *
526      * @return
527      */
528     public Sheet getSheet() {
529         if (sheet != null) {
530             return sheet;
531         }
532 
533         UIComponent parent = getParent();
534         while (parent != null && !(parent instanceof Sheet)) {
535             parent = parent.getParent();
536         }
537         return (Sheet) parent;
538     }
539 
540     /**
541      * Updates the sheet reference to work around getParent sometimes returning null.
542      *
543      * @param sheet the owning sheet
544      */
545     public void setSheet(final Sheet sheet) {
546         this.sheet = sheet;
547     }
548 
549     /**
550      * --------------------------------------------------
551      * <p>
552      * Override UIInput methods
553      */
554 
555     /**
556      * Ignore attempts to set the local value for this column, again done by parent.
557      */
558     @Override
559     public void setValue(final Object value) {
560         localValue = value;
561         setLocalValueSet(true);
562     }
563 
564     /**
565      * When asked for the value, return the local value if available, otherwise the sheet value.
566      */
567     @Override
568     public Object getValue() {
569         return localValue;
570     }
571 
572     /**
573      * We are valid if the sheet is valid, we do not track at the individual column.
574      */
575     @Override
576     public boolean isValid() {
577         return getSheet().isValid();
578     }
579 
580     /**
581      * when we become valid, invalidate the whole sheet.
582      */
583     @Override
584     public void setValid(final boolean valid) {
585         getSheet().setValid(valid);
586     }
587 
588     /**
589      * Sheet handles decoding of all submitted values
590      */
591     @Override
592     public void processDecodes(final FacesContext context) {
593         // do nothing, done for us by sheet
594     }
595 
596     /**
597      * Sheet handles updating of model
598      */
599     @Override
600     public void processUpdates(final FacesContext context) {
601         // do nothing, done for us by sheet
602     }
603 
604     /**
605      * Reset the local value. No submitted value tracked here. Validity not tracked here.
606      */
607     @Override
608     public void resetValue() {
609         setValue(null);
610         setLocalValueSet(false);
611     }
612 
613     /**
614      * Don't do anything when called by inherited behavior. Sheet will call validate directly
615      */
616     @Override
617     public void processValidators(final FacesContext context) {
618         // do nothing, sheet will call validate directly
619     }
620 
621     /**
622      * Process all validators (skip normal UIInput behavior)
623      */
624     @Override
625     public void validate(final FacesContext context) {
626         if (context == null) {
627             throw new NullPointerException();
628         }
629 
630         final Validator[] validators = getValidators();
631         final Object value = getValue();
632 
633         if (!validateRequired(context, value)) {
634             return;
635         }
636 
637         if (validators == null) {
638             return;
639         }
640 
641         for (final Validator validator : validators) {
642             try {
643                 validator.validate(context, this, value);
644             }
645             catch (final ValidatorException ve) {
646                 // If the validator throws an exception, we're
647                 // invalid, and we need to add a message
648                 setValid(false);
649                 final FacesMessage message;
650                 final String validatorMessageString = getValidatorMessage();
651 
652                 if (null != validatorMessageString) {
653                     message = new FacesMessage(FacesMessage.SEVERITY_ERROR,
654                                 validatorMessageString,
655                                 validatorMessageString);
656                     message.setSeverity(FacesMessage.SEVERITY_ERROR);
657                 }
658                 else {
659                     final Collection<FacesMessage> messages = ve.getFacesMessages();
660                     if (null != messages) {
661                         message = null;
662                         final String cid = getClientId(context);
663                         for (final FacesMessage m : messages) {
664                             context.addMessage(cid, m);
665                         }
666                     }
667                     else {
668                         message = ve.getFacesMessage();
669                     }
670                 }
671                 if (message != null) {
672                     final Sheet current = getSheet();
673                     if (current == null) {
674                         return;
675                     }
676                     context.addMessage(getClientId(context), message);
677                     current.getInvalidUpdates().add(
678                                 new SheetInvalidUpdate(current.getRowKeyValue(context), current.getColumns().indexOf(this), this, value,
679                                             message.getDetail()));
680 
681                 }
682             }
683         }
684 
685     }
686 
687     /**
688      * Validates the value against the required flags on this column.
689      *
690      * @param context the FacesContext
691      * @param newValue the new value for this column
692      * @return true if passes validation, otherwise valse
693      */
694     protected boolean validateRequired(final FacesContext context, final Object newValue) {
695         // If our value is valid, enforce the required property if present
696         if (isValid() && isRequired() && UIInput.isEmpty(newValue)) {
697             final String requiredMessageStr = getRequiredMessage();
698             final FacesMessage message;
699             if (null != requiredMessageStr) {
700                 message = new FacesMessage(FacesMessage.SEVERITY_ERROR,
701                             requiredMessageStr,
702                             requiredMessageStr);
703             }
704             else {
705                 message = new FacesMessage(FacesMessage.SEVERITY_ERROR,
706                             MESSAGE_REQUIRED,
707                             MESSAGE_REQUIRED);
708             }
709             context.addMessage(getClientId(context), message);
710             final Sheet current = getSheet();
711             if (current != null) {
712                 current.getInvalidUpdates().add(
713                             new SheetInvalidUpdate(current.getRowKeyValue(context), current.getColumns().indexOf(this), this, newValue,
714                                         message.getDetail()));
715             }
716             setValid(false);
717             return false;
718         }
719         return true;
720     }
721 }