View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.custom.ppr;
20  
21  import org.apache.commons.collections.CollectionUtils;
22  import org.apache.commons.lang.StringUtils;
23  import org.apache.commons.logging.Log;
24  import org.apache.commons.logging.LogFactory;
25  import org.apache.myfaces.component.html.ext.HtmlMessages;
26  import org.apache.myfaces.component.html.ext.UIComponentPerspective;
27  import org.apache.myfaces.shared_tomahawk.component.ExecuteOnCallback;
28  import org.apache.myfaces.shared_tomahawk.renderkit.html.HtmlRendererUtils;
29  import org.apache.myfaces.shared_tomahawk.renderkit.html.HtmlResponseWriterImpl;
30  
31  import javax.faces.FacesException;
32  import javax.faces.application.FacesMessage;
33  import javax.faces.application.StateManager;
34  import javax.faces.component.UIComponent;
35  import javax.faces.component.UIViewRoot;
36  import javax.faces.context.ExternalContext;
37  import javax.faces.context.FacesContext;
38  import javax.faces.event.PhaseEvent;
39  import javax.faces.event.PhaseId;
40  import javax.faces.event.PhaseListener;
41  import javax.servlet.ServletRequest;
42  import javax.servlet.ServletResponse;
43  import java.io.IOException;
44  import java.io.PrintWriter;
45  import java.util.ArrayList;
46  import java.util.Collection;
47  import java.util.HashSet;
48  import java.util.Iterator;
49  import java.util.List;
50  import java.util.Map;
51  import java.util.Set;
52  import java.util.StringTokenizer;
53  
54  /**
55   * Before RenderResponse PhaseListener for processing Ajax requests from
56   * {@link PPRPanelGroup}. It also participates in handling transient components
57   * in PPR Requests
58   *
59   * @author Ernst Fastl
60   */
61  public class PPRPhaseListener implements PhaseListener
62  {
63      private static Log log = LogFactory.getLog(PPRPhaseListener.class);
64  
65      /**
66       * Request parameter which marks a request as PPR request
67       */
68      private static final String PPR_PARAMETER = "org.apache.myfaces.PPRCtrl.ajaxRequest";
69  
70      /**
71       * Request parameter containing a comma separated list of component IDs
72       * of the to be updated components
73       */
74      private static final String TRIGGERED_COMPONENTS_PARAMETER = "org.apache.myfaces.PPRCtrl.triggeredComponents";
75  
76      private static final String XML_HEADER = "<?xml version=\"1.0\"?>\n";
77  
78      public void afterPhase(PhaseEvent phaseEvent)
79      {
80      }
81  
82      /**
83       * Determines wether the currently processed request is a PPR request
84       * (by searching for PPR_PARAMETER in the request parameter map) or an
85       * ordinary HTTP request. If the request is a PPR request the triggered
86       * components are encoded. Otherwise transient components which have
87       * previously been marked not transient by the
88       * {@link PPRPanelGroupRenderer} are set to transient again
89       */
90      public void beforePhase(PhaseEvent event)
91      {
92          if (log.isDebugEnabled()) {
93              log.debug("In PPRPhaseListener beforePhase");
94          }
95  
96          final FacesContext context = event.getFacesContext();
97          final ExternalContext externalContext = context.getExternalContext();
98  
99          Map requestMap = externalContext.getRequestMap();
100 
101         if (isPartialRequest(context)) {
102             processPartialPageRequest(context, externalContext, requestMap);
103         }
104         else {
105             // Iterate over the component tree and set all previously
106             // transient components to transient again
107             resetTransientComponents(context.getViewRoot());
108         }
109     }
110 
111     /**
112      * if the provided component was marked transient in the last request
113      * set it to transient. Recursively do the same for all children
114      *
115      * @param comp the component to reset
116      */
117     private void resetTransientComponents(UIComponent comp)
118     {
119         if (comp.getAttributes().containsKey(PPRPanelGroupRenderer.TRANSIENT_MARKER_ATTRIBUTE)) {
120             comp.setTransient(true);
121         }
122         for (Iterator iter = comp.getChildren().iterator(); iter.hasNext();) {
123             UIComponent child = (UIComponent) iter.next();
124             resetTransientComponents(child);
125         }
126     }
127 
128     /**
129      * Checks if the currently processed Request is an AJAX request from a
130      * PPRPanelGroup
131      *
132      * @param context the current {@link FacesContext}
133      * @return true if a PPR request is being processed , false otherwise
134      */
135     public static boolean isPartialRequest(FacesContext context)
136     {
137         return context.getExternalContext().getRequestParameterMap().containsKey(PPR_PARAMETER);
138     }
139 
140     /**
141      * Respond to an AJAX request from a {@link PPRPanelGroup}. The
142      * triggered components are determined by reading the
143      * TRIGGERED_COMPONENTS_PARAMETER from either the RequestParameterMap or
144      * the Request Map. Those componenets are encoded into an XML response.
145      * The lifecycle is quit afterwards.
146      *
147      * @param context         the current {@link FacesContext}
148      * @param externalContext the current {@link ExternalContext}
149      * @param requestMap      Map containing the request attributes
150      */
151     private void processPartialPageRequest(FacesContext context, final ExternalContext externalContext, Map requestMap)
152     {
153 
154         ServletResponse response = (ServletResponse) externalContext.getResponse();
155         ServletRequest request = (ServletRequest) externalContext.getRequest();
156 
157         UIViewRoot viewRoot = context.getViewRoot();
158 
159         // Set Character encoding, contentType and locale for the response
160         final String characterEncoding = request.getCharacterEncoding();
161         String contentType = getContentType("text/xml", characterEncoding);
162         response.setContentType(contentType);
163         response.setLocale(viewRoot.getLocale());
164 
165         // Fetch the comma-separated list of triggered components
166         String triggeredComponents = getTriggeredComponents(context);
167 
168         try {
169             PrintWriter out = response.getWriter();
170             context.setResponseWriter(new HtmlResponseWriterImpl(out, contentType, characterEncoding));
171             out.print(XML_HEADER);
172             out.print("<response>\n");
173             encodeTriggeredComponents(out, triggeredComponents, viewRoot, context);
174             out.print("</response>");
175             out.flush();
176         }
177         catch (IOException e) {
178             throw new FacesException(e);
179         }
180 
181         context.responseComplete();
182     }
183 
184     /**
185      * Fetch the comma-separated list of triggered components. They are
186      * either obtained from the Request Parameter Map where they had
187      * previously been set using
188      * {@link PPRPhaseListener#addTriggeredComponent(FacesContext, String))
189      * or from the request parameter map.
190      *
191      * @param fc the current {@link FacesContext}
192      * @return a comma separated list of component IDs of the components
193      *         which are to be updated
194      */
195     private static String getTriggeredComponents(FacesContext fc)
196     {
197         String triggeredComponents = (String) fc.getExternalContext().getRequestMap().get(TRIGGERED_COMPONENTS_PARAMETER);
198 
199         if (triggeredComponents == null) {
200             triggeredComponents = (String) fc.getExternalContext().getRequestParameterMap().get(TRIGGERED_COMPONENTS_PARAMETER);
201         }
202 
203         return triggeredComponents;
204     }
205 
206     /**
207      * API method for adding triggeredComponents programmatically.
208      *
209      * @param fc                         the current {@link FacesContext}
210      * @param triggeredComponentClientId client ID of the component which is to be updated in
211      *                                   case of a PPR Response
212      */
213     public static void addTriggeredComponent(FacesContext fc, String triggeredComponentClientId)
214     {
215         String triggeredComponents = getTriggeredComponents(fc);
216 
217         if (triggeredComponents == null || triggeredComponents.trim().length() == 0) {
218             triggeredComponents = new String();
219         }
220         else {
221             triggeredComponents = triggeredComponents + ",";
222         }
223 
224         triggeredComponents = triggeredComponents + triggeredComponentClientId;
225 
226         fc.getExternalContext().getRequestMap().put(TRIGGERED_COMPONENTS_PARAMETER, triggeredComponents);
227     }
228 
229     /**
230      * Generate content-type String either containing only the mime-type or
231      * mime-type and character enconding.
232      *
233      * @param contentType the contentType/mimeType
234      * @param charset     the character set
235      * @return the content-type String to be used in an HTTP response
236      */
237     private String getContentType(String contentType, String charset)
238     {
239         if (charset == null || charset.trim().length() == 0) {
240             return contentType;
241         }
242         else {
243             return contentType + ";charset=" + charset;
244         }
245     }
246 
247     /**
248      * Writes the XML elements for the triggered components to the provided
249      * {@link PrintWriter}. Also encode the current state in a separate XML
250      * element.
251      *
252      * @param out                 the output Writer
253      * @param triggeredComponents comma-separated list of component IDs
254      * @param viewRoot            the current ViewRoot
255      * @param context             the current {@link FacesContext}
256      */
257     private void encodeTriggeredComponents(PrintWriter out, String triggeredComponents, UIViewRoot viewRoot, FacesContext context)
258     {
259         StringTokenizer st = new StringTokenizer(triggeredComponents, ",", false);
260         String clientId;
261         UIComponent component;
262         boolean handleState = true;
263 
264 
265         Set toAppendMessagesComponents = new HashSet();
266         Set toReplaceMessagesComponents = new HashSet();
267 
268         // Iterate over the individual client IDs
269         while (st.hasMoreTokens()) {
270             clientId = st.nextToken();
271             component = viewRoot.findComponent(clientId);
272             if (component != null) {
273                 //get info about state writing/rendering
274                 //if at least one ppr does not update the state
275                 //the response will not include state information
276                 PPRPanelGroup ppr = null;
277                 int oldIndex = 0;
278                 if (component instanceof UIComponentPerspective) {
279                     UIComponentPerspective uiComponentPerspective = (UIComponentPerspective) component;
280                     ExecuteOnCallback getComponentCallback = new ExecuteOnCallback()
281                     {
282                         public Object execute(FacesContext context, UIComponent component)
283                         {
284                             return component;
285                         }
286                     };
287                     Object retval = uiComponentPerspective.executeOn(context, getComponentCallback);
288                     if (retval instanceof PPRPanelGroup) {
289                         ppr = (PPRPanelGroup) retval;
290                     }
291                     else {
292                         throw new IllegalArgumentException("Expect PPRPanelGroup or UiComponentPerspective");
293                     }
294                     //setup perspective
295                     oldIndex = uiComponentPerspective.getUiData().getRowIndex();
296                     uiComponentPerspective.getUiData().setRowIndex(uiComponentPerspective.getRowIndex());
297                 }
298                 else if (component instanceof PPRPanelGroup) {
299                     ppr = (PPRPanelGroup) component;
300                 }
301                 else {
302                     throw new IllegalArgumentException("Expect PPRPanelGroup or UiComponentPerspective");
303                 }
304                 if (ppr.getStateUpdate().booleanValue() == false) {
305                     handleState = false;
306                 }
307                 //Check which messages components this group wants to append to
308                 if (ppr.getAppendMessages() != null) {
309                     List appendMessagesForThisGroup = PPRSupport.getComponentsByCommaSeparatedIdList(context, ppr, ppr.getAppendMessages(), HtmlMessages.class);
310                     toAppendMessagesComponents.addAll(appendMessagesForThisGroup);
311                 }
312 
313                 //Check which messages components this group should refresh
314                 if (ppr.getReplaceMessages() != null) {
315                     List replaceMessagesForThisGroup = PPRSupport.getComponentsByCommaSeparatedIdList(context, ppr, ppr.getReplaceMessages(), HtmlMessages.class);
316                     toReplaceMessagesComponents.addAll(replaceMessagesForThisGroup);
317                 }
318 
319                 // Write a component tag which contains a CDATA section whith
320                 // the rendered HTML
321                 // of the component children
322                 out.print("<component id=\"" + component.getClientId(context) + "\"><![CDATA[");
323                 boolean oldValue = HtmlRendererUtils.isAllowedCdataSection(context);
324                 HtmlRendererUtils.allowCdataSection(context, false);
325                 try {
326                     component.encodeChildren(context);
327                 }
328                 catch (IOException e) {
329                     throw new FacesException(e);
330                 }
331                 HtmlRendererUtils.allowCdataSection(context, oldValue);
332                 out.print("]]></component>");
333 
334                 //tear down perspective
335                 if (component instanceof UIComponentPerspective) {
336                     UIComponentPerspective uiComponentPerspective = (UIComponentPerspective) component;
337                     uiComponentPerspective.getUiData().setRowIndex(oldIndex);
338                 }
339             }
340             else {
341                 log.debug("PPRPhaseListener component with id" + clientId + "not found!");
342             }
343         }
344 
345         boolean handleFacesMessages = !toAppendMessagesComponents.isEmpty() || !toReplaceMessagesComponents.isEmpty();
346 
347         if (handleFacesMessages) {   //encode all facesMessages into  xml-elements
348             //for starter just return all messages (bother with client IDs later on)
349             Iterator messagesIterator = context.getMessages();
350 
351             //only write messages-elements if messages are present
352             while (messagesIterator.hasNext()) {
353                 FacesMessage msg = (FacesMessage) messagesIterator.next();
354                 String messageText = msg.getSummary() + " " + msg.getDetail();
355                 out.print("<message><![CDATA[");
356                 out.print(messageText);
357                 out.print("]]></message>");
358             }
359 
360             //Replace has precedence over append
361             Collection intersection = CollectionUtils.intersection(toAppendMessagesComponents, toReplaceMessagesComponents);
362             for (Iterator iterator = intersection.iterator(); iterator.hasNext();) {
363                 UIComponent uiComponent = (UIComponent) iterator.next();
364                 log.warn("Component with id " + uiComponent.getClientId(context) +
365                     " is marked for replace and append messages -> replace has precedence");
366                 toAppendMessagesComponents.remove(uiComponent);
367             }
368 
369             for (Iterator iterator = toAppendMessagesComponents.iterator(); iterator.hasNext();) {
370                 UIComponent uiComponent = (UIComponent) iterator.next();
371                 out.print("<append id=\"");
372                 out.print(uiComponent.getClientId(context));
373                 out.print("\"/>");
374             }
375 
376             for (Iterator iterator = toReplaceMessagesComponents.iterator(); iterator.hasNext();) {
377                 UIComponent uiComponent = (UIComponent) iterator.next();
378                 out.print("<replace id=\"");
379                 out.print(uiComponent.getClientId(context));
380                 out.print("\"/>");
381             }
382         }
383 
384         if (handleState) {
385             // Write the serialized state into a separate XML element
386             out.print("<state>");
387             FacesContext facesContext = FacesContext.getCurrentInstance();
388             StateManager stateManager = facesContext.getApplication().getStateManager();
389             StateManager.SerializedView serializedView = stateManager.saveSerializedView(facesContext);
390             try {
391                 stateManager.writeState(facesContext, serializedView);
392             }
393             catch (IOException e) {
394                 throw new FacesException(e);
395             }
396             out.print("</state>");
397         }
398 
399     }
400 
401 
402     private static List getComponentsByCommaSeparatedList(FacesContext context, UIComponent comp, String commaSeparatedIdList, Class componentType)
403     {
404         List retval = new ArrayList();
405         UIComponent currentComponent = null;
406         String[] componentIds = StringUtils.split(commaSeparatedIdList, ',');
407         for (int i = 0; i < componentIds.length; i++) {
408             String componentId = componentIds[i];
409 
410         }
411         return retval;
412     }
413 
414     public PhaseId getPhaseId()
415     {
416         return PhaseId.RENDER_RESPONSE;
417     }
418 }