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.JndiUtils;
033 import org.apache.myfaces.tobago.util.RequestUtils;
034 import org.apache.myfaces.tobago.util.ResponseUtils;
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 StringBuilder sb = new StringBuilder(contentType);
190 if (charset == null) {
191 charset = "UTF-8";
192 }
193 sb.append("; charset=");
194 sb.append(charset);
195 ResponseUtils.ensureContentTypeHeader(facesContext, sb.toString());
196
197 }
198
199 private void writeResponse(FacesContext facesContext, RenderKit renderKit,
200 String responseCode, List<FastStringWriter> responseParts, String jsfState)
201 throws IOException {
202 RequestUtils.ensureEncoding(facesContext);
203 ResponseUtils.ensureNoCacheHeader(facesContext);
204 UIComponent page = ComponentUtil.findPage(facesContext);
205 String charset;
206 if (page != null) { // in case of CODE_RELOAD_REQUIRED page is null
207 charset = (String) page.getAttributes().get(ATTR_CHARSET);
208 } else {
209 charset = "UTF-8";
210 }
211 ensureContentTypeHeader(facesContext, charset, contentType);
212 StringBuilder buffer = new StringBuilder(responseCode);
213
214 // add parts to response
215 for (FastStringWriter part : responseParts) {
216 // TODO surround by javascript parsable tokens
217 String partStr = part.toString();
218 // FIXME:
219 if (partStr.startsWith(responseCode)
220 || partStr.equals(CODE_NOT_MODIFIED)) {
221 // remove responseCode from buffer
222 buffer.setLength(0);
223 }
224 // FIXME:
225
226 buffer.append(partStr);
227 }
228
229 // add jsfState to response
230 if (jsfState.length() > 0) {
231 // in case of inputSuggest jsfState.lenght is 0
232 // inputSuggest is a special case, because the form is not included in request.
233 // TODO surround by javascript parsable token
234 buffer.append(writeState(facesContext, renderKit, jsfState));
235 }
236
237 if (LOG.isTraceEnabled()) {
238 LOG.trace("\nresponse follows ##############################################################\n"
239 + buffer
240 + "\nend response ##############################################################");
241 }
242
243
244 ExternalContext externalContext = facesContext.getExternalContext();
245 //TODO: fix this to work in PortletRequest as well
246 if (externalContext.getResponse() instanceof HttpServletResponse) {
247 final HttpServletResponse httpServletResponse
248 = (HttpServletResponse) externalContext.getResponse();
249 PrintWriter responseWriter = httpServletResponse.getWriter();
250 // buf.delete(buf.indexOf("<"), buf.indexOf(">")+1);
251 responseWriter.print(buffer.toString());
252 responseWriter.flush();
253 responseWriter.close();
254 }
255 }
256
257 private String encodeState(String state) {
258 state = StringUtils.replace(state, "\"", "\\\"");
259 return StringUtils.replace(state, "\n", "");
260 }
261 }