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    }