View Javadoc

1   package org.apache.myfaces.tobago.ajax.api;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one or more
5    * contributor license agreements.  See the NOTICE file distributed with
6    * this work for additional information regarding copyright ownership.
7    * The ASF licenses this file to You under the Apache License, Version 2.0
8    * (the "License"); you may not use this file except in compliance with
9    * the License.  You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  
20  import org.apache.commons.logging.Log;
21  import org.apache.commons.logging.LogFactory;
22  import static org.apache.myfaces.tobago.TobagoConstants.ATTR_CHARSET;
23  import org.apache.myfaces.tobago.util.ComponentUtil;
24  import static org.apache.myfaces.tobago.lifecycle.TobagoLifecycle.FACES_MESSAGES_KEY;
25  import static org.apache.myfaces.tobago.lifecycle.TobagoLifecycle.VIEW_ROOT_KEY;
26  import org.apache.myfaces.tobago.util.EncodeAjaxCallback;
27  import org.apache.myfaces.tobago.util.RequestUtils;
28  import org.apache.myfaces.tobago.util.ResponseUtils;
29  import org.apache.myfaces.tobago.compat.FacesUtils;
30  import org.apache.myfaces.tobago.webapp.TobagoResponseJsonWriterImpl;
31  import org.apache.myfaces.tobago.context.TobagoFacesContext;
32  
33  import javax.faces.FactoryFinder;
34  import javax.faces.application.StateManager;
35  import javax.faces.component.UIComponent;
36  import javax.faces.component.UIViewRoot;
37  import javax.faces.context.ExternalContext;
38  import javax.faces.context.FacesContext;
39  import javax.faces.context.ResponseWriter;
40  import javax.faces.render.RenderKit;
41  import javax.faces.render.RenderKitFactory;
42  import javax.servlet.http.HttpServletResponse;
43  import java.io.IOException;
44  import java.io.PrintWriter;
45  import java.util.ArrayList;
46  import java.util.EmptyStackException;
47  import java.util.Iterator;
48  import java.util.List;
49  import java.util.Map;
50  
51  public class AjaxResponseRenderer {
52    public static final int CODE_SUCCESS = 200;
53    public static final int CODE_NOT_MODIFIED = 304;
54    public static final int CODE_RELOAD_REQUIRED = 309;
55    public static final int CODE_ERROR = 500;
56  
57    private static final Log LOG = LogFactory.getLog(AjaxResponseRenderer.class);
58  
59    private EncodeAjaxCallback callback;
60    private String contentType = "application/json";
61  
62    public AjaxResponseRenderer() {
63      callback = new EncodeAjaxCallback();
64    }
65  
66    public void renderResponse(FacesContext facesContext) throws IOException {
67      final UIViewRoot viewRoot = facesContext.getViewRoot();
68      RenderKitFactory renderFactory = (RenderKitFactory)
69          FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
70      RenderKit renderKit = renderFactory.getRenderKit(
71          facesContext, viewRoot.getRenderKitId());
72  
73      UIViewRoot incommingViewRoot = (UIViewRoot)
74          facesContext.getExternalContext().getRequestMap().get(VIEW_ROOT_KEY);
75      if (viewRoot != incommingViewRoot) {
76        if (LOG.isDebugEnabled()) {
77          LOG.debug("requesting full page reload because of navigation to "
78              + viewRoot.getViewId() + " from " + incommingViewRoot.getViewId());
79        }
80        Map sessionMap = facesContext.getExternalContext().getSessionMap();
81        //noinspection unchecked
82        sessionMap.put(VIEW_ROOT_KEY, viewRoot);
83        List<Object[]> messageHolders = new ArrayList<Object[]>();
84        Iterator clientIds = facesContext.getClientIdsWithMessages();
85        while (clientIds.hasNext()) {
86          String clientId = (String) clientIds.next();
87          Iterator messages = facesContext.getMessages(clientId);
88          while (messages.hasNext()) {
89            Object[] messageHolder = new Object[2];
90            messageHolder[0] = clientId;
91            messageHolder[1] = messages.next();
92            messageHolders.add(messageHolder);
93          }
94        }
95        if (!messageHolders.isEmpty()) {
96          //noinspection unchecked
97          sessionMap.put(FACES_MESSAGES_KEY, messageHolders);
98        }
99        writeResponse(facesContext, renderKit, true);
100     } else {
101       writeResponse(facesContext, renderKit, false);
102     }
103   }
104 
105   private void renderComponent(FacesContext facesContext, RenderKit renderKit, String clientId,
106       AjaxComponent component) throws IOException {
107     PrintWriter writer = getPrintWriter(facesContext.getExternalContext());
108     ResponseWriter contentWriter = renderKit.createResponseWriter(writer, contentType, null);
109     facesContext.setResponseWriter(contentWriter);
110 
111     if (LOG.isDebugEnabled()) {
112       LOG.debug("write ajax response for " + component);
113     }
114     writer.write("{\n    ajaxId: \"");
115     writer.write(clientId);
116     writer.write("\",\n");
117 
118     writer.write("    responseCode: ");
119     writer.write(Integer.toString(CODE_SUCCESS));
120     writer.write(",\n");
121 
122     writer.write("    html: \"");
123     try {
124       FacesUtils.invokeOnComponent(facesContext, facesContext.getViewRoot(), clientId, callback);
125     } catch (EmptyStackException e) {
126       //LOG.error(" content = \"" + content.toString() + "\"");
127       throw e;
128     }
129 
130     if (contentWriter instanceof TobagoResponseJsonWriterImpl) {
131       writer.write("\",\n");
132       writer.write("    script: function() {\n");
133       writer.write(((TobagoResponseJsonWriterImpl) contentWriter).getJavascript());
134       writer.write("\n    }");
135     }
136     writer.write("\n  }");
137   }
138 
139   private void saveState(FacesContext facesContext, RenderKit renderKit)
140       throws IOException {
141     ResponseWriter stateWriter =
142         renderKit.createResponseWriter(getPrintWriter(facesContext.getExternalContext()), contentType, null);
143     facesContext.setResponseWriter(stateWriter);
144 
145     StateManager stateManager = facesContext.getApplication().getStateManager();
146     StateManager.SerializedView serializedView = stateManager.saveSerializedView(facesContext);
147     stateManager.writeState(facesContext, serializedView);
148   }
149 
150   private static void ensureContentTypeHeader(FacesContext facesContext, String charset, String contentType) {
151     // TODO PortletRequest
152     if (facesContext.getExternalContext().getResponse() instanceof HttpServletResponse) {
153       HttpServletResponse response = (HttpServletResponse) facesContext.getExternalContext().getResponse();
154 
155       StringBuilder sb = new StringBuilder(contentType);
156       if (charset == null) {
157         charset = "UTF-8";
158       }
159       sb.append("; charset=");
160       sb.append(charset);
161       response.setContentType(sb.toString());
162     }
163   }
164 
165   private void writeResponse(FacesContext facesContext, RenderKit renderKit, boolean reloadRequired)
166       throws IOException {
167 
168     ExternalContext externalContext = facesContext.getExternalContext();
169     RequestUtils.ensureEncoding(externalContext);
170     ResponseUtils.ensureNoCacheHeader(externalContext);
171     UIComponent page = ComponentUtil.findPage(facesContext);
172     String charset;
173     if (page != null) {  // in case of CODE_RELOAD_REQUIRED page is null
174       charset = (String) page.getAttributes().get(ATTR_CHARSET);
175     } else {
176       charset = "UTF-8";
177     }
178     ensureContentTypeHeader(facesContext, charset, contentType);
179 
180     PrintWriter writer = getPrintWriter(externalContext);
181     writer.write("{\n  tobagoAjaxResponse: true,\n");
182     writer.write("  responseCode: ");
183     writer.write(reloadRequired ? Integer.toString(CODE_RELOAD_REQUIRED) : Integer.toString(CODE_SUCCESS));
184 
185     if (!reloadRequired) {
186       writer.write(",\n");
187       writer.write("  jsfState: \"");
188       saveState(facesContext, renderKit);
189       writer.write("\"");
190     }
191 
192     Map<String, UIComponent> ajaxComponents = AjaxUtils.getAjaxComponents(facesContext);
193     int i = 0;
194     for (Map.Entry<String, UIComponent> entry : ajaxComponents.entrySet()) {
195       writer.write(",\n");
196       writer.write("  ajaxPart_");
197       writer.write(Integer.toString(i++));
198       writer.write(": ");
199 
200       AjaxComponent component = (AjaxComponent) entry.getValue();
201       if (facesContext instanceof TobagoFacesContext) {
202         ((TobagoFacesContext) facesContext).setAjaxComponentId(entry.getKey());
203       }
204       renderComponent(facesContext, renderKit, entry.getKey(), component);
205     }
206 
207     writer.write("\n}\n");
208     writer.flush();
209     writer.close();
210   }
211 
212   private PrintWriter getPrintWriter(ExternalContext externalContext) throws IOException {
213     //TODO: fix this to work in PortletRequest as well
214     if (externalContext.getResponse() instanceof HttpServletResponse) {
215       final HttpServletResponse httpServletResponse
216           = (HttpServletResponse) externalContext.getResponse();
217       return httpServletResponse.getWriter();
218     }
219     throw new IOException("No ResponseWriter found for ExternalContext " + externalContext);
220   }
221 }