View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.push;
20  
21  import java.io.IOException;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Map.Entry;
25  import javax.enterprise.inject.spi.BeanManager;
26  import javax.faces.FacesWrapper;
27  import javax.faces.component.UIComponent;
28  import javax.faces.component.UIWebsocket;
29  import javax.faces.component.behavior.ClientBehavior;
30  import javax.faces.component.behavior.ClientBehaviorContext;
31  import javax.faces.context.FacesContext;
32  import javax.faces.context.ResponseWriter;
33  import javax.faces.event.ComponentSystemEvent;
34  import javax.faces.event.ComponentSystemEventListener;
35  import javax.faces.event.ListenerFor;
36  import javax.faces.event.PostAddToViewEvent;
37  import javax.faces.render.Renderer;
38  import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFRenderer;
39  import org.apache.myfaces.cdi.util.CDIUtils;
40  import org.apache.myfaces.push.cdi.WebsocketApplicationBean;
41  import org.apache.myfaces.push.cdi.WebsocketChannelMetadata;
42  import org.apache.myfaces.push.cdi.WebsocketChannelTokenBuilderBean;
43  import org.apache.myfaces.push.cdi.WebsocketSessionBean;
44  import org.apache.myfaces.push.cdi.WebsocketViewBean;
45  import org.apache.myfaces.shared.renderkit.html.HTML;
46  import org.apache.myfaces.shared.renderkit.html.HtmlRendererUtils;
47  import org.apache.myfaces.shared.renderkit.html.util.ResourceUtils;
48  
49  /**
50   *
51   */
52  @JSFRenderer(
53          renderKitId = "HTML_BASIC",
54          family = "javax.faces.Script",
55          type = "javax.faces.Websocket")
56  @ListenerFor(systemEventClass = PostAddToViewEvent.class)
57  public class WebsocketComponentRenderer extends Renderer implements ComponentSystemEventListener
58  {
59  
60      @Override
61      public void processEvent(ComponentSystemEvent event)
62      {
63          if (event instanceof PostAddToViewEvent)
64          {
65              FacesContext facesContext = FacesContext.getCurrentInstance();
66              UIWebsocket component = (UIWebsocket) event.getComponent();
67              WebsocketInit initComponent = (WebsocketInit) facesContext.getViewRoot().findComponent(
68                      (String) component.getAttributes().get("initComponentId"));
69              if (initComponent == null)
70              {
71                  initComponent = (WebsocketInit) facesContext.getApplication().createComponent(facesContext,
72                          "org.apache.myfaces.WebsocketInit", "org.apache.myfaces.WebsocketInit");
73                  initComponent.setId((String) component.getAttributes().get("initComponentId"));
74                  facesContext.getViewRoot().addComponentResource(facesContext,
75                          initComponent, "body");
76              }
77          }
78      }
79  
80      private HtmlBufferResponseWriterWrapper getResponseWriter(FacesContext context)
81      {
82          return HtmlBufferResponseWriterWrapper.getInstance(context.getResponseWriter());
83      }
84  
85      @Override
86      public void decode(FacesContext facesContext, UIComponent component)
87      {
88          HtmlRendererUtils.decodeClientBehaviors(facesContext, component);
89      }
90  
91      @Override
92      public void encodeBegin(FacesContext facesContext, UIComponent component) throws IOException
93      {
94          ResponseWriter writer = facesContext.getResponseWriter();
95  
96          ResourceUtils.renderDefaultJsfJsInlineIfNecessary(facesContext, writer);
97          
98          // Render the tag that will be embedded into the DOM tree that helps to detect if the message
99          // must be processed or not and if the connection must be closed.
100         writer.startElement(HTML.DIV_ELEM, component);
101         writer.writeAttribute(HTML.ID_ATTR, component.getClientId() ,null);
102         writer.writeAttribute(HTML.STYLE_ATTR, "display:none", null);
103         writer.endElement(HTML.DIV_ELEM);
104         
105         if (!facesContext.getPartialViewContext().isAjaxRequest())
106         {
107             facesContext.setResponseWriter(getResponseWriter(facesContext));
108         }
109     }
110 
111     @Override
112     public void encodeEnd(FacesContext facesContext, UIComponent c) throws IOException
113     {
114         super.encodeEnd(facesContext, c); //check for NP
115 
116         UIWebsocket component = (UIWebsocket) c;
117 
118         WebsocketInit init = (WebsocketInit) facesContext.getViewRoot().findComponent(
119                 (String) component.getAttributes().get("initComponentId"));
120 
121         ResponseWriter writer = facesContext.getResponseWriter();
122 
123         String channel = component.getChannel();
124 
125         // TODO: use a single bean and entry point for this algorithm.
126         BeanManager beanManager = CDIUtils.getBeanManager(facesContext.getExternalContext());
127 
128         WebsocketChannelTokenBuilderBean channelTokenBean = CDIUtils.lookup(
129                 beanManager,
130                 WebsocketChannelTokenBuilderBean.class);
131 
132         // This bean is required because you always need to register the token, so it can be properly destroyed
133         WebsocketViewBean viewTokenBean = CDIUtils.lookup(
134                 beanManager,
135                 WebsocketViewBean.class);
136         WebsocketSessionBean sessionTokenBean = CDIUtils.lookup(
137                 beanManager, WebsocketSessionBean.class);
138 
139         // Create channel token 
140         // TODO: Use ResponseStateManager to create the token
141         String scope = component.getScope() == null ? "application" : component.getScope();
142         WebsocketChannelMetadata metadata = new WebsocketChannelMetadata(
143                 channel, scope, component.getUser(), component.isConnected());
144 
145         String channelToken = null;
146         // Force a new channelToken if "connected" property is set to false, because in that case websocket
147         // creation 
148         if (!component.isConnected())
149         {
150             channelToken = viewTokenBean.getChannelToken(metadata);
151         }
152         if (channelToken == null)
153         {
154             // No channel token found for that combination, create a new token for this view
155             channelToken = channelTokenBean.createChannelToken(facesContext, channel);
156             
157             // Register channel in view scope to chain discard view algorithm using @PreDestroy
158             viewTokenBean.registerToken(channelToken, metadata);
159             
160             // Register channel in session scope to allow validation on handshake ( WebsocketConfigurator )
161             sessionTokenBean.registerToken(channelToken, metadata);
162         }
163 
164         // Ask these two scopes 
165         WebsocketApplicationBean appTokenBean = CDIUtils.getInstance(
166                 beanManager, WebsocketApplicationBean.class, false);
167 
168         // Register token and metadata in the proper bean
169         if (scope.equals("view"))
170         {
171             viewTokenBean.registerWebsocketSession(channelToken, metadata);
172         }
173         else if (scope.equals("session"))
174         {
175             sessionTokenBean = (sessionTokenBean != null) ? sessionTokenBean : CDIUtils.lookup(
176                     CDIUtils.getBeanManager(facesContext.getExternalContext()),
177                     WebsocketSessionBean.class);
178 
179             sessionTokenBean.registerWebsocketSession(channelToken, metadata);
180         }
181         else
182         {
183             //Default application
184             appTokenBean = (appTokenBean != null) ? appTokenBean : CDIUtils.lookup(
185                     CDIUtils.getBeanManager(facesContext.getExternalContext()),
186                     WebsocketApplicationBean.class);
187 
188             appTokenBean.registerWebsocketSession(channelToken, metadata);
189         }
190 
191         writer.startElement(HTML.SCRIPT_ELEM, component);
192         writer.writeAttribute(HTML.SCRIPT_TYPE_ATTR, HTML.SCRIPT_TYPE_TEXT_JAVASCRIPT, null);
193 
194         StringBuilder sb = new StringBuilder(50);
195         sb.append("jsf.push.init(");
196         sb.append("'"+component.getClientId()+"'");
197         sb.append(",");
198         sb.append("'"+facesContext.getExternalContext().encodeWebsocketURL(
199                 facesContext.getApplication().getViewHandler().getWebsocketURL(
200                         facesContext, component.getChannel()+"?"+channelToken))+"'");
201         sb.append(",");
202         sb.append("'"+component.getChannel()+"'");
203         sb.append(",");
204         sb.append(component.getOnopen());
205         sb.append(",");
206         sb.append(component.getOnmessage());
207         sb.append(",");
208         sb.append(component.getOnclose());
209         sb.append(",");
210         sb.append(getBehaviorScripts(facesContext, component));
211         sb.append(",");
212         sb.append(component.isConnected());
213         sb.append(");");
214 
215         writer.write(sb.toString());
216 
217         writer.endElement(HTML.SCRIPT_ELEM);
218         
219         if (!facesContext.getPartialViewContext().isAjaxRequest())
220         {
221             ResponseWriter responseWriter = facesContext.getResponseWriter();
222             while (!(responseWriter instanceof HtmlBufferResponseWriterWrapper)
223                     && responseWriter instanceof FacesWrapper)
224             {
225                 responseWriter = (ResponseWriter) ((FacesWrapper) responseWriter).getWrapped();
226             }
227             
228             HtmlBufferResponseWriterWrapper htmlBufferResponseWritter =
229                     (HtmlBufferResponseWriterWrapper) responseWriter;
230             init.getUIWebsocketMarkupList().add(htmlBufferResponseWritter.toString());
231 
232             facesContext.setResponseWriter(htmlBufferResponseWritter.getInitialWriter());
233         }
234     }
235     
236     private String getBehaviorScripts(FacesContext facesContext, UIWebsocket component)
237     {
238         Map<String, List<ClientBehavior>> clientBehaviorsByEvent = component.getClientBehaviors();
239 
240         if (clientBehaviorsByEvent.isEmpty())
241         {
242             return "{}";
243         }
244 
245         String clientId = component.getClientId(facesContext);
246         StringBuilder scripts = new StringBuilder("{");
247 
248         for (Entry<String, List<ClientBehavior>> entry : clientBehaviorsByEvent.entrySet())
249         {
250             String event = entry.getKey();
251             List<ClientBehavior> clientBehaviors = entry.getValue();
252             scripts.append(scripts.length() > 1 ? "," : "").append(event).append(":[");
253 
254             for (int i = 0; i < clientBehaviors.size(); i++)
255             {
256                 scripts.append(i > 0 ? "," : "").append("function(event){");
257                 scripts.append(clientBehaviors.get(i).getScript(
258                         ClientBehaviorContext.createClientBehaviorContext(
259                                 facesContext, component, event, clientId, null)));
260                 scripts.append("}");
261             }
262 
263             scripts.append("]");
264         }
265 
266         return scripts.append("}").toString();
267     }
268         
269 }