001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied. See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019
020 package org.apache.myfaces.tobago.ajax.api;
021
022 import org.apache.commons.lang.StringUtils;
023 import org.apache.commons.logging.Log;
024 import org.apache.commons.logging.LogFactory;
025 import static org.apache.myfaces.tobago.TobagoConstants.ATTR_CHARSET;
026 import org.apache.myfaces.tobago.component.ComponentUtil;
027 import static org.apache.myfaces.tobago.lifecycle.TobagoLifecycle.FACES_MESSAGES_KEY;
028 import static org.apache.myfaces.tobago.lifecycle.TobagoLifecycle.VIEW_ROOT_KEY;
029 import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
030 import org.apache.myfaces.tobago.renderkit.html.HtmlConstants;
031 import org.apache.myfaces.tobago.util.Callback;
032 import org.apache.myfaces.tobago.util.EncodeAjaxCallback;
033 import org.apache.myfaces.tobago.util.FastStringWriter;
034 import org.apache.myfaces.tobago.util.JndiUtils;
035 import org.apache.myfaces.tobago.util.RequestUtils;
036 import org.apache.myfaces.tobago.util.ResponseUtils;
037
038 import javax.faces.FactoryFinder;
039 import javax.faces.application.StateManager;
040 import javax.faces.component.UIComponent;
041 import javax.faces.component.UIViewRoot;
042 import javax.faces.context.ExternalContext;
043 import javax.faces.context.FacesContext;
044 import javax.faces.context.ResponseWriter;
045 import javax.faces.render.RenderKit;
046 import javax.faces.render.RenderKitFactory;
047 import javax.naming.InitialContext;
048 import javax.naming.NamingException;
049 import javax.servlet.http.HttpServletResponse;
050 import java.io.IOException;
051 import java.io.PrintWriter;
052 import java.util.ArrayList;
053 import java.util.Collections;
054 import java.util.EmptyStackException;
055 import java.util.Iterator;
056 import java.util.List;
057 import java.util.Map;
058
059 public class AjaxResponseRenderer {
060
061 private static final Log LOG = LogFactory.getLog(AjaxResponseRenderer.class);
062
063 public static final String CODE_SUCCESS = "<status code=\"200\"/>";
064 public static final String CODE_NOT_MODIFIED = "<status code=\"304\"/>";
065 public static final String CODE_RELOAD_REQUIRED = "<status code=\"309\"/>";
066
067 private Callback callback;
068 private String contentType;
069
070 public AjaxResponseRenderer() {
071 callback = new EncodeAjaxCallback();
072 try {
073 InitialContext ic = new InitialContext();
074 contentType = (String) JndiUtils.getJndiProperty(ic, "tobago.ajax.contentType");
075 } catch (NamingException e) { /*ignore*/ }
076
077 if (StringUtils.isBlank(contentType)) {
078 contentType = "text/html";
079 }
080 }
081
082 public void renderResponse(FacesContext facesContext) throws IOException {
083 final UIViewRoot viewRoot = facesContext.getViewRoot();
084 RenderKitFactory renderFactory = (RenderKitFactory)
085 FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
086 RenderKit renderKit = renderFactory.getRenderKit(
087 facesContext, viewRoot.getRenderKitId());
088
089 UIViewRoot incommingViewRoot = (UIViewRoot)
090 facesContext.getExternalContext().getRequestMap().get(VIEW_ROOT_KEY);
091 if (viewRoot != incommingViewRoot) {
092 if (LOG.isDebugEnabled()) {
093 LOG.debug("requesting full page reload because of navigation to "
094 + viewRoot.getViewId() + " from " + incommingViewRoot.getViewId());
095 }
096 Map sessionMap = facesContext.getExternalContext().getSessionMap();
097 //noinspection unchecked
098 sessionMap.put(VIEW_ROOT_KEY, viewRoot);
099 List<Object[]> messageHolders = new ArrayList<Object[]>();
100 Iterator clientIds = facesContext.getClientIdsWithMessages();
101 while (clientIds.hasNext()) {
102 String clientId = (String) clientIds.next();
103 Iterator messages = facesContext.getMessages(clientId);
104 while (messages.hasNext()) {
105 Object[] messageHolder = new Object[2];
106 messageHolder[0] = clientId;
107 messageHolder[1] = messages.next();
108 messageHolders.add(messageHolder);
109 }
110 }
111 if (!messageHolders.isEmpty()) {
112 //noinspection unchecked
113 sessionMap.put(FACES_MESSAGES_KEY, messageHolders);
114 }
115 writeResponseReload(facesContext, renderKit);
116 } else {
117 List<FastStringWriter> responseParts = new ArrayList<FastStringWriter>();
118 Map<String, UIComponent> ajaxComponents = AjaxUtils.getAjaxComponents(facesContext);
119
120 for (Map.Entry<String, UIComponent> entry : ajaxComponents.entrySet()) {
121 AjaxComponent component = (AjaxComponent) entry.getValue();
122 responseParts.add(renderComponent(facesContext, renderKit, entry.getKey(), component));
123 break; // TODO render multiple components
124 }
125
126 String state = saveState(facesContext, renderKit);
127 writeResponse(facesContext, renderKit, responseParts, state);
128 }
129 }
130
131 private FastStringWriter renderComponent(FacesContext facesContext, RenderKit renderKit, String clientId,
132 AjaxComponent component) throws IOException {
133 FastStringWriter content = new FastStringWriter();
134 ResponseWriter contentWriter = renderKit.createResponseWriter(content, null, null);
135 facesContext.setResponseWriter(contentWriter);
136 if (LOG.isDebugEnabled()) {
137 LOG.debug("write ajax response for " + component);
138 }
139
140 try {
141 // TODO: invokeOnComponent()
142 ComponentUtil.invokeOnComponent(facesContext, clientId, (UIComponent) component, callback);
143 } catch (EmptyStackException e) {
144 LOG.error(" content = \"" + content.toString() + "\"");
145 throw e;
146 }
147
148 return content;
149 }
150
151 private void writeResponse(FacesContext facesContext, RenderKit renderKit,
152 List<FastStringWriter> parts, String state)
153 throws IOException {
154 writeResponse(facesContext, renderKit, CODE_SUCCESS, parts, state);
155 }
156
157 private void writeResponseReload(FacesContext facesContext, RenderKit renderKit)
158 throws IOException {
159 writeResponse(facesContext, renderKit, CODE_RELOAD_REQUIRED, Collections.EMPTY_LIST, "");
160 }
161
162
163 private FastStringWriter writeState(FacesContext facesContext, RenderKit renderKit, String state)
164 throws IOException {
165 FastStringWriter jsfState = new FastStringWriter();
166 ResponseWriter stateWriter = renderKit.createResponseWriter(jsfState, null, null);
167 facesContext.setResponseWriter(stateWriter);
168 stateWriter.startElement(HtmlConstants.SCRIPT, null);
169 stateWriter.writeAttribute(HtmlAttributes.TYPE, "text/javascript", null);
170 stateWriter.flush();
171 stateWriter.write("Tobago.replaceJsfState(\"");
172 stateWriter.write(encodeState(state));
173 stateWriter.write("\");");
174 stateWriter.endElement(HtmlConstants.SCRIPT);
175 return jsfState;
176 }
177
178 private String saveState(FacesContext facesContext, RenderKit renderKit)
179 throws IOException {
180 FastStringWriter jsfState = new FastStringWriter();
181 ResponseWriter stateWriter = renderKit.createResponseWriter(jsfState, null, null);
182 facesContext.setResponseWriter(stateWriter);
183
184 StateManager stateManager = facesContext.getApplication().getStateManager();
185 StateManager.SerializedView serializedView
186 = stateManager.saveSerializedView(facesContext);
187 stateManager.writeState(facesContext, serializedView);
188 return jsfState.toString();
189 }
190
191 private static void ensureContentTypeHeader(FacesContext facesContext, String charset, String contentType) {
192 StringBuilder sb = new StringBuilder(contentType);
193 if (charset == null) {
194 charset = "UTF-8";
195 }
196 sb.append("; charset=");
197 sb.append(charset);
198 ResponseUtils.ensureContentTypeHeader(facesContext, sb.toString());
199
200 }
201
202 private void writeResponse(FacesContext facesContext, RenderKit renderKit,
203 String responseCode, List<FastStringWriter> responseParts, String jsfState)
204 throws IOException {
205 RequestUtils.ensureEncoding(facesContext);
206 ResponseUtils.ensureNoCacheHeader(facesContext);
207 UIComponent page = ComponentUtil.findPage(facesContext);
208 String charset;
209 if (page != null) { // in case of CODE_RELOAD_REQUIRED page is null
210 charset = (String) page.getAttributes().get(ATTR_CHARSET);
211 } else {
212 charset = "UTF-8";
213 }
214 ensureContentTypeHeader(facesContext, charset, contentType);
215 StringBuilder buffer = new StringBuilder(responseCode);
216
217 // add parts to response
218 for (FastStringWriter part : responseParts) {
219 // TODO surround by javascript parsable tokens
220 String partStr = part.toString();
221 // FIXME:
222 if (partStr.startsWith(responseCode)
223 || partStr.equals(CODE_NOT_MODIFIED)) {
224 // remove responseCode from buffer
225 buffer.setLength(0);
226 }
227 // FIXME:
228
229 buffer.append(partStr);
230 }
231
232 // add jsfState to response
233 if (jsfState.length() > 0) {
234 // in case of inputSuggest jsfState.lenght is 0
235 // inputSuggest is a special case, because the form is not included in request.
236 // TODO surround by javascript parsable token
237 buffer.append(writeState(facesContext, renderKit, jsfState));
238 }
239
240 if (LOG.isTraceEnabled()) {
241 LOG.trace("\nresponse follows ##############################################################\n"
242 + buffer
243 + "\nend response ##############################################################");
244 }
245
246
247 ExternalContext externalContext = facesContext.getExternalContext();
248 //TODO: fix this to work in PortletRequest as well
249 if (externalContext.getResponse() instanceof HttpServletResponse) {
250 final HttpServletResponse httpServletResponse
251 = (HttpServletResponse) externalContext.getResponse();
252 PrintWriter responseWriter = httpServletResponse.getWriter();
253 // buf.delete(buf.indexOf("<"), buf.indexOf(">")+1);
254 responseWriter.print(buffer.toString());
255 responseWriter.flush();
256 responseWriter.close();
257 }
258 }
259
260 private String encodeState(String state) {
261 state = StringUtils.replace(state, "\"", "\\\"");
262 return StringUtils.replace(state, "\n", "");
263 }
264 }