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.io.IOException;
25  import java.util.List;
26  import java.util.Map;
27  
28  import javax.faces.FacesException;
29  import javax.faces.component.EditableValueHolder;
30  import javax.faces.component.UIComponent;
31  import javax.faces.component.visit.VisitContext;
32  import javax.faces.context.FacesContext;
33  import javax.faces.context.ResponseWriter;
34  import javax.faces.convert.Converter;
35  import javax.faces.render.Renderer;
36  
37  import org.primefaces.component.breadcrumb.BreadCrumb;
38  import org.primefaces.extensions.util.Attrs;
39  import org.primefaces.extensions.util.ExtLangUtils;
40  import org.primefaces.model.menu.DefaultMenuItem;
41  import org.primefaces.model.menu.MenuElement;
42  import org.primefaces.model.menu.MenuItem;
43  import org.primefaces.renderkit.CoreRenderer;
44  import org.primefaces.util.ComponentUtils;
45  import org.primefaces.util.FacetUtils;
46  import org.primefaces.util.FastStringWriter;
47  import org.primefaces.util.LangUtils;
48  
49  /**
50   * Renderer for the {@link MasterDetail} component.
51   *
52   * @author Oleg Varaksin / last modified by $Author$
53   * @version $Revision$
54   * @since 0.2
55   */
56  public class MasterDetailRenderer extends CoreRenderer {
57  
58      private static final String FACET_HEADER = "header";
59      private static final String FACET_FOOTER = "footer";
60      private static final String FACET_LABEL = Attrs.LABEL;
61  
62      @Override
63      public void encodeEnd(final FacesContext fc, final UIComponent component) throws IOException {
64          final MasterDetail masterDetail = (MasterDetail) component;
65          final MasterDetailLevel mdl;
66  
67          if (masterDetail.isSelectDetailRequest(fc)) {
68              // component has been navigated via SelectDetailLevel
69              final MasterDetailLevel mdlToProcess = masterDetail.getDetailLevelToProcess(fc);
70  
71              if (fc.isValidationFailed()) {
72                  mdl = mdlToProcess;
73              }
74              else {
75                  mdl = getDetailLevelToEncode(fc, masterDetail, mdlToProcess, masterDetail.getDetailLevelToGo(fc));
76  
77                  // reset last saved validation state and stored values of editable components
78                  final MasterDetailLevelVisitCallback visitCallback = new MasterDetailLevelVisitCallback();
79                  mdlToProcess.visitTree(VisitContext.createVisitContext(fc), visitCallback);
80  
81                  final String preserveInputs = masterDetail.getPreserveInputs(fc);
82                  final String resetInputs = masterDetail.getResetInputs(fc);
83                  final String[] piIds = preserveInputs != null ? preserveInputs.split("[\\s,]+") : null;
84                  final String[] riIds = resetInputs != null ? resetInputs.split("[\\s,]+") : null;
85                  final boolean preserveAll = ExtLangUtils.contains(piIds, "@all");
86                  final boolean resetAll = ExtLangUtils.contains(riIds, "@all");
87  
88                  final List<EditableValueHolder> editableValueHolders = visitCallback.getEditableValueHolders();
89                  for (final EditableValueHolder editableValueHolder : editableValueHolders) {
90                      final String clientId = ((UIComponent) editableValueHolder).getClientId(fc);
91                      if (resetAll || ExtLangUtils.contains(riIds, clientId)) {
92                          editableValueHolder.resetValue();
93                      }
94                      else if (preserveAll || ExtLangUtils.contains(piIds, clientId)) {
95                          editableValueHolder.setValue(getConvertedSubmittedValue(fc, editableValueHolder));
96                      }
97                      else {
98                          // default behavior
99                          editableValueHolder.resetValue();
100                     }
101                 }
102             }
103 
104             masterDetail.updateModel(fc, mdl);
105         }
106         else {
107             // component has been navigated from the outside, e.g. GET request or POST update from another component
108             mdl = masterDetail.getDetailLevelByLevel(masterDetail.getLevel());
109         }
110 
111         if (mdl == null) {
112             throw new FacesException(
113                         "MasterDetailLevel [Level=" + masterDetail.getLevel() +
114                                     "] must be nested inside a MasterDetail component!");
115         }
116 
117         // render MasterDetailLevel
118         encodeMarkup(fc, masterDetail, mdl);
119 
120         // reset calculated values
121         masterDetail.resetCalculatedValues();
122     }
123 
124     protected MasterDetailLevel getDetailLevelToEncode(final FacesContext fc, final MasterDetail masterDetail,
125                 final MasterDetailLevel mdlToProcess,
126                 final MasterDetailLevel mdlToGo) {
127         if (masterDetail.getSelectLevelListener() != null) {
128             final SelectLevelEvent selectLevelEvent = new SelectLevelEvent(masterDetail, mdlToProcess.getLevel(),
129                         mdlToGo.getLevel());
130             final int levelToEncode = (Integer) masterDetail.getSelectLevelListener()
131                         .invoke(fc.getELContext(), new Object[] {selectLevelEvent});
132             if (levelToEncode != mdlToGo.getLevel()) {
133                 // new MasterDetailLevel to go
134                 return masterDetail.getDetailLevelByLevel(levelToEncode);
135             }
136         }
137 
138         return mdlToGo;
139     }
140 
141     protected void encodeMarkup(final FacesContext fc, final MasterDetail masterDetail, final MasterDetailLevel mdl)
142                 throws IOException {
143         if (mdl == null) {
144             throw new FacesException("MasterDetailLevel must be nested inside a MasterDetail component!");
145         }
146         final ResponseWriter writer = fc.getResponseWriter();
147         final String clientId = masterDetail.getClientId(fc);
148         final String styleClass = masterDetail.getStyleClass() == null ? "pe-master-detail" : "pe-master-detail " + masterDetail.getStyleClass();
149 
150         writer.startElement("div", masterDetail);
151         writer.writeAttribute("id", clientId, "id");
152         writer.writeAttribute(Attrs.CLASS, styleClass, "styleClass");
153         if (masterDetail.getStyle() != null) {
154             writer.writeAttribute(Attrs.STYLE, masterDetail.getStyle(), Attrs.STYLE);
155         }
156 
157         if (masterDetail.isShowBreadcrumb()) {
158             if (masterDetail.isBreadcrumbAboveHeader()) {
159                 // render breadcrumb and then header
160                 renderBreadcrumb(fc, masterDetail, mdl);
161                 encodeFacet(fc, masterDetail, FACET_HEADER);
162             }
163             else {
164                 // render header and then breadcrumb
165                 encodeFacet(fc, masterDetail, FACET_HEADER);
166                 renderBreadcrumb(fc, masterDetail, mdl);
167             }
168         }
169         else {
170             // render header without breadcrumb
171             encodeFacet(fc, masterDetail, FACET_HEADER);
172         }
173 
174         // render container for MasterDetailLevel
175         writer.startElement("div", null);
176         writer.writeAttribute("id", clientId + "_detaillevel", "id");
177         writer.writeAttribute(Attrs.CLASS, "pe-master-detail-level", null);
178 
179         // try to get context value if contextVar exists
180         Object contextValue = null;
181         final String contextVar = mdl.getContextVar();
182         if (LangUtils.isNotBlank(contextVar)) {
183             contextValue = masterDetail.getContextValueFromFlow(fc, mdl, true);
184         }
185 
186         if (contextValue != null) {
187             final Map<String, Object> requestMap = fc.getExternalContext().getRequestMap();
188             requestMap.put(contextVar, contextValue);
189         }
190 
191         // render MasterDetailLevel
192         mdl.encodeAll(fc);
193 
194         if (contextValue != null) {
195             fc.getExternalContext().getRequestMap().remove(contextVar);
196         }
197 
198         writer.endElement("div");
199 
200         // render footer
201         encodeFacet(fc, masterDetail, FACET_FOOTER);
202         writer.endElement("div");
203     }
204 
205     protected void renderBreadcrumb(final FacesContext fc, final MasterDetail masterDetail, final MasterDetailLevel mdl)
206                 throws IOException {
207         // get breadcrumb and its current model
208         final BreadCrumb breadcrumb = masterDetail.getBreadcrumb();
209 
210         // update breadcrumb items
211         updateBreadcrumb(fc, breadcrumb, masterDetail, mdl);
212 
213         if (!masterDetail.isShowBreadcrumbFirstLevel()) {
214             final int levelToRender = mdl.getLevel();
215             if (levelToRender == 1) {
216                 breadcrumb.setStyleClass("ui-helper-hidden");
217             }
218             else {
219                 breadcrumb.setStyleClass("");
220             }
221         }
222 
223         // render breadcrumb
224         breadcrumb.encodeAll(fc);
225     }
226 
227     protected void encodeFacet(final FacesContext fc, final UIComponent component, final String name)
228                 throws IOException {
229         final UIComponent facet = component.getFacet(name);
230         if (FacetUtils.shouldRenderFacet(facet)) {
231             facet.encodeAll(fc);
232         }
233     }
234 
235     protected void updateBreadcrumb(final FacesContext fc, final BreadCrumb breadcrumb, final MasterDetail masterDetail,
236                 final MasterDetailLevel mdlToRender) throws IOException {
237         boolean lastMdlFound = false;
238         final int levelToRender = mdlToRender.getLevel();
239         final boolean isShowAllBreadcrumbItems = masterDetail.isShowAllBreadcrumbItems();
240 
241         for (final UIComponent child : masterDetail.getChildren()) {
242             if (child instanceof MasterDetailLevel) {
243                 final MasterDetailLevel mdl = (MasterDetailLevel) child;
244                 final DefaultMenuItem menuItem = getMenuItemByLevel(breadcrumb, masterDetail, mdl);
245                 if (menuItem == null) {
246                     // note: don't throw exception because menuItem can be null when MasterDetail is within DataTable
247                     return;
248                 }
249 
250                 if (!child.isRendered()) {
251                     menuItem.setRendered(false);
252                     if (!lastMdlFound) {
253                         lastMdlFound = mdl.getLevel() == mdlToRender.getLevel();
254                     }
255 
256                     continue;
257                 }
258 
259                 if (lastMdlFound && !isShowAllBreadcrumbItems) {
260                     menuItem.setRendered(false);
261                 }
262                 else {
263                     menuItem.setRendered(true);
264 
265                     final Object contextValue = masterDetail.getContextValueFromFlow(fc, mdl,
266                                 mdl.getLevel() == mdlToRender.getLevel());
267                     final String contextVar = mdl.getContextVar();
268                     final boolean putContext = LangUtils.isNotBlank(contextVar) && contextValue != null;
269 
270                     if (putContext) {
271                         final Map<String, Object> requestMap = fc.getExternalContext().getRequestMap();
272                         requestMap.put(contextVar, contextValue);
273                     }
274 
275                     final UIComponent facet = mdl.getFacet(FACET_LABEL);
276                     if (FacetUtils.shouldRenderFacet(facet)) {
277                         // swap writers
278                         final ResponseWriter writer = fc.getResponseWriter();
279                         final FastStringWriter fsw = new FastStringWriter();
280                         final ResponseWriter clonedWriter = writer.cloneWithWriter(fsw);
281                         fc.setResponseWriter(clonedWriter);
282 
283                         // render facet's children
284                         facet.encodeAll(fc);
285 
286                         // restore the original writer
287                         fc.setResponseWriter(writer);
288 
289                         // set menuitem label from facet
290                         menuItem.setValue(ExtLangUtils.unescapeXml(fsw.toString()));
291                     }
292                     else {
293                         // set menuitem label from tag attribute
294                         menuItem.setValue(mdl.getLevelLabel());
295                     }
296 
297                     if (isShowAllBreadcrumbItems && lastMdlFound) {
298                         menuItem.setDisabled(true);
299                     }
300                     else {
301                         menuItem.setDisabled(mdl.isLevelDisabled());
302                     }
303 
304                     if (putContext) {
305                         fc.getExternalContext().getRequestMap().remove(contextVar);
306                     }
307 
308                     if (!menuItem.isDisabled()) {
309                         // set current level parameter
310                         updateUIParameter(menuItem, masterDetail.getClientId(fc) + MasterDetail.CURRENT_LEVEL,
311                                     levelToRender);
312                     }
313                 }
314 
315                 if (!lastMdlFound) {
316                     lastMdlFound = mdl.getLevel() == mdlToRender.getLevel();
317                 }
318             }
319         }
320     }
321 
322     protected DefaultMenuItem getMenuItemByLevel(final BreadCrumb breadcrumb, final MasterDetail masterDetail,
323                 final MasterDetailLevel mdl) {
324         final String menuItemId = masterDetail.getId() + "_bcItem_" + mdl.getLevel();
325         for (final MenuElement child : breadcrumb.getModel().getElements()) {
326             if (menuItemId.equals(child.getId())) {
327                 return (DefaultMenuItem) child;
328             }
329         }
330 
331         return null;
332     }
333 
334     protected void updateUIParameter(final MenuItem menuItem, final String name, final Object value) {
335         final Map<String, List<String>> params = menuItem.getParams();
336         if (params == null) {
337             return;
338         }
339 
340         for (final String key : params.keySet()) {
341             if (key.equals(name)) {
342                 params.remove(key);
343                 menuItem.setParam(name, value);
344 
345                 break;
346             }
347         }
348     }
349 
350     @Override
351     public void encodeChildren(final FacesContext fc, final UIComponent component) {
352         // rendering happens on encodeEnd
353     }
354 
355     @Override
356     public boolean getRendersChildren() {
357         return true;
358     }
359 
360     public static Object getConvertedSubmittedValue(final FacesContext fc, final EditableValueHolder evh) {
361         final Object submittedValue = evh.getSubmittedValue();
362         if (submittedValue == null) {
363             return null;
364         }
365 
366         try {
367             final UIComponent component = (UIComponent) evh;
368             final Renderer renderer = getRenderer(fc, component);
369             if (renderer != null) {
370                 // convert submitted value by renderer
371                 return renderer.getConvertedValue(fc, component, submittedValue);
372             }
373             else if (submittedValue instanceof String) {
374                 // convert submitted value by registered (implicit or explicit)
375                 // converter
376                 final Converter converter = ComponentUtils.getConverter(fc, component);
377                 if (converter != null) {
378                     return converter.getAsObject(fc, component, (String) submittedValue);
379                 }
380             }
381         }
382         catch (final Exception e) {
383             // a conversion error occurred
384             return null;
385         }
386 
387         return submittedValue;
388     }
389 
390     public static Renderer getRenderer(final FacesContext fc, final UIComponent component) {
391         final String rendererType = component.getRendererType();
392         if (rendererType != null) {
393             return fc.getRenderKit().getRenderer(component.getFamily(), rendererType);
394         }
395 
396         return null;
397     }
398 }