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.base;
23  
24  import java.util.Map;
25  
26  import javax.faces.FacesException;
27  import javax.faces.application.Application;
28  import javax.faces.application.FacesMessage;
29  import javax.faces.component.ContextCallback;
30  import javax.faces.component.EditableValueHolder;
31  import javax.faces.component.NamingContainer;
32  import javax.faces.component.UIComponent;
33  import javax.faces.component.UIComponentBase;
34  import javax.faces.component.UINamingContainer;
35  import javax.faces.component.UIViewRoot;
36  import javax.faces.component.UniqueIdVendor;
37  import javax.faces.component.visit.VisitCallback;
38  import javax.faces.component.visit.VisitContext;
39  import javax.faces.component.visit.VisitResult;
40  import javax.faces.context.FacesContext;
41  import javax.faces.event.FacesEvent;
42  import javax.faces.event.PhaseId;
43  import javax.faces.event.PostValidateEvent;
44  import javax.faces.event.PreValidateEvent;
45  import javax.faces.render.Renderer;
46  
47  import org.primefaces.component.api.UITabPanel;
48  import org.primefaces.extensions.event.EventDataWrapper;
49  import org.primefaces.extensions.model.common.KeyData;
50  import org.primefaces.extensions.util.Attrs;
51  import org.primefaces.extensions.util.SavedEditableValueState;
52  import org.primefaces.util.ComponentTraversalUtils;
53  
54  /**
55   * Abstract base class for all components with dynamic behavior like UIData.
56   *
57   * @author Oleg Varaksin / last modified by $Author$
58   * @version $Revision$
59   * @since 0.5
60   */
61  public abstract class AbstractDynamicData extends UIComponentBase implements NamingContainer, UniqueIdVendor {
62  
63      protected KeyData data;
64      private String clientId = null;
65      private final StringBuilder idBuilder = new StringBuilder();
66      private Boolean isNested = null;
67  
68      /**
69       * Properties that are tracked by state saving.
70       *
71       * @author Oleg Varaksin / last modified by $Author$
72       * @version $Revision$
73       */
74      @SuppressWarnings("java:S115")
75      protected enum PropertyKeys {
76  
77          // @formatter:off
78        saved,
79        lastId,
80        var,
81        varContainerId,
82        value;
83        //@formatter:on
84  
85          private final String toString;
86  
87          PropertyKeys() {
88              toString = null;
89          }
90  
91          @Override
92          public String toString() {
93              return toString != null ? toString : super.toString();
94          }
95      }
96  
97      public String getVar() {
98          return (String) getStateHelper().get(PropertyKeys.var);
99      }
100 
101     public void setVar(final String var) {
102         getStateHelper().put(PropertyKeys.var, var);
103     }
104 
105     public String getVarContainerId() {
106         return (String) getStateHelper().get(PropertyKeys.varContainerId);
107     }
108 
109     public void setVarContainerId(final String varContainerId) {
110         getStateHelper().put(PropertyKeys.varContainerId, varContainerId);
111     }
112 
113     public Object getValue() {
114         return getStateHelper().eval(PropertyKeys.value, null);
115     }
116 
117     public void setValue(final Object value) {
118         getStateHelper().put(PropertyKeys.value, value);
119     }
120 
121     /**
122      * Finds instance of {@link org.primefaces.extensions.model.common.KeyData} by corresponding key.
123      *
124      * @param key unique key
125      * @return KeyData found data
126      */
127     protected abstract KeyData findData(String key);
128 
129     /**
130      * Processes children components during processDecodes(), processValidators(), processUpdates().
131      *
132      * @param context faces context {@link FacesContext}
133      * @param phaseId current JSF phase id
134      */
135     protected abstract void processChildren(FacesContext context, PhaseId phaseId);
136 
137     /**
138      * Visits children components during visitTree().
139      *
140      * @param context visit context {@link VisitContext}
141      * @param callback visit callback {@link VisitCallback}
142      * @return boolean true - indicates that the children's visit is complete (e.g. all components that need to be visited have been visited), false -
143      *         otherwise.
144      */
145     protected abstract boolean visitChildren(VisitContext context, VisitCallback callback);
146 
147     /**
148      * Searches a child component with the given clientId during invokeOnComponent() and invokes the callback on it if found.
149      *
150      * @param context faces context {@link FacesContext}
151      * @param clientId client Id
152      * @param callback {@link ContextCallback}
153      * @return boolean true - child component was found, else - otherwise
154      */
155     protected abstract boolean invokeOnChildren(FacesContext context, String clientId, ContextCallback callback);
156 
157     public void setData(final String key) {
158         saveDescendantState();
159 
160         data = findData(key);
161         exposeVar();
162 
163         restoreDescendantState();
164     }
165 
166     public void setData(final KeyData keyData) {
167         saveDescendantState();
168 
169         data = keyData;
170         exposeVar();
171 
172         restoreDescendantState();
173     }
174 
175     public void resetData() {
176         saveDescendantState();
177 
178         data = null;
179         exposeVar();
180     }
181 
182     public KeyData getData() {
183         return data;
184     }
185 
186     @Override
187     public String getClientId(final FacesContext context) {
188         if (clientId != null) {
189             return clientId;
190         }
191 
192         String id = getId();
193         if (id == null) {
194             final UniqueIdVendor parentUniqueIdVendor = ComponentTraversalUtils.closestUniqueIdVendor(this);
195 
196             if (parentUniqueIdVendor == null) {
197                 final UIViewRoot viewRoot = context.getViewRoot();
198 
199                 if (viewRoot != null) {
200                     id = viewRoot.createUniqueId(context, null);
201                 }
202                 else {
203                     throw new FacesException("Cannot create clientId for " + this.getClass().getCanonicalName());
204                 }
205             }
206             else {
207                 id = parentUniqueIdVendor.createUniqueId(context, null);
208             }
209 
210             setId(id);
211         }
212 
213         final UIComponent namingContainer = ComponentTraversalUtils.closestNamingContainer(this);
214         if (namingContainer != null) {
215             final String containerClientId = namingContainer.getContainerClientId(context);
216 
217             if (containerClientId != null) {
218                 clientId = idBuilder.append(containerClientId).append(UINamingContainer.getSeparatorChar(context))
219                             .append(id)
220                             .toString();
221                 idBuilder.setLength(0);
222             }
223             else {
224                 clientId = id;
225             }
226         }
227         else {
228             clientId = id;
229         }
230 
231         final Renderer renderer = getRenderer(context);
232         if (renderer != null) {
233             clientId = renderer.convertClientId(context, clientId);
234         }
235 
236         return clientId;
237     }
238 
239     @Override
240     public void setId(final String id) {
241         super.setId(id);
242 
243         clientId = null;
244     }
245 
246     @Override
247     public String getContainerClientId(final FacesContext context) {
248         final String id = this.getClientId(context);
249 
250         final KeyData keyData = getData();
251         final String key = keyData != null ? keyData.getKey() : null;
252 
253         if (key == null) {
254             return id;
255         }
256         else {
257             final String containerClientId = idBuilder.append(id).append(UINamingContainer.getSeparatorChar(context))
258                         .append(key).toString();
259             idBuilder.setLength(0);
260 
261             return containerClientId;
262         }
263     }
264 
265     @Override
266     public void processDecodes(final FacesContext context) {
267         if (!isRendered()) {
268             return;
269         }
270 
271         pushComponentToEL(context, this);
272         preDecode(context);
273         processFacets(context, PhaseId.APPLY_REQUEST_VALUES, this);
274         processChildren(context, PhaseId.APPLY_REQUEST_VALUES);
275 
276         try {
277             decode(context);
278         }
279         catch (final RuntimeException e) {
280             context.renderResponse();
281             throw e;
282         }
283         finally {
284             popComponentFromEL(context);
285         }
286     }
287 
288     @Override
289     public void processValidators(final FacesContext context) {
290         if (!isRendered()) {
291             return;
292         }
293 
294         pushComponentToEL(context, this);
295 
296         final Application app = context.getApplication();
297         app.publishEvent(context, PreValidateEvent.class, this);
298 
299         processFacets(context, PhaseId.PROCESS_VALIDATIONS, this);
300         processChildren(context, PhaseId.PROCESS_VALIDATIONS);
301 
302         app.publishEvent(context, PostValidateEvent.class, this);
303         popComponentFromEL(context);
304     }
305 
306     @Override
307     public void processUpdates(final FacesContext context) {
308         if (!isRendered()) {
309             return;
310         }
311 
312         pushComponentToEL(context, this);
313         processFacets(context, PhaseId.UPDATE_MODEL_VALUES, this);
314         processChildren(context, PhaseId.UPDATE_MODEL_VALUES);
315         popComponentFromEL(context);
316     }
317 
318     protected void preDecode(final FacesContext context) {
319         final Map<String, SavedEditableValueState> saved = (Map<String, SavedEditableValueState>) getStateHelper()
320                     .get(PropertyKeys.saved);
321         if (null == saved) {
322             getStateHelper().remove(PropertyKeys.saved);
323         }
324         else if (!keepSaved(context)) {
325             for (final SavedEditableValueState saveState : saved.values()) {
326                 saveState.reset();
327             }
328         }
329     }
330 
331     private boolean keepSaved(final FacesContext context) {
332         return contextHasErrorMessages(context) || isNestedWithinIterator();
333     }
334 
335     private boolean contextHasErrorMessages(final FacesContext context) {
336         final FacesMessage.Severity sev = context.getMaximumSeverity();
337         return sev != null && FacesMessage.SEVERITY_ERROR.compareTo(sev) >= 0;
338     }
339 
340     protected Boolean isNestedWithinIterator() {
341         if (isNested == null) {
342             UIComponent parent = this;
343             while (null != (parent = parent.getParent())) {
344                 if (parent instanceof javax.faces.component.UIData ||
345                             parent.getClass().getName().endsWith("UIRepeat") ||
346                             parent instanceof UITabPanel && ((UITabPanel) parent).isRepeating() ||
347                             parent instanceof AbstractDynamicData) {
348                     isNested = Boolean.TRUE;
349                     break;
350                 }
351             }
352             if (isNested == null) {
353                 isNested = Boolean.FALSE;
354             }
355         }
356         return isNested;
357     }
358 
359     @Override
360     public void queueEvent(final FacesEvent event) {
361         super.queueEvent(new EventDataWrapper(this, event, getData()));
362     }
363 
364     @Override
365     public void broadcast(final FacesEvent event) {
366         if (!(event instanceof EventDataWrapper)) {
367             super.broadcast(event);
368 
369             return;
370         }
371 
372         final FacesContext context = FacesContext.getCurrentInstance();
373         final KeyData oldData = getData();
374         final EventDataWrapper eventDataWrapper = (EventDataWrapper) event;
375         final FacesEvent originalEvent = eventDataWrapper.getFacesEvent();
376         final UIComponent originalSource = (UIComponent) originalEvent.getSource();
377         setData(eventDataWrapper.getData());
378 
379         UIComponent compositeParent = null;
380         try {
381             if (!UIComponent.isCompositeComponent(originalSource)) {
382                 compositeParent = getCompositeComponentParent(originalSource);
383             }
384 
385             if (compositeParent != null) {
386                 compositeParent.pushComponentToEL(context, null);
387             }
388 
389             originalSource.pushComponentToEL(context, null);
390             originalSource.broadcast(originalEvent);
391         }
392         finally {
393             originalSource.popComponentFromEL(context);
394             if (compositeParent != null) {
395                 compositeParent.popComponentFromEL(context);
396             }
397         }
398 
399         setData(oldData);
400     }
401 
402     @Override
403     public boolean visitTree(final VisitContext context, final VisitCallback callback) {
404         if (!isVisitable(context)) {
405             return false;
406         }
407 
408         final FacesContext fc = context.getFacesContext();
409         final KeyData oldData = getData();
410         resetData();
411 
412         pushComponentToEL(fc, null);
413 
414         try {
415             final VisitResult result = context.invokeVisitCallback(this, callback);
416 
417             if (result == VisitResult.COMPLETE) {
418                 return true;
419             }
420 
421             if (result == VisitResult.ACCEPT && !context.getSubtreeIdsToVisit(this).isEmpty()) {
422                 if (getFacetCount() > 0) {
423                     for (final UIComponent facet : getFacets().values()) {
424                         if (facet.visitTree(context, callback)) {
425                             return true;
426                         }
427                     }
428                 }
429 
430                 if (visitChildren(context, callback)) {
431                     return true;
432                 }
433             }
434         }
435         finally {
436             popComponentFromEL(fc);
437             setData(oldData);
438         }
439 
440         return false;
441     }
442 
443     @Override
444     public boolean invokeOnComponent(final FacesContext context, final String clientId, final ContextCallback callback) {
445         final KeyData oldData = getData();
446         resetData();
447 
448         try {
449             if (clientId.equals(super.getClientId(context))) {
450                 pushComponentToEL(context, getCompositeComponentParent(this));
451                 callback.invokeContextCallback(context, this);
452 
453                 return true;
454             }
455 
456             if (getFacetCount() > 0) {
457                 for (final UIComponent c : getFacets().values()) {
458                     if (clientId.equals(c.getClientId(context))) {
459                         callback.invokeContextCallback(context, c);
460 
461                         return true;
462                     }
463                 }
464             }
465 
466             // skip if the component is not a children
467             if (!clientId.startsWith(this.getClientId(context))) {
468                 return false;
469             }
470 
471             return invokeOnChildren(context, clientId, callback);
472         }
473         catch (final FacesException fe) {
474             throw fe;
475         }
476         catch (final Exception e) {
477             throw new FacesException(e);
478         }
479         finally {
480             popComponentFromEL(context);
481             setData(oldData);
482         }
483     }
484 
485     protected void processFacets(final FacesContext context, final PhaseId phaseId, final UIComponent component) {
486         resetData();
487 
488         if (component.getFacetCount() > 0) {
489             for (final UIComponent facet : component.getFacets().values()) {
490                 if (phaseId == PhaseId.APPLY_REQUEST_VALUES) {
491                     facet.processDecodes(context);
492                 }
493                 else if (phaseId == PhaseId.PROCESS_VALIDATIONS) {
494                     facet.processValidators(context);
495                 }
496                 else if (phaseId == PhaseId.UPDATE_MODEL_VALUES) {
497                     facet.processUpdates(context);
498                 }
499                 else {
500                     throw new IllegalArgumentException();
501                 }
502             }
503         }
504     }
505 
506     @Override
507     public String createUniqueId(final FacesContext context, final String seed) {
508         final Integer i = (Integer) getStateHelper().get(PropertyKeys.lastId);
509         int lastId = i != null ? i : 0;
510         getStateHelper().put(PropertyKeys.lastId, ++lastId);
511 
512         return UIViewRoot.UNIQUE_ID_PREFIX + (seed == null ? lastId : seed);
513     }
514 
515     protected void exposeVar() {
516         final FacesContext fc = FacesContext.getCurrentInstance();
517         final Map<String, Object> requestMap = fc.getExternalContext().getRequestMap();
518 
519         final String var = getVar();
520         if (var != null) {
521             final KeyData keyData = getData();
522             if (keyData == null) {
523                 requestMap.remove(var);
524             }
525             else {
526                 requestMap.put(var, keyData.getData());
527             }
528         }
529 
530         final String varContainerId = getVarContainerId();
531         if (varContainerId != null) {
532             final String containerClientId = getContainerClientId(fc);
533             if (containerClientId == null) {
534                 requestMap.remove(varContainerId);
535             }
536             else {
537                 requestMap.put(varContainerId, containerClientId);
538             }
539         }
540     }
541 
542     protected void saveDescendantState() {
543         for (final UIComponent child : getChildren()) {
544             saveDescendantState(FacesContext.getCurrentInstance(), child);
545         }
546     }
547 
548     protected void saveDescendantState(final FacesContext context, final UIComponent component) {
549         // force id reset
550         component.setId(component.getId());
551 
552         final Map<String, SavedEditableValueState> saved = (Map<String, SavedEditableValueState>) getStateHelper()
553                     .get(PropertyKeys.saved);
554 
555         if (component instanceof EditableValueHolder) {
556             final EditableValueHolder input = (EditableValueHolder) component;
557             SavedEditableValueState state = null;
558             final String id = component.getClientId(context);
559 
560             if (saved == null) {
561                 state = new SavedEditableValueState();
562                 getStateHelper().put(PropertyKeys.saved, id, state);
563             }
564 
565             if (state == null) {
566                 state = saved.get(clientId);
567 
568                 if (state == null) {
569                     state = new SavedEditableValueState();
570                     getStateHelper().put(PropertyKeys.saved, id, state);
571                 }
572             }
573 
574             state.setValue(input.getLocalValue());
575             state.setValid(input.isValid());
576             state.setSubmittedValue(input.getSubmittedValue());
577             state.setLocalValueSet(input.isLocalValueSet());
578             state.setLabelValue(((UIComponent) input).getAttributes().get(Attrs.LABEL));
579 
580             // currently we can't save/restore the disabled: See #571 #644
581             // we also can't change it easily as the var is not not exposed at this time; it would need some refactoring
582             /*
583              * state.setDisabled(((UIComponent) input).getAttributes().get("disabled"));
584              */
585         }
586 
587         for (final UIComponent child : component.getChildren()) {
588             saveDescendantState(context, child);
589         }
590 
591         if (component.getFacetCount() > 0) {
592             for (final UIComponent facet : component.getFacets().values()) {
593                 saveDescendantState(context, facet);
594             }
595         }
596     }
597 
598     protected void restoreDescendantState() {
599         for (final UIComponent child : getChildren()) {
600             restoreDescendantState(FacesContext.getCurrentInstance(), child);
601         }
602     }
603 
604     protected void restoreDescendantState(final FacesContext context, final UIComponent component) {
605         // force id reset
606         component.setId(component.getId());
607 
608         final Map<String, SavedEditableValueState> saved = (Map<String, SavedEditableValueState>) getStateHelper()
609                     .get(PropertyKeys.saved);
610 
611         if (component instanceof EditableValueHolder) {
612             final EditableValueHolder input = (EditableValueHolder) component;
613             final String id = component.getClientId(context);
614 
615             SavedEditableValueState state = saved.get(id);
616             if (state == null) {
617                 state = new SavedEditableValueState();
618             }
619 
620             input.setValue(state.getValue());
621             input.setValid(state.isValid());
622             input.setSubmittedValue(state.getSubmittedValue());
623             input.setLocalValueSet(state.isLocalValueSet());
624             if (state.getLabelValue() != null) {
625                 ((UIComponent) input).getAttributes().put(Attrs.LABEL, state.getLabelValue());
626             }
627 
628             // currently we can't save/restore the disabled: See #571 #644
629             // we also can't change it easily as the var is not not exposed at this time; it would need some refactoring
630             /*
631              * if (state.getDisabled() != null) { ((UIComponent) input).getAttributes().put("disabled", state.getDisabled()); }
632              */
633         }
634 
635         for (final UIComponent child : component.getChildren()) {
636             restoreDescendantState(context, child);
637         }
638 
639         if (component.getFacetCount() > 0) {
640             for (final UIComponent facet : component.getFacets().values()) {
641                 restoreDescendantState(context, facet);
642             }
643         }
644     }
645 
646     @Override
647     public Object saveState(FacesContext context) {
648         // reset component for MyFaces view pooling
649         data = null;
650         clientId = null;
651         isNested = null;
652 
653         return super.saveState(context);
654     }
655 }