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