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.masterdetail;
23  
24  import java.util.Map;
25  
26  import javax.el.MethodExpression;
27  import javax.el.ValueExpression;
28  import javax.faces.FacesException;
29  import javax.faces.application.ResourceDependency;
30  import javax.faces.component.UIComponent;
31  import javax.faces.component.UIComponentBase;
32  import javax.faces.context.FacesContext;
33  import javax.faces.context.PartialViewContext;
34  import javax.faces.event.ComponentSystemEvent;
35  import javax.faces.event.PostRestoreStateEvent;
36  
37  import org.primefaces.component.breadcrumb.BreadCrumb;
38  import org.primefaces.model.menu.DefaultMenuItem;
39  import org.primefaces.model.menu.MenuModel;
40  import org.primefaces.util.Constants;
41  import org.primefaces.util.LangUtils;
42  
43  /**
44   * <code>MasterDetail</code> component.
45   *
46   * @author Oleg Varaksin / last modified by $Author$
47   * @version $Revision$
48   * @since 0.2
49   */
50  @ResourceDependency(library = "primefaces-extensions", name = "primefaces-extensions.css")
51  public class MasterDetail extends UIComponentBase {
52  
53      public static final String CONTEXT_VALUE_VALUE_EXPRESSION = "mdContextValueVE";
54      public static final String SELECTED_LEVEL_VALUE_EXPRESSION = "selectedLevelVE";
55      public static final String SELECTED_STEP_VALUE_EXPRESSION = "selectedStepVE";
56      public static final String PRESERVE_INPUTS_VALUE_EXPRESSION = "preserveInputsVE";
57      public static final String RESET_INPUTS_VALUE_EXPRESSION = "resetInputsVE";
58      public static final String CONTEXT_VALUES = "mdContextValues";
59      public static final String SELECT_DETAIL_REQUEST = "_selectDetailRequest";
60      public static final String CURRENT_LEVEL = "_currentLevel";
61      public static final String SELECTED_LEVEL = "_selectedLevel";
62      public static final String SELECTED_STEP = "_selectedStep";
63      public static final String PRESERVE_INPUTS = "_preserveInputs";
64      public static final String RESET_INPUTS = "_resetInputs";
65      public static final String CURRENT_CONTEXT_VALUE = "_curContextValue";
66      public static final String RESOLVED_CONTEXT_VALUE = "contextValue_";
67      public static final String BREADCRUMB_ID_PREFIX = "_bc";
68  
69      public static final String COMPONENT_TYPE = "org.primefaces.extensions.component.MasterDetail";
70      public static final String COMPONENT_FAMILY = "org.primefaces.extensions.component";
71      private static final String DEFAULT_RENDERER = "org.primefaces.extensions.component.MasterDetailRenderer";
72  
73      private MasterDetailLevel detailLevelToProcess;
74      private MasterDetailLevel detailLevelToGo;
75      private int levelPositionToProcess = -1;
76      private int levelCount = -1;
77  
78      /**
79       * Properties that are tracked by state saving.
80       *
81       * @author Oleg Varaksin / last modified by $Author$
82       * @version $Revision$
83       */
84      @SuppressWarnings("java:S115")
85      protected enum PropertyKeys {
86          // @formatter:off
87          level,
88          contextValue,
89          selectLevelListener,
90          showBreadcrumb,
91          showAllBreadcrumbItems,
92          showBreadcrumbFirstLevel,
93          breadcrumbAboveHeader,
94          style,
95          styleClass
96          // @formatter:on
97      }
98  
99      public MasterDetail() {
100         setRendererType(DEFAULT_RENDERER);
101     }
102 
103     @Override
104     public String getFamily() {
105         return COMPONENT_FAMILY;
106     }
107 
108     public int getLevel() {
109         return (Integer) getStateHelper().eval(PropertyKeys.level, 1);
110     }
111 
112     public void setLevel(final int level) {
113         getStateHelper().put(PropertyKeys.level, level);
114     }
115 
116     public Object getContextValue() {
117         return getStateHelper().eval(PropertyKeys.contextValue, null);
118     }
119 
120     public void setContextValue(final Object contextValue) {
121         getStateHelper().put(PropertyKeys.contextValue, contextValue);
122     }
123 
124     public MethodExpression getSelectLevelListener() {
125         return (MethodExpression) getStateHelper().eval(PropertyKeys.selectLevelListener, null);
126     }
127 
128     public void setSelectLevelListener(final MethodExpression selectLevelListener) {
129         getStateHelper().put(PropertyKeys.selectLevelListener, selectLevelListener);
130     }
131 
132     public boolean isShowBreadcrumb() {
133         return (Boolean) getStateHelper().eval(PropertyKeys.showBreadcrumb, true);
134     }
135 
136     public void setShowBreadcrumb(final boolean showBreadcrumb) {
137         getStateHelper().put(PropertyKeys.showBreadcrumb, showBreadcrumb);
138     }
139 
140     public boolean isShowAllBreadcrumbItems() {
141         return (Boolean) getStateHelper().eval(PropertyKeys.showAllBreadcrumbItems, false);
142     }
143 
144     public void setShowBreadcrumbFirstLevel(final boolean showBreadcrumbFirstLevel) {
145         getStateHelper().put(PropertyKeys.showBreadcrumbFirstLevel, showBreadcrumbFirstLevel);
146     }
147 
148     public boolean isShowBreadcrumbFirstLevel() {
149         return (Boolean) getStateHelper().eval(PropertyKeys.showBreadcrumbFirstLevel, true);
150     }
151 
152     public void setShowAllBreadcrumbItems(final boolean showAllBreadcrumbItems) {
153         getStateHelper().put(PropertyKeys.showAllBreadcrumbItems, showAllBreadcrumbItems);
154     }
155 
156     public boolean isBreadcrumbAboveHeader() {
157         return (Boolean) getStateHelper().eval(PropertyKeys.breadcrumbAboveHeader, true);
158     }
159 
160     public void setBreadcrumbAboveHeader(final boolean breadcrumbAboveHeader) {
161         getStateHelper().put(PropertyKeys.breadcrumbAboveHeader, breadcrumbAboveHeader);
162     }
163 
164     public String getStyle() {
165         return (String) getStateHelper().eval(PropertyKeys.style, null);
166     }
167 
168     public void setStyle(final String style) {
169         getStateHelper().put(PropertyKeys.style, style);
170     }
171 
172     public String getStyleClass() {
173         return (String) getStateHelper().eval(PropertyKeys.styleClass, null);
174     }
175 
176     public void setStyleClass(final String styleClass) {
177         getStateHelper().put(PropertyKeys.styleClass, styleClass);
178     }
179 
180     @Override
181     public void processEvent(final ComponentSystemEvent event) {
182         super.processEvent(event);
183 
184         final FacesContext fc = FacesContext.getCurrentInstance();
185         if (!(event instanceof PostRestoreStateEvent) || !isSelectDetailRequest(fc)) {
186             return;
187         }
188 
189         final PartialViewContext pvc = fc.getPartialViewContext();
190         if (pvc.getRenderIds().isEmpty()) {
191             // update the MasterDetail component automatically
192             pvc.getRenderIds().add(getClientId(fc));
193         }
194 
195         final MasterDetailLevel mdl = getDetailLevelToProcess(fc);
196         final Object contextValue = getContextValueFromFlow(fc, mdl, true);
197         final String contextVar = mdl.getContextVar();
198         if (LangUtils.isNotBlank(contextVar) && contextValue != null) {
199             final Map<String, Object> requestMap = fc.getExternalContext().getRequestMap();
200             requestMap.put(contextVar, contextValue);
201         }
202     }
203 
204     @Override
205     public void processDecodes(final FacesContext fc) {
206         if (!isSelectDetailRequest(fc)) {
207             super.processDecodes(fc);
208         }
209         else {
210             getDetailLevelToProcess(fc).processDecodes(fc);
211         }
212     }
213 
214     @Override
215     public void processValidators(final FacesContext fc) {
216         if (!isSelectDetailRequest(fc)) {
217             super.processValidators(fc);
218         }
219         else {
220             getDetailLevelToProcess(fc).processValidators(fc);
221         }
222     }
223 
224     @Override
225     public void processUpdates(final FacesContext fc) {
226         if (!isSelectDetailRequest(fc)) {
227             super.processUpdates(fc);
228         }
229         else {
230             getDetailLevelToProcess(fc).processUpdates(fc);
231         }
232     }
233 
234     public MasterDetailLevel getDetailLevelToProcess(final FacesContext fc) {
235         if (detailLevelToProcess == null) {
236             initDataForLevels(fc);
237         }
238 
239         return detailLevelToProcess;
240     }
241 
242     public MasterDetailLevel getDetailLevelToGo(final FacesContext fc) {
243         if (detailLevelToGo != null) {
244             return detailLevelToGo;
245         }
246 
247         final String strSelectedLevel = fc.getExternalContext().getRequestParameterMap()
248                     .get(getClientId(fc) + SELECTED_LEVEL);
249         final String strSelectedStep = fc.getExternalContext().getRequestParameterMap()
250                     .get(getClientId(fc) + SELECTED_STEP);
251 
252         // selected level != null
253         if (strSelectedLevel != null) {
254             final int selectedLevel = Integer.parseInt(strSelectedLevel);
255             detailLevelToGo = getDetailLevelByLevel(selectedLevel);
256             if (detailLevelToGo != null) {
257                 return detailLevelToGo;
258             }
259 
260             throw new FacesException("MasterDetailLevel for selected level = " + selectedLevel + " not found.");
261         }
262 
263         final int step;
264         if (strSelectedStep != null) {
265             // selected step != null
266             step = Integer.parseInt(strSelectedStep);
267         }
268         else {
269             // selected level and selected step are null ==> go to the next level
270             step = 1;
271         }
272 
273         detailLevelToGo = getDetailLevelByStep(step);
274 
275         return detailLevelToGo;
276     }
277 
278     public MasterDetailLevel getDetailLevelByLevel(final int level) {
279         for (final UIComponent child : getChildren()) {
280             if (child instanceof MasterDetailLevel) {
281                 final MasterDetailLevel mdl = (MasterDetailLevel) child;
282                 if (mdl.getLevel() == level) {
283                     return mdl;
284                 }
285             }
286         }
287 
288         return null;
289     }
290 
291     public boolean isSelectDetailRequest(final FacesContext fc) {
292         return fc.getPartialViewContext().isAjaxRequest()
293                     && fc.getExternalContext().getRequestParameterMap()
294                                 .containsKey(getClientId(fc) + SELECT_DETAIL_REQUEST);
295     }
296 
297     public String getPreserveInputs(final FacesContext fc) {
298         return fc.getExternalContext().getRequestParameterMap().get(getClientId(fc) + PRESERVE_INPUTS);
299     }
300 
301     public String getResetInputs(final FacesContext fc) {
302         return fc.getExternalContext().getRequestParameterMap().get(getClientId(fc) + RESET_INPUTS);
303     }
304 
305     public void updateModel(final FacesContext fc, final MasterDetailLevel mdlToGo) {
306         final int levelToGo = mdlToGo.getLevel();
307         final ValueExpression levelVE = getValueExpression(PropertyKeys.level.toString());
308         if (levelVE != null) {
309             // update "level"
310             levelVE.setValue(fc.getELContext(), levelToGo);
311             getStateHelper().remove(PropertyKeys.level);
312         }
313 
314         // get component caused this ajax request
315         final String source = fc.getExternalContext().getRequestParameterMap()
316                     .get(Constants.RequestParams.PARTIAL_SOURCE_PARAM);
317         final MasterDetailLevel mdl = getDetailLevelToProcess(fc);
318 
319         // get resolved context value
320         Object contextValue = null;
321         final Map<String, Object> contextValues = (Map<String, Object>) mdl.getAttributes().get(CONTEXT_VALUES);
322         if (contextValues != null) {
323             contextValue = contextValues.get(RESOLVED_CONTEXT_VALUE + source);
324         }
325 
326         if (contextValue != null) {
327             // update current context value for corresponding MasterDetailLevel
328             mdlToGo.getAttributes().put(getClientId(fc) + CURRENT_CONTEXT_VALUE, contextValue);
329 
330             final ValueExpression contextValueVE = getValueExpression(PropertyKeys.contextValue.toString());
331             if (contextValueVE != null) {
332                 // update "contextValue"
333                 contextValueVE.setValue(fc.getELContext(), contextValue);
334                 getStateHelper().remove(PropertyKeys.contextValue);
335             }
336         }
337     }
338 
339     public Object getContextValueFromFlow(final FacesContext fc, final MasterDetailLevel mdl,
340                 final boolean includeModel) {
341         // try to get context value from internal storage
342         final Object contextValue = mdl.getAttributes().get(getClientId(fc) + MasterDetail.CURRENT_CONTEXT_VALUE);
343         if (contextValue != null) {
344             return contextValue;
345         }
346 
347         // try to get context value from external storage (e.g. managed bean)
348         if (includeModel) {
349             return getContextValue();
350         }
351 
352         return null;
353     }
354 
355     public BreadCrumb getBreadcrumb() {
356         BreadCrumb breadCrumb = null;
357         for (final UIComponent child : getChildren()) {
358             if (child instanceof BreadCrumb) {
359                 breadCrumb = (BreadCrumb) child;
360 
361                 break;
362             }
363         }
364 
365         final MenuModel model = breadCrumb != null ? breadCrumb.getModel() : null;
366 
367         if (model != null && model.getElements().isEmpty()) {
368             final String clientId = getClientId();
369             final String menuItemIdPrefix = getId() + "_bcItem_";
370 
371             for (final UIComponent child : getChildren()) {
372                 if (child instanceof MasterDetailLevel) {
373                     final int level = ((MasterDetailLevel) child).getLevel();
374 
375                     // create menu item to detail level
376                     final DefaultMenuItem menuItem = new DefaultMenuItem();
377                     menuItem.setId(menuItemIdPrefix + level);
378                     menuItem.setAjax(true);
379                     menuItem.setImmediate(true);
380                     menuItem.setProcess("@none");
381                     menuItem.setUpdate("@parent");
382 
383                     // add UIParameter
384                     menuItem.setParam(clientId + MasterDetail.SELECT_DETAIL_REQUEST, true);
385                     menuItem.setParam(clientId + MasterDetail.CURRENT_LEVEL, -1);
386                     menuItem.setParam(clientId + MasterDetail.SELECTED_LEVEL, level);
387 
388                     model.getElements().add(menuItem);
389                 }
390             }
391         }
392 
393         return breadCrumb;
394     }
395 
396     public void resetCalculatedValues() {
397         detailLevelToProcess = null;
398         detailLevelToGo = null;
399         levelPositionToProcess = -1;
400         levelCount = -1;
401     }
402 
403     private void initDataForLevels(final FacesContext fc) {
404         final String strCurrentLevel = fc.getExternalContext().getRequestParameterMap()
405                     .get(getClientId(fc) + CURRENT_LEVEL);
406         if (strCurrentLevel == null) {
407             throw new FacesException("Current level is missing in request.");
408         }
409 
410         final int currentLevel = Integer.parseInt(strCurrentLevel);
411         int count = 0;
412 
413         for (final UIComponent child : getChildren()) {
414             if (child instanceof MasterDetailLevel) {
415                 final MasterDetailLevel mdl = (MasterDetailLevel) child;
416                 count++;
417 
418                 if (detailLevelToProcess == null && mdl.getLevel() == currentLevel) {
419                     detailLevelToProcess = mdl;
420                     levelPositionToProcess = count;
421                 }
422             }
423         }
424 
425         levelCount = count;
426 
427         if (detailLevelToProcess == null) {
428             throw new FacesException("Current MasterDetailLevel to process not found.");
429         }
430     }
431 
432     private MasterDetailLevel getDetailLevelByStep(final int step) {
433         int levelPositionToGo = getLevelPositionToProcess() + step;
434         if (levelPositionToGo < 1) {
435             levelPositionToGo = 1;
436         }
437         else if (levelPositionToGo > getLevelCount()) {
438             levelPositionToGo = getLevelCount();
439         }
440 
441         int pos = 0;
442         for (final UIComponent child : getChildren()) {
443             if (child instanceof MasterDetailLevel) {
444                 final MasterDetailLevel mdl = (MasterDetailLevel) child;
445                 pos++;
446 
447                 if (pos == levelPositionToGo) {
448                     return mdl;
449                 }
450             }
451         }
452 
453         // should not happen
454         return null;
455     }
456 
457     private int getLevelPositionToProcess() {
458         if (levelPositionToProcess == -1) {
459             initDataForLevels(FacesContext.getCurrentInstance());
460         }
461 
462         return levelPositionToProcess;
463     }
464 
465     private int getLevelCount() {
466         if (levelCount == -1) {
467             initDataForLevels(FacesContext.getCurrentInstance());
468         }
469 
470         return levelCount;
471     }
472 
473     @Override
474     public Object saveState(final FacesContext context) {
475         // reset component for MyFaces view pooling
476         detailLevelToGo = null;
477         detailLevelToProcess = null;
478         levelCount = -1;
479         levelPositionToProcess = -1;
480 
481         return super.saveState(context);
482     }
483 }