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    }