1 /*
  2  * Licensed to the Apache Software Foundation (ASF) under one or more
  3  * contributor license agreements.  See the NOTICE file distributed with
  4  * this work for additional information regarding copyright ownership.
  5  * The ASF licenses this file to you under the Apache License, Version 2.0
  6  * (the "License"); you may not use this file except in compliance with
  7  * the License.  You may obtain a copy of the License at
  8  *
  9  *      http://www.apache.org/licenses/LICENSE-2.0
 10  *
 11  * Unless required by applicable law or agreed to in writing, software
 12  * distributed under the License is distributed on an "AS IS" BASIS,
 13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14  * See the License for the specific language governing permissions and
 15  * limitations under the License.
 16 */
 17 
 18 /**
 19  *MyFaces core javascripting libraries
 20  *
 21  *  Those are the central public API functions in the JSF2
 22  *  Ajax API! They handle the entire form submit and ajax send
 23  *  and resolve cycle!
 24  */
 25 
 26 /**
 27  * reserve the root namespace
 28  */
 29 if ('undefined' != typeof OpenAjax && ('undefined' == typeof jsf || null == typeof jsf)) {
 30     OpenAjax.hub.registerLibrary("jsf", "www.sun.com", "1.0", null);
 31 }
 32 //just in case openajax has failed (testing environment)
 33 /**
 34 * @ignore
 35 */
 36 if (!window.jsf) {
 37 	/**
 38 	* @namespace jsf
 39 	*/
 40     var jsf = new function() {
 41         /*
 42          * Version of the implementation for the jsf.js.
 43          * <p />
 44          * as specified within the jsf specifications jsf.html:
 45          * <ul>
 46          * <li>left two digits major release number</li>
 47          * <li>middle two digits minor spec release number</li>
 48          * <li>right two digits bug release number</li>
 49          * </ul>
 50 		 * @constant
 51          */
 52         this.specversion = 220000;
 53         /**
 54          * Implementation version as specified within the jsf specification.
 55          * <p />
 56          * A number increased with every implementation version
 57          * and reset by moving to a new spec release number
 58          *
 59 		 * @constant
 60          */
 61         this.implversion = 0;
 62 
 63         /**
 64          * SeparatorChar as defined by UINamingContainer.getNamingContainerSeparatorChar()
 65          * @type {Char}
 66          */
 67         this.separatorchar = getSeparatorChar();
 68 
 69         /**
 70          * This method is responsible for the return of a given project stage as defined
 71          * by the jsf specification.
 72          * <p/>
 73          * Valid return values are:
 74          * <ul>
 75          *     <li>"Production"</li>
 76          *     <li>"Development"</li>
 77          *     <li>"SystemTest"</li>
 78          *     <li>"UnitTest"</li>
 79          * </li>
 80          *
 81          * @return {String} the current project state emitted by the server side method:
 82          * <i>javax.faces.application.Application.getProjectStage()</i>
 83          */
 84         this.getProjectStage = function() {
 85             var impl = myfaces._impl.core._Runtime.getGlobalConfig("jsfAjaxImpl", myfaces._impl.core.Impl);
 86             return impl.getProjectStage();
 87         };
 88 
 89         /**
 90          * collect and encode data for a given form element (must be of type form)
 91          * find the javax.faces.ViewState element and encode its value as well!
 92          * return a concatenated string of the encoded values!
 93          *
 94          * @throws an exception in case of the given element not being of type form!
 95          * https://issues.apache.org/jira/browse/MYFACES-2110
 96          */
 97         this.getViewState = function(formElement) {
 98             /*we are not allowed to add the impl on a global scope so we have to inline the code*/
 99             var impl = myfaces._impl.core._Runtime.getGlobalConfig("jsfAjaxImpl", myfaces._impl.core.Impl);
100             return impl.getViewState(formElement);
101         };
102 
103         /**
104          * returns the window identifier for the given node / window
105          * @param {optional String | DomNode}  the node for which the client identifier has to be determined
106          * @return the window identifier or null if none is found
107          */
108         this.getClientWindow = function() {
109             /*we are not allowed to add the impl on a global scope so we have to inline the code*/
110             var impl = myfaces._impl.core._Runtime.getGlobalConfig("jsfAjaxImpl", myfaces._impl.core.Impl);
111             return (arguments.length)? impl.getClientWindow(arguments[0]) : impl.getClientWindow();
112         }
113 
114         //private helper functions
115         function getSeparatorChar() {
116             var impl = myfaces._impl.core._Runtime.getGlobalConfig("jsfAjaxImpl", myfaces._impl.core.Impl);
117             return impl.getSeparatorChar();
118         }
119 
120     };
121 	//jsdoc helper to avoid warnings, we map later
122 	window.jsf = jsf;
123 }
124 
125 /**
126  * just to make sure no questions arise, I simply prefer here a weak
127  * typeless comparison just in case some frameworks try to interfere
128  * by overriding null or fiddeling around with undefined or typeof in some ways
129  * it is safer in this case than the standard way of doing a strong comparison
130  **/
131 if (!jsf.ajax) {
132 	/**
133 	* @namespace jsf.ajax
134 	*/
135     jsf.ajax = new function() {
136 
137 
138         /**
139          * this function has to send the ajax requests
140          *
141          * following request conditions must be met:
142          * <ul>
143          *  <li> the request must be sent asynchronously! </li>
144          *  <li> the request must be a POST!!! request </li>
145          *  <li> the request url must be the form action attribute </li>
146          *  <li> all requests must be queued with a client side request queue to ensure the request ordering!</li>
147          * </ul>
148          *
149          * @param {String|Node} element: any dom element no matter being it html or jsf, from which the event is emitted
150          * @param {EVENT} event: any javascript event supported by that object
151          * @param {Map} options : map of options being pushed into the ajax cycle
152          */
153         this.request = function(element, event, options) {
154             if (!options) {
155                 options = {};
156             }
157             /*we are not allowed to add the impl on a global scope so we have to inline the code*/
158             var impl = myfaces._impl.core._Runtime.getGlobalConfig("jsfAjaxImpl", myfaces._impl.core.Impl);
159             return impl.request(element, event, options);
160         };
161 
162 		/**
163 		* Adds an error handler to our global error queue.
164 		* the error handler must be of the format <i>function errorListener(<errorData>)</i>
165 		* with errorData being of following format:
166 		* <ul>
167          *     <li> errorData.type : "error"</li>
168          *     <li> errorData.status : the error status message</li>
169          *     <li> errorData.errorName : the server error name in case of a server error</li>
170          *     <li> errorData.errorMessage : the server error message in case of a server error</li>
171          *     <li> errorData.source  : the issuing source element which triggered the request </li>
172          *     <li> eventData.responseCode: the response code (aka http request response code, 401 etc...) </li>
173          *     <li> eventData.responseText: the request response text </li>
174          *     <li> eventData.responseXML: the request response xml </li>
175         * </ul>
176          *
177          * @param {function} errorListener error handler must be of the format <i>function errorListener(<errorData>)</i>
178 		*/
179         this.addOnError = function(/*function*/errorListener) {
180             var impl = myfaces._impl.core._Runtime.getGlobalConfig("jsfAjaxImpl", myfaces._impl.core.Impl);
181             return impl.addOnError(errorListener);
182         };
183 
184         /**
185          * Adds a global event listener to the ajax event queue. The event listener must be a function
186          * of following format: <i>function eventListener(<eventData>)</i>
187          *
188          * @param {function} eventListener event must be of the format <i>function eventListener(<eventData>)</i>
189          */
190         this.addOnEvent = function(/*function*/eventListener) {
191             var impl = myfaces._impl.core._Runtime.getGlobalConfig("jsfAjaxImpl", myfaces._impl.core.Impl);
192             return impl.addOnEvent(eventListener);
193         };
194 
195         /**
196          * processes the ajax response if the ajax request completes successfully
197          * @param request the ajax request!
198          * @param context the ajax context!
199          */
200         this.response = function(/*xhr request object*/request, context) {
201             var impl = myfaces._impl.core._Runtime.getGlobalConfig("jsfAjaxImpl", myfaces._impl.core.Impl);
202             return impl.response(request, context);
203         };
204     }
205 }
206 
207 if (!jsf.util) {
208 	/**
209 	* @namespace jsf.util
210 	*/
211     jsf.util = new function() {
212 
213         /**
214          * varargs function which executes a chain of code (functions or any other code)
215          *
216          * if any of the code returns false, the execution
217          * is terminated prematurely skipping the rest of the code!
218          *
219          * @param {DomNode} source, the callee object
220          * @param {Event} event, the event object of the callee event triggering this function
221          * @param {optional} functions to be chained, if any of those return false the chain is broken
222          */
223         this.chain = function(source, event) {
224             var impl = myfaces._impl.core._Runtime.getGlobalConfig("jsfAjaxImpl", myfaces._impl.core.Impl);
225             return impl.chain.apply(impl, arguments);
226         };
227     }
228 }
229 
230 if (!jsf.push) {
231 
232   /**
233   * @namespace jsf.push
234   */
235   jsf.push = new function() {
236 
237     // "Constant" fields ----------------------------------------------------------------------------------------------
238     var URL_PROTOCOL = window.location.protocol.replace("http", "ws") + "//";
239     var RECONNECT_INTERVAL = 500;
240     var MAX_RECONNECT_ATTEMPTS = 25;
241     var REASON_EXPIRED = "Expired";
242 
243     // Private static fields ------------------------------------------------------------------------------------------
244 
245     /* socket map by token */
246     var sockets = {};
247     /* component attributes by clientId */
248     var components = {};
249     /* client ids by token (share websocket connection) */
250     var clientIdsByTokens = {};
251     var self = {};
252 
253     // Private constructor functions ----------------------------------------------------------------------------------
254     /**
255      * Creates a reconnecting web socket. When the web socket successfully connects on first attempt, then it will
256      * automatically reconnect on timeout with cumulative intervals of 500ms with a maximum of 25 attempts (~3 minutes).
257      * The <code>onclose</code> function will be called with the error code of the last attempt.
258      * @constructor
259      * @param {string} channelToken the channel token associated with this websocket connection
260      * @param {string} url The URL of the web socket
261      * @param {string} channel The name of the web socket channel.
262      */
263     function Socket(channelToken, url, channel) {
264 
265         // Private fields -----------------------------------------------------------------------------------------
266 
267         var socket;
268         var reconnectAttempts = 0;
269         var self = this;
270 
271         // Public functions ---------------------------------------------------------------------------------------
272 
273         /**
274          * Opens the reconnecting web socket.
275          */
276         self.open = function() {
277             if (socket && socket.readyState == 1) {
278                 return;
279             }
280 
281             socket = new WebSocket(url);
282 
283             socket.onopen = function(event) {
284                 if (!reconnectAttempts) {
285                     var clientIds = clientIdsByTokens[channelToken];
286                     for (var i = clientIds.length - 1; i >= 0; i--){
287                         var socketClientId = clientIds[i];
288                         components[socketClientId]['onopen'](channel);
289                     }
290                 }
291                 reconnectAttempts = 0;
292             };
293 
294             socket.onmessage = function(event) {
295                 var message = JSON.parse(event.data);
296                 for (var i = clientIdsByTokens[channelToken].length - 1; i >= 0; i--){
297                     var socketClientId = clientIdsByTokens[channelToken][i];
298                     if(document.getElementById(socketClientId)) {
299                         try{
300                             components[socketClientId]['onmessage'](message, channel, event);
301                         }catch(e){
302                             //Ignore
303                         }
304                         var behaviors = components[socketClientId]['behaviors'];
305                         var functions = behaviors[message];
306                         if (functions && functions.length) {
307                             for (var j = 0; j < functions.length; j++) {
308                                 try{
309                                     functions[j](null);
310                                 }catch(e){
311                                     //Ignore
312                                 }
313                             }
314                         }
315                     } else {
316                         clientIdsByTokens[channelToken].splice(i,1);
317                     }
318                 }
319                 if (clientIdsByTokens[channelToken].length == 0){
320                     //tag dissapeared
321                     self.close();
322                 }
323 
324             };
325 
326             socket.onclose = function(event) {
327                 if (!socket
328                     || (event.code == 1000 && event.reason == REASON_EXPIRED)
329                     || (event.code == 1008)
330                     || (!reconnectAttempts)
331                     || (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS))
332                 {
333                     var clientIds = clientIdsByTokens[channelToken];
334                     for (var i = clientIds.length - 1; i >= 0; i--){
335                         var socketClientId = clientIds[i];
336                         components[socketClientId]['onclose'](event.code, channel, event);
337                     }
338                 }
339                 else {
340                     setTimeout(self.open, RECONNECT_INTERVAL * reconnectAttempts++);
341                 }
342             };
343         };
344 
345         /**
346          * Closes the reconnecting web socket.
347          */
348         self.close = function() {
349             if (socket) {
350                 var s = socket;
351                 socket = null;
352                 s.close();
353             }
354         }
355 
356     }
357 
358     // Public static functions ----------------------------------------------------------------------------------------
359 
360     /**
361      *
362      * @param {function} onopen The function to be invoked when the web socket is opened.
363      * @param {function} onmessage The function to be invoked when a message is received.
364      * @param {function} onclose The function to be invoked when the web socket is closed.
365      * @param {boolean} autoconnect Whether or not to immediately open the socket. Defaults to <code>false</code>.
366      */
367     this.init = function(socketClientId, uri, channel, onopen, onmessage, onclose, behaviorScripts, autoconnect) {
368 
369         onclose = resolveFunction(onclose);
370 
371         if (!window.WebSocket) { // IE6-9.
372             onclose(-1, channel);
373             return;
374         }
375 
376         var channelToken = uri.substr(uri.indexOf('?')+1);
377 
378         if (!components[socketClientId]) {
379             components[socketClientId] = {
380                 'channelToken': channelToken,
381                 'onopen': resolveFunction(onopen),
382                 'onmessage' : resolveFunction(onmessage),
383                 'onclose': onclose,
384                 'behaviors': behaviorScripts,
385                 'autoconnect': autoconnect};
386             if (!clientIdsByTokens[channelToken]) {
387                 clientIdsByTokens[channelToken] = [];
388             }
389             clientIdsByTokens[channelToken].push(socketClientId);
390             if (!sockets[channelToken]){
391                 sockets[channelToken] = new Socket(channelToken,
392                                     getBaseURL(uri), channel);
393             }
394         }
395 
396         if (autoconnect) {
397             this.open(socketClientId);
398         }
399     }
400 
401     /**
402      * Open the web socket on the given channel.
403      * @param {string} channel The name of the web socket channel.
404      * @throws {Error} When channel is unknown.
405      */
406     this.open = function(socketClientId) {
407         getSocket(components[socketClientId]['channelToken']).open();
408     }
409 
410     /**
411      * Close the web socket on the given channel.
412      * @param {string} channel The name of the web socket channel.
413      * @throws {Error} When channel is unknown.
414      */
415     this.close = function(socketClientId) {
416         getSocket(components[socketClientId]['channelToken']).close();
417     }
418 
419     // Private static functions ---------------------------------------------------------------------------------------
420 
421     /**
422      *
423      */
424     function getBaseURL(url) {
425         if (url.indexOf("://") < 0)
426         {
427             var base = window.location.hostname+":"+window.location.port
428             return URL_PROTOCOL + base + url;
429         }else
430         {
431             return url;
432         }
433     }
434 
435     /**
436      * Get socket associated with given channelToken.
437      * @param {string} channelToken The name of the web socket channelToken.
438      * @return {Socket} Socket associated with given channelToken.
439      * @throws {Error} When channelToken is unknown, you may need to initialize
440      *                 it first via <code>init()</code> function.
441      */
442     function getSocket(channelToken) {
443         var socket = sockets[channelToken];
444         if (socket) {
445             return socket;
446         } else {
447             throw new Error("Unknown channelToken: " + channelToken);
448         }
449     }
450 
451     function resolveFunction(fn) {
452         return (typeof fn !== "function") && (fn = window[fn] || function(){}), fn;
453     }
454     // Expose self to public ------------------------------------------------------------------------------------------
455 
456     //return self;
457   }
458 }
459