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 static org.apache.myfaces.tobago.lifecycle.TobagoLifecycle.FACES_MESSAGES_KEY; 021 022 import org.apache.commons.lang.StringUtils; 023 import org.apache.commons.logging.Log; 024 import org.apache.commons.logging.LogFactory; 025 026 import org.apache.myfaces.tobago.util.RequestUtils; 027 import org.apache.myfaces.tobago.util.ResponseUtils; 028 import org.apache.myfaces.tobago.util.EncodeAjaxCallback; 029 import org.apache.myfaces.tobago.util.Callback; 030 import org.apache.myfaces.tobago.renderkit.html.HtmlConstants; 031 import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes; 032 import static org.apache.myfaces.tobago.lifecycle.TobagoLifecycle.VIEW_ROOT_KEY; 033 import static org.apache.myfaces.tobago.TobagoConstants.ATTR_CHARSET; 034 import org.apache.myfaces.tobago.component.ComponentUtil; 035 036 import javax.faces.context.FacesContext; 037 import javax.faces.context.ResponseWriter; 038 import javax.faces.context.ExternalContext; 039 import javax.faces.component.UIViewRoot; 040 import javax.faces.component.UIComponent; 041 import javax.faces.render.RenderKitFactory; 042 import javax.faces.render.RenderKit; 043 import javax.faces.FactoryFinder; 044 import javax.faces.application.StateManager; 045 import javax.servlet.http.HttpServletResponse; 046 import javax.naming.InitialContext; 047 import javax.naming.Context; 048 import javax.naming.NamingException; 049 import java.io.StringWriter; 050 import java.io.IOException; 051 import java.io.PrintWriter; 052 import java.util.List; 053 import java.util.ArrayList; 054 import java.util.Map; 055 import java.util.Iterator; 056 import java.util.EmptyStackException; 057 058 public class AjaxResponseRenderer { 059 060 private static final Log LOG = LogFactory.getLog(AjaxResponseRenderer.class); 061 062 public static final String CODE_SUCCESS = "<status code=\"200\"/>"; 063 public static final String CODE_NOT_MODIFIED = "<status code=\"304\"/>"; 064 public static final String CODE_RELOAD_REQUIRED = "<status code=\"309\"/>"; 065 066 private Callback callback; 067 private String contentType; 068 069 public AjaxResponseRenderer() { 070 callback = new EncodeAjaxCallback(); 071 try { 072 InitialContext ic = new InitialContext(); 073 Context ctx = (Context) ic.lookup("java:comp/env"); 074 contentType = (String) ctx.lookup("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<StringWriter> responseParts = new ArrayList<StringWriter>(); 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 StringWriter renderComponent(FacesContext facesContext, RenderKit renderKit, String clientId, 132 AjaxComponent component) throws IOException { 133 StringWriter content = new StringWriter(); 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.getBuffer().toString() + "\""); 145 throw e; 146 } 147 148 return content; 149 } 150 151 private void writeResponse(FacesContext facesContext, RenderKit renderKit, 152 List<StringWriter> 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, new ArrayList<StringWriter>(0), ""); 160 } 161 162 163 private StringWriter writeState(FacesContext facesContext, RenderKit renderKit, String state) 164 throws IOException { 165 StringWriter jsfState = new StringWriter(); 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 StringWriter jsfState = new StringWriter(); 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 // TODO PortletRequest 193 if (facesContext.getExternalContext().getResponse() instanceof HttpServletResponse) { 194 HttpServletResponse response = (HttpServletResponse) facesContext.getExternalContext().getResponse(); 195 196 StringBuilder sb = new StringBuilder(contentType); 197 if (charset == null) { 198 charset = "UTF-8"; 199 } 200 sb.append("; charset="); 201 sb.append(charset); 202 response.setContentType(sb.toString()); 203 } 204 } 205 206 private void writeResponse(FacesContext facesContext, RenderKit renderKit, 207 String responseCode, List<StringWriter> responseParts, String jsfState) 208 throws IOException { 209 ExternalContext externalContext = facesContext.getExternalContext(); 210 RequestUtils.ensureEncoding(externalContext); 211 ResponseUtils.ensureNoCacheHeader(externalContext); 212 UIComponent page = ComponentUtil.findPage(facesContext); 213 String charset; 214 if (page != null) { // in case of CODE_RELOAD_REQUIRED page is null 215 charset = (String) page.getAttributes().get(ATTR_CHARSET); 216 } else { 217 charset = "UTF-8"; 218 } 219 ensureContentTypeHeader(facesContext, charset, contentType); 220 StringBuilder buffer = new StringBuilder(responseCode); 221 222 // add parts to response 223 for (StringWriter part : responseParts) { 224 // TODO surround by javascript parsable tokens 225 226 // FIXME: 227 if (part.toString().startsWith(CODE_NOT_MODIFIED) 228 && buffer.toString().equals(responseCode)) { 229 // remove resopnseCode from buffer 230 buffer.setLength(0); 231 } 232 // /FIXME: 233 234 buffer.append(part.toString()); 235 } 236 237 // add jsfState to response 238 if (jsfState.length() > 0) { 239 // in case of inputSuggest jsfState.lenght is 0 240 // inputSuggest is a special case, because the form is not included in request. 241 // TODO surround by javascript parsable token 242 buffer.append(writeState(facesContext, renderKit, jsfState)); 243 } 244 245 if (LOG.isTraceEnabled()) { 246 LOG.trace("\nresponse follows ##############################################################\n" 247 + buffer 248 + "\nend response ##############################################################"); 249 } 250 251 String content = buffer.toString(); 252 253 // TODO optimize me!! 254 buffer.insert(0, Integer.toHexString(content.getBytes("UTF-8").length) + "\r\n"); 255 buffer.append("\r\n" + 0 + "\r\n\r\n"); 256 257 //TODO: fix this to work in PortletRequest as well 258 if (externalContext.getResponse() instanceof HttpServletResponse) { 259 final HttpServletResponse httpServletResponse 260 = (HttpServletResponse) externalContext.getResponse(); 261 httpServletResponse.addHeader("Transfer-Encoding", "chunked"); 262 PrintWriter responseWriter = httpServletResponse.getWriter(); 263 // buf.delete(buf.indexOf("<"), buf.indexOf(">")+1); 264 responseWriter.print(buffer.toString()); 265 responseWriter.flush(); 266 responseWriter.close(); 267 } 268 } 269 270 private String encodeState(String state) { 271 state = StringUtils.replace(state, "\"", "\\\""); 272 return StringUtils.replace(state, "\n", ""); 273 } 274 }