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.Collections;
052    import java.util.EmptyStackException;
053    import java.util.Iterator;
054    import java.util.List;
055    import java.util.Map;
056    
057    public class AjaxResponseRenderer {
058    
059      private static final Log LOG = LogFactory.getLog(AjaxResponseRenderer.class);
060    
061      public static final String CODE_SUCCESS = "<status code=\"200\"/>";
062      public static final String CODE_NOT_MODIFIED = "<status code=\"304\"/>";
063      public static final String CODE_RELOAD_REQUIRED = "<status code=\"309\"/>";
064    
065      private Callback callback;
066      private String contentType;
067    
068      public AjaxResponseRenderer() {
069        callback = new EncodeAjaxCallback();
070        try {
071          InitialContext ic = new InitialContext();
072          contentType = (String) JndiUtils.getJndiProperty(ic, "tobago.ajax.contentType");
073        } catch (NamingException e) { /*ignore*/ }
074    
075        if (StringUtils.isBlank(contentType)) {
076          contentType = "text/html";
077        }
078      }
079    
080      public void renderResponse(FacesContext facesContext) throws IOException {
081        final UIViewRoot viewRoot = facesContext.getViewRoot();
082        RenderKitFactory renderFactory = (RenderKitFactory)
083            FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
084        RenderKit renderKit = renderFactory.getRenderKit(
085            facesContext, viewRoot.getRenderKitId());
086    
087        UIViewRoot incommingViewRoot = (UIViewRoot)
088            facesContext.getExternalContext().getRequestMap().get(VIEW_ROOT_KEY);
089        if (viewRoot != incommingViewRoot) {
090          if (LOG.isDebugEnabled()) {
091            LOG.debug("requesting full page reload because of navigation to "
092                + viewRoot.getViewId() + " from " + incommingViewRoot.getViewId());
093          }
094          Map sessionMap = facesContext.getExternalContext().getSessionMap();
095          //noinspection unchecked
096          sessionMap.put(VIEW_ROOT_KEY, viewRoot);
097          List<Object[]> messageHolders = new ArrayList<Object[]>();
098          Iterator clientIds = facesContext.getClientIdsWithMessages();
099          while (clientIds.hasNext()) {
100            String clientId = (String) clientIds.next();
101            Iterator messages = facesContext.getMessages(clientId);
102            while (messages.hasNext()) {
103              Object[] messageHolder = new Object[2];
104              messageHolder[0] = clientId;
105              messageHolder[1] = messages.next();
106              messageHolders.add(messageHolder);
107            }
108          }
109          if (!messageHolders.isEmpty()) {
110            //noinspection unchecked
111            sessionMap.put(FACES_MESSAGES_KEY, messageHolders);
112          }
113          writeResponseReload(facesContext, renderKit);
114        } else {
115          List<FastStringWriter> responseParts = new ArrayList<FastStringWriter>();
116          Map<String, UIComponent> ajaxComponents = AjaxUtils.getAjaxComponents(facesContext);
117    
118          for (Map.Entry<String, UIComponent> entry : ajaxComponents.entrySet()) {
119            AjaxComponent component = (AjaxComponent) entry.getValue();
120            responseParts.add(renderComponent(facesContext, renderKit, entry.getKey(), component));
121            break;  // TODO render multiple components
122          }
123    
124          String state = saveState(facesContext, renderKit);
125          writeResponse(facesContext, renderKit, responseParts, state);
126        }
127      }
128    
129      private FastStringWriter renderComponent(FacesContext facesContext, RenderKit renderKit, String clientId,
130          AjaxComponent component) throws IOException {
131        FastStringWriter content = new FastStringWriter();
132        ResponseWriter contentWriter = renderKit.createResponseWriter(content, null, null);
133        facesContext.setResponseWriter(contentWriter);
134        if (LOG.isDebugEnabled()) {
135          LOG.debug("write ajax response for " + component);
136        }
137    
138        try {
139          // TODO: invokeOnComponent()
140          ComponentUtil.invokeOnComponent(facesContext, clientId, (UIComponent) component, callback);
141        } catch (EmptyStackException e) {
142          LOG.error(" content = \"" + content.toString() + "\"");
143          throw e;
144        }
145    
146        return content;
147      }
148    
149      private void writeResponse(FacesContext facesContext, RenderKit renderKit,
150          List<FastStringWriter> parts, String state)
151          throws IOException {
152        writeResponse(facesContext, renderKit, CODE_SUCCESS, parts, state);
153      }
154    
155      private void writeResponseReload(FacesContext facesContext, RenderKit renderKit)
156          throws IOException {
157        writeResponse(facesContext, renderKit, CODE_RELOAD_REQUIRED, Collections.EMPTY_LIST, "");
158      }
159    
160    
161      private FastStringWriter writeState(FacesContext facesContext, RenderKit renderKit, String state)
162          throws IOException {
163        FastStringWriter jsfState = new FastStringWriter();
164        ResponseWriter stateWriter = renderKit.createResponseWriter(jsfState, null, null);
165        facesContext.setResponseWriter(stateWriter);
166        stateWriter.startElement(HtmlConstants.SCRIPT, null);
167        stateWriter.writeAttribute(HtmlAttributes.TYPE, "text/javascript", null);
168        stateWriter.flush();
169        stateWriter.write("Tobago.replaceJsfState(\"");
170        stateWriter.write(encodeState(state));
171        stateWriter.write("\");");
172        stateWriter.endElement(HtmlConstants.SCRIPT);
173        return jsfState;
174      }
175    
176      private String saveState(FacesContext facesContext, RenderKit renderKit)
177          throws IOException {
178        FastStringWriter jsfState = new FastStringWriter();
179        ResponseWriter stateWriter = renderKit.createResponseWriter(jsfState, null, null);
180        facesContext.setResponseWriter(stateWriter);
181    
182        StateManager stateManager = facesContext.getApplication().getStateManager();
183        StateManager.SerializedView serializedView
184            = stateManager.saveSerializedView(facesContext);
185        stateManager.writeState(facesContext, serializedView);
186        return jsfState.toString();
187      }
188    
189      private static void ensureContentTypeHeader(FacesContext facesContext, String charset, String contentType) {
190        StringBuilder sb = new StringBuilder(contentType);
191        if (charset == null) {
192          charset = "UTF-8";
193        }
194        sb.append("; charset=");
195        sb.append(charset);
196        ResponseUtils.ensureContentTypeHeader(facesContext, sb.toString());
197    
198      }
199    
200      private void writeResponse(FacesContext facesContext, RenderKit renderKit,
201          String responseCode, List<FastStringWriter> responseParts, String jsfState)
202          throws IOException {
203        RequestUtils.ensureEncoding(facesContext);
204        ResponseUtils.ensureNoCacheHeader(facesContext);
205        UIComponent page = ComponentUtil.findPage(facesContext);
206        String charset;
207        if (page != null) {  // in case of CODE_RELOAD_REQUIRED page is null
208          charset = (String) page.getAttributes().get(ATTR_CHARSET);
209        } else {
210          charset = "UTF-8";
211        }
212        ensureContentTypeHeader(facesContext, charset, contentType);
213        StringBuilder buffer = new StringBuilder(responseCode);
214    
215        // add parts to response
216        for (FastStringWriter part : responseParts) {
217          // TODO surround by javascript parsable tokens
218          String partStr = part.toString();
219          // FIXME:
220          if (partStr.startsWith(responseCode)
221              || partStr.equals(CODE_NOT_MODIFIED)) {
222            // remove responseCode from buffer
223            buffer.setLength(0);
224          }
225          // FIXME:
226    
227          buffer.append(partStr);
228        }
229    
230        // add jsfState to response
231        if (jsfState.length() > 0) {
232          // in case of inputSuggest jsfState.lenght is 0
233          // inputSuggest is a special case, because the form is not included in request.
234          // TODO surround by javascript parsable token
235          buffer.append(writeState(facesContext, renderKit, jsfState));
236        }
237    
238        if (LOG.isTraceEnabled()) {
239          LOG.trace("\nresponse follows ##############################################################\n"
240              + buffer
241              + "\nend response    ##############################################################");
242        }
243    
244    
245        ExternalContext externalContext = facesContext.getExternalContext();
246        //TODO: fix this to work in PortletRequest as well
247        if (externalContext.getResponse() instanceof HttpServletResponse) {
248          final HttpServletResponse httpServletResponse
249              = (HttpServletResponse) externalContext.getResponse();
250          PrintWriter responseWriter = httpServletResponse.getWriter();
251          // buf.delete(buf.indexOf("<"), buf.indexOf(">")+1);
252          responseWriter.print(buffer.toString());
253          responseWriter.flush();
254          responseWriter.close();
255        }
256      }
257    
258      private String encodeState(String state) {
259        state = StringUtils.replace(state, "\"", "\\\"");
260        return StringUtils.replace(state, "\n", "");
261      }
262    }