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  
20  package org.apache.myfaces.tobago.internal.ajax;
21  
22  import org.apache.myfaces.tobago.component.Attributes;
23  import org.apache.myfaces.tobago.internal.lifecycle.TobagoLifecycle;
24  import org.apache.myfaces.tobago.internal.util.FacesContextUtils;
25  import org.apache.myfaces.tobago.internal.util.ResponseUtils;
26  import org.apache.myfaces.tobago.internal.webapp.JsonResponseWriter;
27  import org.apache.myfaces.tobago.util.ComponentUtils;
28  import org.apache.myfaces.tobago.util.EncodeAjaxCallback;
29  import org.apache.myfaces.tobago.util.RequestUtils;
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
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.Map;
46  
47  public class AjaxResponseRenderer {
48  
49    public static final int CODE_SUCCESS = 200;
50    /**
51     * @deprecated since 1.5. May do some stuff on the response like in ApplyRequestValuesCallback.
52     */
53    @Deprecated
54    public static final int CODE_NOT_MODIFIED = 304;
55    public static final int CODE_RELOAD_REQUIRED = 309;
56    public static final int CODE_ERROR = 500;
57    public static final String CONTENT_TYPE = "application/json";
58    private static final Logger LOG = LoggerFactory.getLogger(AjaxResponseRenderer.class);
59  
60    private EncodeAjaxCallback callback;
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 incomingViewRoot = (UIViewRoot)
74          facesContext.getExternalContext().getRequestMap().get(TobagoLifecycle.VIEW_ROOT_KEY);
75      LOG.error("TODO: Reimplement AJAX stuff for JSF 2.0");
76  /* XXX: reimplement it for JSF 2.0? Works only, when the TobagoLifecycle is active...
77      if (viewRoot != incomingViewRoot) {
78        if (LOG.isDebugEnabled()) {
79          LOG.debug("requesting full page reload because of navigation to {} from {}",
80              viewRoot.getViewId(), incomingViewRoot.getViewId());
81        }
82        Map sessionMap = facesContext.getExternalContext().getSessionMap();
83        //noinspection unchecked
84        sessionMap.put(TobagoLifecycle.VIEW_ROOT_KEY, viewRoot);
85        List<Object[]> messageHolders = new ArrayList<Object[]>();
86        Iterator clientIds = facesContext.getClientIdsWithMessages();
87        while (clientIds.hasNext()) {
88          String clientId = (String) clientIds.next();
89          Iterator messages = facesContext.getMessages(clientId);
90          while (messages.hasNext()) {
91            Object[] messageHolder = new Object[2];
92            messageHolder[0] = clientId;
93            messageHolder[1] = messages.next();
94            messageHolders.add(messageHolder);
95          }
96        }
97        if (!messageHolders.isEmpty()) {
98          //noinspection unchecked
99          sessionMap.put(TobagoLifecycle.FACES_MESSAGES_KEY, messageHolders);
100       }
101       writeResponse(facesContext, renderKit, true);
102     } else {
103 */
104       writeResponse(facesContext, renderKit, false);
105 //    }
106   }
107 
108   private void renderComponent(FacesContext facesContext, RenderKit renderKit, String clientId, UIComponent component)
109       throws IOException {
110     final PrintWriter writer = getPrintWriter(facesContext);
111     final JsonResponseWriter jsonWriter = getJsonResponseWriter(renderKit, writer);
112 
113     facesContext.setResponseWriter(jsonWriter);
114 
115     if (LOG.isDebugEnabled()) {
116       LOG.debug("write ajax response for {}", component);
117     }
118     writer.write("{\n    \"ajaxId\": \"");
119     writer.write(clientId);
120     writer.write("\",\n");
121 
122     writer.write("    \"html\": \"");
123     ComponentUtils.invokeOnComponent(facesContext, facesContext.getViewRoot(), clientId, callback);
124     writer.write("\",\n");
125 
126     writer.write("    \"responseCode\": ");
127     writer.write(Integer.toString(CODE_SUCCESS));
128 
129     writer.write(",\n");
130     writer.write("    \"script\": \"function() { ");
131     writer.write(jsonWriter.getJavascript());
132     writer.write(" }\"");
133 
134     writer.write("\n  }");
135   }
136 
137   private void saveState(FacesContext facesContext, RenderKit renderKit) throws IOException {
138 
139     ResponseWriter stateWriter = renderKit.createResponseWriter(getPrintWriter(facesContext), CONTENT_TYPE, null);
140     facesContext.setResponseWriter(stateWriter);
141 
142     StateManager stateManager = facesContext.getApplication().getStateManager();
143     StateManager.SerializedView serializedView = stateManager.saveSerializedView(facesContext);
144     stateManager.writeState(facesContext, serializedView);
145   }
146 
147   private static void ensureContentTypeHeader(FacesContext facesContext, String charset, String contentType) {
148     StringBuilder sb = new StringBuilder(contentType);
149     if (charset == null) {
150       charset = "UTF-8";
151     }
152     sb.append("; charset=");
153     sb.append(charset);
154     ResponseUtils.ensureContentTypeHeader(facesContext, sb.toString());
155   }
156 
157   private void writeResponse(FacesContext facesContext, RenderKit renderKit, boolean reloadRequired)
158       throws IOException {
159 
160     RequestUtils.ensureEncoding(facesContext);
161     ResponseUtils.ensureNoCacheHeader(facesContext);
162     UIComponent page = ComponentUtils.findPage(facesContext);
163     String charset;
164     if (page != null) {  // in case of CODE_RELOAD_REQUIRED page is null
165       charset = (String) page.getAttributes().get(Attributes.CHARSET);
166     } else {
167       charset = "UTF-8";
168     }
169     ensureContentTypeHeader(facesContext, charset, CONTENT_TYPE);
170 
171     PrintWriter writer = getPrintWriter(facesContext);
172     writer.write("{\n  \"tobagoAjaxResponse\": true,\n");
173     writer.write("  \"responseCode\": ");
174     writer.write(reloadRequired ? Integer.toString(CODE_RELOAD_REQUIRED) : Integer.toString(CODE_SUCCESS));
175 
176     Map<String, UIComponent> ajaxComponents = AjaxInternalUtils.getAjaxComponents(facesContext);
177     if (ajaxComponents != null) {
178       int i = 0;
179       for (Map.Entry<String, UIComponent> entry : ajaxComponents.entrySet()) {
180         writer.write(",\n");
181         writer.write("  \"ajaxPart_");
182         writer.write(Integer.toString(i++));
183         writer.write("\": ");
184 
185         UIComponent component = entry.getValue();
186         FacesContextUtils.setAjaxComponentId(facesContext, entry.getKey());
187         renderComponent(facesContext, renderKit, entry.getKey(), component);
188       }
189     }
190 
191     if (!reloadRequired) {
192       writer.write(",\n");
193       writer.write("  \"jsfState\": \"");
194       saveState(facesContext, renderKit);
195       writer.write("\"");
196     }
197 
198     writer.write("\n}\n");
199     writer.flush();
200     writer.close();
201   }
202 
203   private PrintWriter getPrintWriter(FacesContext facesContext) throws IOException {
204     ExternalContext externalContext = facesContext.getExternalContext();
205     //TODO: fix this to work in PortletRequest as well
206     if (externalContext.getResponse() instanceof HttpServletResponse) {
207       final HttpServletResponse httpServletResponse
208           = (HttpServletResponse) externalContext.getResponse();
209       return httpServletResponse.getWriter();
210     }
211     throw new IOException("No ResponseWriter found for ExternalContext " + externalContext);
212   }
213 
214   private JsonResponseWriter getJsonResponseWriter(RenderKit renderKit, PrintWriter writer) {
215 
216     ResponseWriter newWriter = renderKit.createResponseWriter(writer, CONTENT_TYPE, null);
217     if (newWriter instanceof JsonResponseWriter) {
218       return (JsonResponseWriter) newWriter;
219     } else {
220       // with different RenderKit we got not the correct class
221       return new JsonResponseWriter(newWriter, CONTENT_TYPE, newWriter.getCharacterEncoding());
222     }
223   }
224 }