diff options
25 files changed, 913 insertions, 38 deletions
diff --git a/WebContent/VAADIN/atmosphere.min.js b/WebContent/VAADIN/atmosphere.min.js new file mode 100644 index 0000000000..64f6604951 --- /dev/null +++ b/WebContent/VAADIN/atmosphere.min.js @@ -0,0 +1,12 @@ +/* + * Atmosphere.js + * https://github.com/Atmosphere/atmosphere + * + * Requires Portal 1.0 + * https://github.com/flowersinthesand/portal + * + * Copyright 2012-2013, Donghwan Kim + * Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + */ +(function(){function l(a,b){var c,f,g=b.headers||{};if(b.readResponsesHeaders)for(c in b.lastTimestamp=(a.getResponseHeader("X-Cache-Date")||"").split(" ").pop(),b.uuid=(a.getResponseHeader("X-Atmosphere-tracking-id")||"").split(" ").pop(),g)(f=a.getResponseHeader(c))&&(g[c]=f)}function m(a,b){var c,f,g=b.headers||{};b.dropAtmosphereHeaders||(a.setRequestHeader("X-Atmosphere-Framework",n),a.setRequestHeader("X-Atmosphere-Transport",b.transport),a.setRequestHeader("X-Cache-Date",b.lastTimestamp||0), b.trackMessageLength&&a.setRequestHeader("X-Atmosphere-TrackMessageSize","true"),b.contentType&&a.setRequestHeader("Content-Type",b.contentType),a.setRequestHeader("X-Atmosphere-tracking-id",b.uuid));for(c in g)f=g[c],(f=portal.support.isFunction(f)?f.call(null,a,b):f)&&a.setRequestHeader(c,f)}var n="1.1",k={},p=portal.support.now();k.subscribe=function(a){a=new k.AtmosphereRequest(a);a.open();return a};k.unsubscribe=portal.finalize;k.AtmosphereRequest=function(a){var b,c;a=portal.support.extend({url:"", connectTimeout:-1,reconnectInterval:0,timeout:3E5,method:"GET",fallbackMethod:"GET",headers:{},maxRequest:-1,transport:"long-polling",fallbackTransport:"streaming",dispatchUrl:null,webSocketPathDelimiter:"@@",webSocketBinaryType:null,enableXDR:!1,rewriteURL:!1,attachHeadersAsQueryString:!0,withCredentials:!1,trackMessageLength:!1,messageDelimiter:"|",shared:!1,lastTimestamp:0,readResponsesHeaders:!0,dropAtmosphereHeaders:!0,contentType:"",uuid:0,executeCallbackBeforeReconnect:!1},a);this.open=function(){function f(){g(); j=setTimeout(function(){b.fire("close","idletimeout")},a.timeout)}function g(){clearTimeout(j)}function h(a){return{"long-polling":"longpoll",streaming:"stream",jsonp:"longpolljsonp",sse:"sse",websocket:"ws",session:"session",test:"test"}[a]}var j;b=portal.open(a.url,{atrequest:a,method:a.method,transports:[h(a.transport)],timeout:a.connectTimeout,credentials:a.withCredentials,sharing:a.shared,params:a.headers,longpollTest:!1,urlBuilder:function(b,d){if(!a.attachHeadersAsQueryString)return b;delete d.id; delete d.transport;delete d.heartbeat;delete d.lastEventId;portal.support.extend(d,{"X-Atmosphere-tracking-id":a.uuid,"X-Atmosphere-Framework":n,"X-Atmosphere-Transport":a.transport,"X-Cache-Date":a.lastTimestamp||0});a.trackMessageLength&&(d["X-Atmosphere-TrackMessageSize"]=!0);a.contentType&&(d["Content-Type"]=a.contentType);return b+(/\?/.test(b)?"&":"?")+portal.support.param(d)},reconnect:function(b,d){return-1===a.maxRequest||d<a.maxRequest?a.reconnectInterval:!1},xdrURL:a.enableXDR&&function(e){return(a.rewriteURL|| portal.defaults.xdrURL||function(){}).call(a.rewriteURL?window:b,e)||e},inbound:function(e){var d,c,j=[];0<a.timeout&&f();if(a.trackMessageLength){b.data("data")&&(e=b.data("data")+e);d=0;for(c=e.indexOf(a.messageDelimiter);-1!==c;){d=e.substring(d,c);e=e.substring(c+a.messageDelimiter.length,e.length);if(!e||e.length<d)break;c=e.indexOf(a.messageDelimiter);j.push(e.substring(0,d))}b.data("data",!j.length||-1!==c&&e&&d!==e.length?d+a.messageDelimiter+e:"")}else j.push(e);for(e=0;e<j.length;e++)j[e]= {type:"message",data:j[e]};return j},outbound:function(b){0<a.timeout&&f();return b.data},streamParser:function(a){return[a.replace(/^\s+/g,"")]},initIframe:function(a){var b;b=a.contentDocument||a.contentWindow.document;if(!b.body||!b.body.firstChild||"pre"!==b.body.firstChild.nodeName.toLowerCase())a=b.head||b.getElementsByTagName("head")[0]||b.documentElement||b,b=b.createElement("script"),b.text="document.write('<plaintext>')",a.insertBefore(b,a.firstChild),a.removeChild(b)}});b.on({connecting:function(){b.data("t1", {ws:"websocket",sse:"sse",streamxhr:"streaming",streamxdr:"streaming",streamiframe:"streaming",longpollajax:"long-polling",longpollxdr:"long-polling",longpolljsonp:"jsonp",session:"session",test:"test"}[b.data("transport")])},open:function(){var e={status:200,responseBody:"",headers:[],state:"messageReceived",transport:b.data("t1"),error:null,request:a};0<a.timeout&&(f(),b.one("close",g));if(c){if(e.state="re-opening",a.onReconnect)a.onReconnect(a,e)}else{e.state="opening";if(a.onOpen)a.onOpen(e); c=!0}a.callback&&a.callback(e)},message:function(c){var d={status:200,responseBody:c,headers:[],state:"messageReceived",transport:b.data("t1"),error:null,request:a};b.data("lastData",c);if(a.onMessage)a.onMessage(d);a.callback&&a.callback(d)},close:function(e){var d={status:200,responseBody:"",headers:[],state:"messageReceived",transport:b.data("t1"),error:null,request:a};switch(e){case "aborted":d.status=408;d.state="unsubscribe";break;case "done":case "timeout":d.status=!c?501:200;d.state="closed"; break;case "error":d.status=500;d.state="error";break;case "notransport":if(a.onTransportFailure)a.onTransportFailure(e,a);a.method=a.fallbackMethod;a.transport=a.fallbackTransport;b.option("method",a.method);b.option("transports",[h(a.transport)])}if("error"===e){if(a.onError)a.onError(d)}else if(a.onClose)a.onClose(d);a.callback&&a.callback(d);a.executeCallbackBeforeReconnect&&(b.fire("message",b.data("lastData")),b.option("method",a.method),b.option("transports",[h(a.transport)]))},waiting:function(){a.executeCallbackBeforeReconnect|| b.fire("message",b.data("lastData"))},session:function(c){if(c.from!==b.option("id")&&a.onLocalMessage)a.onLocalMessage(c.message)}})};this.push=function(a,c){var h=b.option("dispatchUrl");b.option("dispatchUrl",c);b.send("message",a);b.option("dispatchUrl",h)};this.pushLocal=function(a){b.broadcast("session",{from:b.option("id"),data:a})}};portal.support.extend(portal.transports,{ws:function(a,b){var c,f,g=window.WebSocket||window.MozWebSocket;if(g)return{feedback:!0,open:function(){var h=portal.support.getAbsoluteURL(a.data("url")).replace(/^http/, "ws");a.data("url",h);c=new g(h);b.atrequest.webSocketBinaryType&&(c.binaryType=b.atrequest.webSocketBinaryType);c.onopen=function(b){a.data("event",b).fire("open")};c.onmessage=function(b){a.data("event",b)._fire(b.data)};c.onerror=function(b){a.data("event",b).fire("close",f?"aborted":"error")};c.onclose=function(b){a.data("event",b).fire("close",f?"aborted":b.wasClean?"done":"error")}},send:function(a){var f=b.atrequest.dispatchUrl,e=b.atrequest.webSocketPathDelimiter;console.log(b.atrequest); c.send((f?e+f+e:"")+a)},close:function(){f=!0;c.close()}}},httpbase:function(a,b){function c(){h.length?f(b.url+(b.dispatchUrl||""),h.shift()):g=!1}var f,g,h=[];f=!b.crossDomain||portal.support.corsable?function(a,e){var d=portal.support.xhr();d.onreadystatechange=function(){4===d.readyState&&(l(d,b.atrequest),c())};d.open("POST",a);m(d,b.atrequest);portal.support.corsable&&(d.withCredentials=b.credentials);d.send(e)}:window.XDomainRequest&&b.xdrURL&&b.xdrURL.call(a,"t")?function(f,e){var d=new window.XDomainRequest; d.onload=d.onerror=c;d.open("POST",b.xdrURL.call(a,f));d.send(e)}:function(a,b){var d=document.createElement("form");d.action=a;d.target="socket-"+ ++p;d.method="POST";d.enctype=d.encoding="text/plain";d.acceptCharset="UTF-8";d.style.display="none";d.innerHTML='<textarea name="data"></textarea><iframe name="'+d.target+'"></iframe>';d.firstChild.value=b;portal.support.on(d.lastChild,"load",function(){document.body.removeChild(d);c()});document.body.appendChild(d);d.submit()};return{send:function(a){h.push(a); g||(g=!0,c())}}},streamxhr:function(a,b){var c;if(!(portal.support.browser.msie&&10>+portal.support.browser.version||b.crossDomain&&!portal.support.corsable))return portal.support.extend(portal.transports.httpbase(a,b),{open:function(){var f;c=portal.support.xhr();c.onreadystatechange=function(){function g(){var b=a.data("index"),f=c.responseText.length;b?f>b&&a._fire(c.responseText.substring(b,f),!0):a.fire("open")._fire(c.responseText,!0);a.data("index",f)}2===c.readyState?l(c,b.atrequest):3=== c.readyState&&200===c.status?portal.support.browser.opera&&!f?f=portal.support.iterate(g):g():4===c.readyState&&(f&&f(),a.fire("close",200===c.status?"done":"error"))};c.open("GET",a.data("url"));portal.support.corsable&&(c.withCredentials=b.credentials);m(c,b.atrequest);c.send(null)},close:function(){c.abort()}})},longpollajax:function(a,b){var c,f,g=0;if(!b.crossDomain||portal.support.corsable)return portal.support.extend(portal.transports.httpbase(a,b),{open:function(){function h(){var j=a.buildURL(!g? "open":"poll",{count:++g});a.data("url",j);c=portal.support.xhr();c.onreadystatechange=function(){var e;!f&&4===c.readyState&&(200===c.status?(l(c,b.atrequest),(e=c.responseText)||1===g?(1===g&&a.fire("open"),e&&a._fire(e),h()):a.fire("close","done")):a.fire("close","error"))};c.open("GET",j);m(c,b.atrequest);portal.support.corsable&&(c.withCredentials=b.credentials);c.send(null)}b.longpollTest?h():setTimeout(function(){a.fire("open");h()},50)},close:function(){f=!0;c.abort()}})}});portal.support.on(window, "keypress",function(a){27===a.which&&a.preventDefault()});window.atmosphere=k})();
\ No newline at end of file diff --git a/WebContent/VAADIN/portal.min.js b/WebContent/VAADIN/portal.min.js new file mode 100644 index 0000000000..83228d205c --- /dev/null +++ b/WebContent/VAADIN/portal.min.js @@ -0,0 +1,9 @@ +/* + * Portal v1.0 + * http://github.com/flowersinthesand/portal + * + * Copyright 2011-2013, Donghwan Kim + * Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + */ +(function(){function z(a){var c=[],b,e,g,d,h,m,l=function(b,n){n=n||[];e=!a||[b,n];g=!0;m=d||0;d=0;for(h=c.length;m<h;m++)c[m].apply(b,n);g=!1};return{add:function(a){var n=c.length;c.push(a);g?h=c.length:!b&&(e&&!0!==e)&&(d=n,l(e[0],e[1]))},remove:function(a){var b;for(b=0;b<c.length;b++)if(a===c[b]||a.guid&&a.guid===c[b].guid)g&&b<=h&&(h--,b<=m&&m--),c.splice(b--,1)},fire:function(c,d){!b&&(!g&&(!a||!e))&&l(c,d)},lock:function(){b=!0},locked:function(){return!!b},unlock:function(){b=e=g=d=h=m=void 0}}} var v,w,e={},q={},B=[],x=Object.prototype.toString,C=Object.prototype.hasOwnProperty,y=Array.prototype.slice;e.support={now:function(){return(new Date).getTime()},isArray:function(a){return"[object Array]"===x.call(a)},isBinary:function(a){a=x.call(a);return"[object Blob]"===a||"[object ArrayBuffer]"===a},isFunction:function(a){return"[object Function]"===x.call(a)},getAbsoluteURL:function(a){var c=document.createElement("div");c.innerHTML='<a href="'+a+'"/>';return encodeURI(decodeURI(c.firstChild.href))}, iterate:function(a){var c;(function f(){c=setTimeout(function(){!1!==a()&&f()},1)})();return function(){clearTimeout(c)}},each:function(a,c){var b;for(b=0;b<a.length;b++)c(b,a[b])},extend:function(a){var c,b,e;for(c=1;c<arguments.length;c++)if(null!=(b=arguments[c]))for(e in b)a[e]=b[e];return a},on:function(a,c,b){a.addEventListener?a.addEventListener(c,b,!1):a.attachEvent&&a.attachEvent("on"+c,b)},off:function(a,c,b){a.removeEventListener?a.removeEventListener(c,b,!1):a.detachEvent&&a.detachEvent("on"+ c,b)},param:function(a){function c(a,b){b=e.support.isFunction(b)?b():null==b?"":b;g.push(encodeURIComponent(a)+"="+encodeURIComponent(b))}function b(a,h){var f;if(e.support.isArray(h))e.support.each(h,function(e,h){/\[\]$/.test(a)?c(a,h):b(a+"["+("object"===typeof h?e:"")+"]",h)});else if("[object Object]"===x.call(h))for(f in h)b(a+"["+f+"]",h[f]);else c(a,h)}var f,g=[];for(f in a)b(f,a[f]);return g.join("&").replace(/%20/g,"+")},xhr:function(){try{return new window.XMLHttpRequest}catch(a){try{return new window.ActiveXObject("Microsoft.XMLHTTP")}catch(c){}}}, parseJSON:function(a){return!a?null:window.JSON&&window.JSON.parse?window.JSON.parse(a):(new Function("return "+a))()},stringifyJSON:function(a){function c(a){return'"'+a.replace(e,function(a){var b=g[a];return"string"===typeof b?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"'}function b(a){return 10>a?"0"+a:a}var e=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,g={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f", "\r":"\\r",'"':'\\"',"\\":"\\\\"};return window.JSON&&window.JSON.stringify?window.JSON.stringify(a):function h(a,e){var f,n,g,k=e[a];g=typeof k;k&&("object"===typeof k&&"function"===typeof k.toJSON)&&(k=k.toJSON(a),g=typeof k);switch(g){case "string":return c(k);case "number":return isFinite(k)?String(k):"null";case "boolean":return String(k);case "object":if(!k)return"null";switch(x.call(k)){case "[object Date]":return isFinite(k.valueOf())?'"'+k.getUTCFullYear()+"-"+b(k.getUTCMonth()+1)+"-"+b(k.getUTCDate())+ "T"+b(k.getUTCHours())+":"+b(k.getUTCMinutes())+":"+b(k.getUTCSeconds())+'Z"':"null";case "[object Array]":n=k.length;g=[];for(f=0;f<n;f++)g.push(h(f,k)||"null");return"["+g.join(",")+"]";default:g=[];for(f in k)C.call(k,f)&&(n=h(f,k))&&g.push(c(f)+":"+n);return"{"+g.join(",")+"}"}}}("",{"":a})},browser:{},storage:!(!window.localStorage||!window.StorageEvent)};e.support.corsable="withCredentials"in e.support.xhr();v=e.support.now();var t=navigator.userAgent.toLowerCase(),t=/(chrome)[ \/]([\w.]+)/.exec(t)|| /(webkit)[ \/]([\w.]+)/.exec(t)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(t)||/(msie) ([\w.]+)/.exec(t)||0>t.indexOf("compatible")&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(t)||[];e.support.browser[t[1]||""]=!0;e.support.browser.version=t[2]||"0";if(e.support.browser.msie||e.support.browser.mozilla&&"1"===e.support.browser.version.split(".")[0])e.support.storage=!1;e.find=function(a){var c;if(!arguments.length){for(c in q)if(q[c])return q[c];return null}return q[e.support.getAbsoluteURL(a)]||null};e.open= function(a,c){var b=a=e.support.getAbsoluteURL(a),f,g=a,d,h,m,l={},A=0,n={},r=[],k,u,s,p={};f=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/.exec(g.toLowerCase());var j={option:function(a,b){if(void 0===b)return d[a];d[a]=b;return this},data:function(a,b){if(void 0===b)return p[a];p[a]=b;return this},state:function(){return m},on:function(a,b){var c;if("object"===typeof a){for(c in a)j.on(c,a[c]);return this}c=l[a];if(!c){if(l.message.locked())return this;c=l[a]=z();c.order=l.message.order}c.add(b); return this},off:function(a,b){var c=l[a];c&&c.remove(b);return this},one:function(a,b){function c(){j.off(a,c);b.apply(j,arguments)}b.guid=b.guid||v++;c.guid=b.guid;return j.on(a,c)},fire:function(a){var b=l[a];b&&b.fire(j,y.call(arguments,1));return this},open:function(){var a,b,c=function(){var a,c;if(!b){b=!0;for(a=p.candidates=y.call(d.transports);!h&&a.length;)c=a.shift(),p.transport=c,p.url=j.buildURL("open"),h=e.transports[c](j,d);s&&s++;h?(j.fire("connecting"),h.open()):j.fire("close","notransport")}}, f=function(){b||(b=!0,j.fire("close","canceled"))};k&&clearTimeout(k);p={};for(a in l)l[a].unlock();h=void 0;m="preparing";d.sharing&&(p.transport="session",h=e.transports.session(j,d));h?c():d.prepare.call(j,c,f,d);return this},send:function(a,b,c,f){var g;if("opened"!==m)return r.push(arguments),this;g={id:++A,socket:d.id,type:a,data:b,reply:!(!c&&!f)};g.reply&&("session"===p.transport?(g.doneCallback=c,g.failCallback=f):n[A]={done:c,fail:f});h.send(e.support.isBinary(b)?b:d.outbound.call(j,g)); return this},close:function(){var a,b;d.reconnect=!1;k&&clearTimeout(k);if(w||!h||!h.feedback)j.fire("close",w?"error":"aborted"),d.notifyAbort&&"session"!==p.transport&&(b=document.head||document.getElementsByTagName("head")[0]||document.documentElement,a=document.createElement("script"),a.async=!1,a.src=j.buildURL("abort"),a.onload=a.onreadystatechange=function(){if(!a.readyState||/loaded|complete/.test(a.readyState))a.onload=a.onreadystatechange=null,a.parentNode&&a.parentNode.removeChild(a)}, b.insertBefore(a,b.firstChild));h&&h.close();return this},broadcast:function(a,b){var c=p.broadcastable;c&&c.broadcast({type:"fire",data:{type:a,data:b}});return this},_fire:function(a,b){var c;if(b){for(a=d.streamParser.call(j,a);a.length;)j._fire(a.shift());return this}e.support.isBinary(a)?c=[{type:"message",data:a}]:(c=d.inbound.call(j,a),c=null==c?[]:!e.support.isArray(c)?[c]:c);p.lastEventIds=[];e.support.each(c,function(a,b){var c,e=[b.type,b.data];d.lastEventId=b.id;p.lastEventIds.push(b.id); b.reply&&e.push(function(a){c||(c=!0,j.send("reply",{id:b.id,data:a}))});j.fire.apply(j,e).fire("_message",e)});return this},buildURL:function(a,b){var c="open"===a?{transport:p.transport,heartbeat:d.heartbeat,lastEventId:d.lastEventId}:"poll"===a?{transport:p.transport,lastEventIds:p.lastEventIds&&p.lastEventIds.join(","),lastEventId:d.lastEventId}:{};e.support.extend(c,{id:d.id,_:v++},d.params&&d.params[a],b);return d.urlBuilder.call(j,g,c,a)}};d=e.support.extend({},e.defaults,c);c&&c.transports&& (d.transports=y.call(c.transports));d.url=g;d.id=d.idGenerator.call(j);d.crossDomain=!(!f||!(f[1]!=location.protocol||f[2]!=location.hostname||(f[3]||("http:"===f[1]?80:443))!=(location.port||("http:"===location.protocol?80:443))));e.support.each(["connecting","open","message","close","waiting"],function(a,b){l[b]=z("message"!==b);l[b].order=a;var c=j[b],d=function(a){return j.on(b,a)};j[b]=!c?d:function(a){return(e.support.isFunction(a)?d:c).apply(this,arguments)}});j.on({connecting:function(){function a(){clearTimeout(b)} m="connecting";var b;0<d.timeout&&(b=setTimeout(function(){h.close();j.fire("close","timeout")},d.timeout),j.one("open",a).one("close",a));if(d.sharing&&"session"!==p.transport){var c=function(a){a=e.support.parseJSON(a);var b=a.data;if(a.target){if("p"===a.target)switch(a.type){case "send":j.send(b.type,b.data,b.doneCallback,b.failCallback);break;case "close":j.close()}}else"fire"===a.type&&j.fire(b.type,b.data)},f=function(a){k.broadcast({target:"c",type:"message",data:a})},n=function(){document.cookie= encodeURIComponent(r)+"="+encodeURIComponent(e.support.stringifyJSON({ts:e.support.now()+1,heir:(k.get("children")||[])[0]}))},l,k,r="socket-"+g,s={storage:function(){if(e.support.storage){var a=window.localStorage;return{init:function(){function b(a){a.key===r&&a.newValue&&c(a.newValue)}e.support.on(window,"storage",b);j.one("close",function(){e.support.off(window,"storage",b);j.one("close",function(){a.removeItem(r);a.removeItem(r+"-opened");a.removeItem(r+"-children")})})},broadcast:function(b){var d= e.support.stringifyJSON(b);a.setItem(r,d);setTimeout(function(){c(d)},50)},get:function(b){return e.support.parseJSON(a.getItem(r+"-"+b))},set:function(b,c){a.setItem(r+"-"+b,e.support.stringifyJSON(c))}}}},windowref:function(){var a=r.replace(/\W/g,""),b=document.getElementById(a),d;b||(b=document.createElement("div"),b.id=a,b.style.display="none",b.innerHTML='<iframe name="'+a+'" />',document.body.appendChild(b));d=b.firstChild.contentWindow;return{init:function(){d.callbacks=[c];d.fire=function(a){var b; for(b=0;b<d.callbacks.length;b++)d.callbacks[b](a)}},broadcast:function(a){!d.closed&&d.fire&&d.fire(e.support.stringifyJSON(a))},get:function(a){return!d.closed?d[a]:null},set:function(a,b){d.closed||(d[a]=b)}}}};k=s.storage()||s.windowref();k.init();p.broadcastable=k;k.set("children",[]);k.set("opened",!1);n();l=setInterval(n,1E3);j.on("_message",f).one("open",function(){k.set("opened",!0);k.broadcast({target:"c",type:"open"})}).one("close",function(a){clearInterval(l);document.cookie=encodeURIComponent(r)+ "=; expires=Thu, 01 Jan 1970 00:00:00 GMT";k.broadcast({target:"c",type:"close",data:{reason:a,heir:!w?d.id:(k.get("children")||[])[0]}});j.off("_message",f)})}},open:function(){function a(){c=setTimeout(function(){j.send("heartbeat").one("heartbeat",function(){b();a()});c=setTimeout(function(){h.close();j.fire("close","error")},d._heartbeat)},d.heartbeat-d._heartbeat)}function b(){clearTimeout(c)}m="opened";var c;d.heartbeat>d._heartbeat&&(a(),j.one("close",b));l.connecting.lock();for(k=u=s=null;r.length;)j.send.apply(j, r.shift())},close:function(){m="closed";var a,b,c=l.close.order;for(a in l)b=l[a],b.order<c&&b.lock();if(d.reconnect)j.one("close",function(){s=s||1;u=d.reconnect.call(j,u,s);!1!==u&&(k=setTimeout(function(){j.open()},u),j.fire("waiting",u,s))})},waiting:function(){m="waiting"},reply:function(a){var b=a.id,c=a.data;a=a.exception;var d=n[b];if(d&&(a=a?d.fail:d.done))e.support.isFunction(a)?a.call(j,c):j.fire(a,c).fire("_message",[a,c]),delete n[b]}});f=j.open();q[b]=f;return e.find(a)};e.defaults= {transports:["ws","sse","stream","longpoll"],timeout:!1,heartbeat:!1,lastEventId:0,sharing:!1,prepare:function(a){a()},reconnect:function(a){return 2*(a||250)},idGenerator:function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(a){var c=16*Math.random()|0;return("x"===a?c:c&3|8).toString(16)})},urlBuilder:function(a,c,b){return a+(/\?/.test(a)?"&":"?")+"when="+b+"&"+e.support.param(c)},inbound:e.support.parseJSON,outbound:e.support.stringifyJSON,credentials:!1,notifyAbort:!1, xdrURL:function(a){var c=/(?:^|; )(JSESSIONID|PHPSESSID)=([^;]*)/.exec(document.cookie);switch(c&&c[1]){case "JSESSIONID":return a.replace(/;jsessionid=[^\?]*|(\?)|$/,";jsessionid="+c[2]+"$1");case "PHPSESSID":return a.replace(/\?PHPSESSID=[^&]*&?|\?|$/,"?PHPSESSID="+c[2]+"&").replace(/&$/,"");default:return!1}},streamParser:function(a){var c=/\r\n|[\r\n]/g,b=[],e=this.data("data"),g=[],d=0,h;for(a=a.replace(/^\s+/g,"");h=c.exec(a);)b.push(a.substring(d,h.index)),d=h.index+h[0].length;b.push(a.length=== d?"":a.substring(d));e||(e=[],this.data("data",e));for(d=0;d<b.length;d++)(a=b[d])?/^data:\s/.test(a)?e.push(a.substring(6)):e[e.length-1]+=a:(g.push(e.join("\n")),e=[],this.data("data",e));return g},_heartbeat:5E3,longpollTest:!0};e.transports={session:function(a,c){function b(a,b){var c,e=a.length;for(c=0;c<e;c++)a[c]===b&&a.splice(c,1);return e!==a.length}function f(b){b=e.support.parseJSON(b);var d=b.data;if(b.target){if("c"===b.target)switch(b.type){case "open":a.fire("open");break;case "close":h|| (h=!0,"aborted"===d.reason?a.close():d.heir===c.id?a.fire("close",d.reason):setTimeout(function(){a.fire("close",d.reason)},100));break;case "message":if("connecting"===a.state())a.one("open",function(){a.fire.apply(a,d)});else a.fire.apply(a,d)}}else"fire"===b.type&&a.fire(d.type,d.data)}function g(){var a=RegExp("(?:^|; )("+encodeURIComponent(l)+")=([^;]*)").exec(document.cookie);if(a)return e.support.parseJSON(decodeURIComponent(a[2]))}var d,h,m,l="socket-"+c.url,q={storage:function(){function d(a){return e.support.parseJSON(h.getItem(l+ "-"+a))}if(e.support.storage){var h=window.localStorage;return{init:function(){function g(a){a.key===l&&a.newValue&&f(a.newValue)}var m=d("children").concat([c.id]);h.setItem(l+"-children",e.support.stringifyJSON(m));e.support.on(window,"storage",g);a.one("close",function(){var a=d("children");e.support.off(window,"storage",g);a&&b(a,c.id)&&h.setItem(l+"-children",e.support.stringifyJSON(a))});return d("opened")},broadcast:function(a){var b=e.support.stringifyJSON(a);h.setItem(l,b);setTimeout(function(){f(b)}, 50)}}}},windowref:function(){var d=window.open("",l.replace(/\W/g,""));if(d&&!d.closed&&d.callbacks)return{init:function(){d.callbacks.push(f);d.children.push(c.id);a.one("close",function(){h||(b(d.callbacks,f),b(d.children,c.id))});return d.opened},broadcast:function(a){!d.closed&&d.fire&&d.fire(e.support.stringifyJSON(a))}}}};if((d=g())&&!(1E3<e.support.now()-d.ts))if(m=q.storage()||q.windowref())return a.data("broadcastable",m),{open:function(){var b,h=c.timeout,l=c.heartbeat,q=c.outbound;c.timeout= c.heartbeat=!1;c.outbound=function(a){return a};b=setInterval(function(){var a=d;d=g();(!d||a.ts===d.ts)&&f(e.support.stringifyJSON({target:"c",type:"close",data:{reason:"error",heir:a.heir}}))},1E3);a.one("close",function(){clearInterval(b);c.timeout=h;c.heartbeat=l;c.outbound=q});m.init()&&setTimeout(function(){a.fire("open")},50)},send:function(a){m.broadcast({target:"p",type:"send",data:a})},close:function(){w||m.broadcast({target:"p",type:"close"})}}},ws:function(a){var c,b,f=window.WebSocket|| window.MozWebSocket;if(f)return{feedback:!0,open:function(){var g=e.support.getAbsoluteURL(a.data("url")).replace(/^http/,"ws");a.data("url",g);c=new f(g);c.onopen=function(b){a.data("event",b).fire("open")};c.onmessage=function(b){a.data("event",b)._fire(b.data)};c.onerror=function(c){a.data("event",c).fire("close",b?"aborted":"error")};c.onclose=function(c){a.data("event",c).fire("close",b?"aborted":c.wasClean?"done":"error")}},send:function(a){c.send(a)},close:function(){b=!0;c.close()}}},httpbase:function(a, c){function b(){d.length?f(c.url,d.shift()):g=!1}var f,g,d=[];f=!c.crossDomain||e.support.corsable?function(a,d){var f=e.support.xhr();f.onreadystatechange=function(){4===f.readyState&&b()};f.open("POST",a);f.setRequestHeader("Content-Type","text/plain; charset=UTF-8");e.support.corsable&&(f.withCredentials=c.credentials);f.send("data="+d)}:window.XDomainRequest&&c.xdrURL&&c.xdrURL.call(a,"t")?function(d,e){var f=new window.XDomainRequest;f.onload=f.onerror=b;f.open("POST",c.xdrURL.call(a,d));f.send("data="+ e)}:function(a,c){var d=document.createElement("form");d.action=a;d.target="socket-"+ ++v;d.method="POST";d.enctype=d.encoding="text/plain";d.acceptCharset="UTF-8";d.style.display="none";d.innerHTML='<textarea name="data"></textarea><iframe name="'+d.target+'"></iframe>';d.firstChild.value=c;e.support.on(d.lastChild,"load",function(){document.body.removeChild(d);b()});document.body.appendChild(d);d.submit()};return{send:function(a){d.push(a);g||(g=!0,b())}}},sse:function(a,c){var b,f=window.EventSource; if(f){if(c.crossDomain)try{if(!e.support.corsable||!("withCredentials"in new f("about:blank")))return}catch(g){return}return e.support.extend(e.transports.httpbase(a,c),{open:function(){var d=a.data("url");b=!c.crossDomain?new f(d):new f(d,{withCredentials:c.credentials});b.onopen=function(b){a.data("event",b).fire("open")};b.onmessage=function(b){a.data("event",b)._fire(b.data)};b.onerror=function(c){b.close();a.data("event",c).fire("close","done")}},close:function(){b.close()}})}},stream:function(a){a.data("candidates").unshift("streamxhr", "streamxdr","streamiframe")},streamxhr:function(a,c){var b;if(!(e.support.browser.msie&&10>+e.support.browser.version||c.crossDomain&&!e.support.corsable))return e.support.extend(e.transports.httpbase(a,c),{open:function(){var f;b=e.support.xhr();b.onreadystatechange=function(){function c(){var d=a.data("index"),e=b.responseText.length;d?e>d&&a._fire(b.responseText.substring(d,e),!0):a.fire("open")._fire(b.responseText,!0);a.data("index",e)}3===b.readyState&&200===b.status?e.support.browser.opera&& !f?f=e.support.iterate(c):c():4===b.readyState&&(f&&f(),a.fire("close",200===b.status?"done":"error"))};b.open(c.method||"GET",a.data("url"));e.support.corsable&&(b.withCredentials=c.credentials);b.send(null)},close:function(){b.abort()}})},streamiframe:function(a,c){var b,f,g=window.ActiveXObject;if(g&&!c.crossDomain){try{new g("htmlfile")}catch(d){return}return e.support.extend(e.transports.httpbase(a,c),{open:function(){var d,m;b=new g("htmlfile");b.open();b.close();d=b.createElement("iframe"); d.src=a.data("url");b.body.appendChild(d);m=d.contentDocument||d.contentWindow.document;f=e.support.iterate(function(){function b(){var a=g.cloneNode(!0);a.appendChild(m.createTextNode("."));a=a.innerText;return a.substring(0,a.length-1)}var g;if(m.firstChild){c.initIframe&&c.initIframe.call(a,d);g=m.body.lastChild;if(!g)return a.fire("close","error"),!1;a.fire("open")._fire(b(),!0);g.innerText="";f=e.support.iterate(function(){var c=b();c&&(g.innerText="",a._fire(c,!0));if("complete"===m.readyState)return a.fire("close", "done"),!1});return!1}})},close:function(){f();b.execCommand("Stop")}})}},streamxdr:function(a,c){var b,f=window.XDomainRequest;if(f&&c.xdrURL&&c.xdrURL.call(a,"t"))return e.support.extend(e.transports.httpbase(a,c),{open:function(){var e=c.xdrURL.call(a,a.data("url"));a.data("url",e);b=new f;b.onprogress=function(){var c=a.data("index"),e=b.responseText.length;c?a._fire(b.responseText.substring(c,e),!0):a.fire("open")._fire(b.responseText,!0);a.data("index",e)};b.onerror=function(){a.fire("close", "error")};b.onload=function(){a.fire("close","done")};b.open(c.method||"GET",e);b.send()},close:function(){b.abort()}})},longpoll:function(a){a.data("candidates").unshift("longpollajax","longpollxdr","longpolljsonp")},longpollajax:function(a,c){var b,f,g=0;if(!c.crossDomain||e.support.corsable)return e.support.extend(e.transports.httpbase(a,c),{open:function(){function d(){var h=a.buildURL(!g?"open":"poll",{count:++g});a.data("url",h);b=e.support.xhr();b.onreadystatechange=function(){var c;!f&&4=== b.readyState&&(200===b.status?(c=b.responseText)||1===g?(1===g&&a.fire("open"),c&&a._fire(c),d()):a.fire("close","done"):a.fire("close","error"))};b.open(c.method||"GET",h);e.support.corsable&&(b.withCredentials=c.credentials);b.send(null)}c.longpollTest?d():setTimeout(function(){a.fire("open");d()},50)},close:function(){f=!0;b.abort()}})},longpollxdr:function(a,c){var b,f=0,g=window.XDomainRequest;if(g&&c.xdrURL&&c.xdrURL.call(a,"t"))return e.support.extend(e.transports.httpbase(a,c),{open:function(){function d(){var e= c.xdrURL.call(a,a.buildURL(!f?"open":"poll",{count:++f}));a.data("url",e);b=new g;b.onload=function(){var c=b.responseText;c||1===f?(1===f&&a.fire("open"),c&&a._fire(c),d()):a.fire("close","done")};b.onerror=function(){a.fire("close","error")};b.open(c.method||"GET",e);b.send()}c.longpollTest?d():setTimeout(function(){a.fire("open");d()},50)},close:function(){b.abort()}})},longpolljsonp:function(a,c){var b,f,g=0,d=B.pop()||"socket_"+ ++v;return e.support.extend(e.transports.httpbase(a,c),{open:function(){function e(){var c= a.buildURL(!g?"open":"poll",{callback:d,count:++g}),l=document.head||document.getElementsByTagName("head")[0]||document.documentElement;a.data("url",c);b=document.createElement("script");b.async=!0;b.src=c;b.clean=function(){b.clean=b.onerror=b.onload=b.onreadystatechange=null;b.parentNode&&b.parentNode.removeChild(b)};b.onload=b.onreadystatechange=function(){if(!b.readyState||/loaded|complete/.test(b.readyState))b.clean(),f?(f=!1,e()):1===g?(a.fire("open"),e()):a.fire("close","done")};b.onerror= function(){b.clean();a.fire("close","error")};l.insertBefore(b,l.firstChild)}window[d]=function(b){f=!0;1===g&&a.fire("open");a._fire(b)};a.one("close",function(){window[d]=function(){};B.push(d)});c.longpollTest?e():setTimeout(function(){a.fire("open");e()},50)},close:function(){b.clean&&b.clean()}})}};e.finalize=function(){var a,c;for(a in q)c=q[a],"closed"!==c.state()&&c.close(),delete q[a]};e.support.on(window,"unload",function(){w=!0;e.finalize()});e.support.on(window,"online",function(){var a, c;for(a in q)c=q[a],"waiting"===c.state()&&c.open()});e.support.on(window,"offline",function(){var a,c;for(a in q)c=q[a],"opened"===c.state()&&c.fire("close","error")});window.portal=e})();
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ApplicationConfiguration.java b/client/src/com/vaadin/client/ApplicationConfiguration.java index 2291f21361..a6cc3cf531 100644 --- a/client/src/com/vaadin/client/ApplicationConfiguration.java +++ b/client/src/com/vaadin/client/ApplicationConfiguration.java @@ -36,6 +36,7 @@ import com.vaadin.client.metadata.NoDataException; import com.vaadin.client.metadata.TypeData; import com.vaadin.client.ui.UnknownComponentConnector; import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.communication.PushMode; import com.vaadin.shared.ui.ui.UIConstants; public class ApplicationConfiguration implements EntryPoint { @@ -201,6 +202,7 @@ public class ApplicationConfiguration implements EntryPoint { private ErrorMessage authorizationError; private ErrorMessage sessionExpiredError; private int heartbeatInterval; + private PushMode pushMode; private HashMap<Integer, String> unknownComponents; @@ -304,6 +306,10 @@ public class ApplicationConfiguration implements EntryPoint { return heartbeatInterval; } + public PushMode getPushMode() { + return pushMode; + } + public JavaScriptObject getVersionInfoJSObject() { return getJsoConfiguration(id).getVersionInfoJSObject(); } @@ -357,6 +363,14 @@ public class ApplicationConfiguration implements EntryPoint { heartbeatInterval = jsoConfiguration .getConfigInteger("heartbeatInterval"); + String pushMode = jsoConfiguration.getConfigString("pushMode"); + if (pushMode != null) { + this.pushMode = Enum + .valueOf(PushMode.class, pushMode.toUpperCase()); + } else { + this.pushMode = PushMode.DISABLED; + } + communicationError = jsoConfiguration.getConfigError("comErrMsg"); authorizationError = jsoConfiguration.getConfigError("authErrMsg"); sessionExpiredError = jsoConfiguration.getConfigError("sessExpMsg"); @@ -365,7 +379,6 @@ public class ApplicationConfiguration implements EntryPoint { if (jsoConfiguration.getConfigBoolean("initPending") == Boolean.FALSE) { setBrowserDetailsSent(); } - } /** diff --git a/client/src/com/vaadin/client/ApplicationConnection.java b/client/src/com/vaadin/client/ApplicationConnection.java index d59abc892a..0341a9d5c4 100644 --- a/client/src/com/vaadin/client/ApplicationConnection.java +++ b/client/src/com/vaadin/client/ApplicationConnection.java @@ -66,6 +66,7 @@ import com.vaadin.client.communication.HasJavaScriptConnectorHelper; import com.vaadin.client.communication.JavaScriptMethodInvocation; import com.vaadin.client.communication.JsonDecoder; import com.vaadin.client.communication.JsonEncoder; +import com.vaadin.client.communication.PushConnection; import com.vaadin.client.communication.RpcManager; import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.extensions.AbstractExtensionConnector; @@ -88,6 +89,7 @@ import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.Version; import com.vaadin.shared.communication.LegacyChangeVariablesInvocation; import com.vaadin.shared.communication.MethodInvocation; +import com.vaadin.shared.communication.PushMode; import com.vaadin.shared.communication.SharedState; import com.vaadin.shared.ui.ui.UIConstants; @@ -222,6 +224,8 @@ public class ApplicationConnection { private final RpcManager rpcManager; + private PushConnection push; + /** * If responseHandlingLocks contains any objects, response handling is * suspended until the collection is empty or a timeout has occurred. @@ -439,6 +443,8 @@ public class ApplicationConnection { scheduleHeartbeat(); + initializePush(); + Window.addWindowClosingHandler(new ClosingHandler() { @Override public void onWindowClosing(ClosingEvent event) { @@ -831,13 +837,16 @@ public class ApplicationConnection { response.getText().length() - 1); handleJSONText(jsonText, statusCode); } - }; - try { - doAjaxRequest(uri, payload, requestCallback); - } catch (RequestException e) { - VConsole.error(e); - endRequest(); + if (push != null) { + push.push(payload); + } else { + try { + doAjaxRequest(uri, payload, requestCallback); + } catch (RequestException e) { + VConsole.error(e); + endRequest(); + } } } @@ -848,7 +857,7 @@ public class ApplicationConnection { * @param jsonText * @param statusCode */ - private void handleJSONText(String jsonText, int statusCode) { + public void handleJSONText(String jsonText, int statusCode) { final Date start = new Date(); final ValueMap json; try { @@ -952,7 +961,7 @@ public class ApplicationConnection { * servicing the session so far. These values are always one request behind, * since they cannot be measured before the request is finished. */ - private ValueMap serverTimingInfo; + public ValueMap serverTimingInfo; static final int MAX_CSS_WAITS = 100; @@ -1462,7 +1471,11 @@ public class ApplicationConnection { + jsonText.length() + " characters of JSON"); VConsole.log("Referenced paintables: " + connectorMap.size()); - endRequest(); + if (meta == null || !meta.containsKey("async")) { + // End the request if the received message was a response, + // not sent asynchronously + endRequest(); + } if (Profiler.isEnabled()) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @@ -1473,7 +1486,6 @@ public class ApplicationConnection { } }); } - } /** @@ -3314,4 +3326,28 @@ public class ApplicationConnection { return Util.getConnectorForElement(this, getUIConnector().getWidget(), focusedElement); } + + private void initializePush() { + if (getConfiguration().getPushMode() != PushMode.DISABLED) { + push = GWT.create(PushConnection.class); + push.init(this); + + final String pushUri = addGetParameters( + translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX + + ApplicationConstants.PUSH_PATH + '/'), + UIConstants.UI_ID_PARAMETER + "=" + + getConfiguration().getUIId()); + + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + push.connect(pushUri); + } + }); + } + } + + public void handlePushMessage(String message) { + handleJSONText(message, 200); + } } diff --git a/client/src/com/vaadin/client/communication/PushConnection.java b/client/src/com/vaadin/client/communication/PushConnection.java new file mode 100644 index 0000000000..f87ddf430a --- /dev/null +++ b/client/src/com/vaadin/client/communication/PushConnection.java @@ -0,0 +1,135 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.client.communication; + +import java.util.ArrayList; + +import com.google.gwt.core.client.JavaScriptObject; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.VConsole; + +/** + * Represents the client-side endpoint of a bidirectional ("push") communication + * channel. Can be used to send UIDL request messages to the server and to + * receive UIDL messages from the server (either asynchronously or as a response + * to a UIDL request.) Delegates the UIDL handling to the + * {@link ApplicationConnection}. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class PushConnection { + + private ApplicationConnection connection; + + private JavaScriptObject socket; + + private ArrayList<String> messageQueue = new ArrayList<String>(); + + private boolean connected = false; + + private JavaScriptObject config = createConfig(); + + public PushConnection() { + } + + /** + * Two-phase construction to allow using GWT.create() + * + * @param ac + * The ApplicationConnection + */ + public void init(ApplicationConnection ac) { + this.connection = ac; + } + + public void connect(String uri) { + VConsole.log("Establishing Atmosphere connection"); + socket = doConnect(uri, getConfig()); + } + + public void push(String message) { + if (!connected) { + VConsole.log("Queuing Atmosphere message: " + message); + messageQueue.add(message); + } else { + VConsole.log("Pushing Atmosphere message: " + message); + doPush(socket, message); + } + } + + protected JavaScriptObject getConfig() { + return config; + } + + protected void onOpen() { + VConsole.log("Atmosphere connection established"); + connected = true; + for (String message : messageQueue) { + push(message); + } + messageQueue.clear(); + } + + protected void onMessage(String message) { + if (message.startsWith("for(;;);")) { + VConsole.log("Received Atmosphere message: " + message); + // "for(;;);[{json}]" -> "{json}" + message = message.substring(9, message.length() - 1); + connection.handlePushMessage(message); + } + } + + protected void onError() { + VConsole.error("Atmosphere connection failed!"); + } + + private static native JavaScriptObject createConfig() + /*-{ + return { + transport: 'websocket', + fallbackTransport: 'streaming', + contentType: 'application/json; charset=UTF-8', + reconnectInterval: '5000', + trackMessageLength: true + }; + }-*/; + + private native JavaScriptObject doConnect(String uri, + JavaScriptObject config) + /*-{ + var self = this; + + config.url = uri; + config.onOpen = $entry(function(response) { + self.@com.vaadin.client.communication.PushConnection::onOpen()(); + }); + config.onMessage = $entry(function(response) { + self.@com.vaadin.client.communication.PushConnection::onMessage(*)(response.responseBody); + }); + config.onError = $entry(function(response) { + self.@com.vaadin.client.communication.PushConnection::onError()(); + }); + + return $wnd.atmosphere.subscribe(config); + }-*/; + + private native void doPush(JavaScriptObject socket, String message) + /*-{ + socket.push(message); + }-*/; +} diff --git a/server/ivy.xml b/server/ivy.xml index d757e3a3cd..09e34fc075 100644 --- a/server/ivy.xml +++ b/server/ivy.xml @@ -49,6 +49,10 @@ <!-- Jsoup for BootstrapHandler --> <dependency org="org.jsoup" name="jsoup" rev="1.6.3" conf="build,ide,test -> default" /> + + <!-- Atmosphere --> + <dependency org="org.atmosphere" name="atmosphere-runtime" rev="1.0.12" + conf="build,ide,test -> default" /> <!-- TESTING DEPENDENCIES --> diff --git a/server/src/com/vaadin/server/BootstrapHandler.java b/server/src/com/vaadin/server/BootstrapHandler.java index 671279219e..d4f1ad308a 100644 --- a/server/src/com/vaadin/server/BootstrapHandler.java +++ b/server/src/com/vaadin/server/BootstrapHandler.java @@ -41,6 +41,7 @@ import org.jsoup.parser.Tag; import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.Version; +import com.vaadin.shared.communication.PushMode; import com.vaadin.ui.UI; /** @@ -337,8 +338,8 @@ public abstract class BootstrapHandler extends SynchronizedRequestHandler { VaadinRequest request = context.getRequest(); VaadinService vaadinService = request.getService(); - String staticFileLocation = vaadinService - .getStaticFileLocation(request); + String vaadinLocation = vaadinService.getStaticFileLocation(request) + + "/VAADIN/"; fragmentNodes .add(new Element(Tag.valueOf("iframe"), "") @@ -348,8 +349,17 @@ public abstract class BootstrapHandler extends SynchronizedRequestHandler { "position:absolute;width:0;height:0;border:0;overflow:hidden") .attr("src", "javascript:false")); - String bootstrapLocation = staticFileLocation - + "/VAADIN/vaadinBootstrap.js"; + if (context.getSession().getPushMode() != PushMode.DISABLED) { + // Load client-side dependencies for push support + fragmentNodes.add(new Element(Tag.valueOf("script"), "").attr( + "type", "text/javascript").attr("src", + vaadinLocation + "portal.min.js")); + fragmentNodes.add(new Element(Tag.valueOf("script"), "").attr( + "type", "text/javascript").attr("src", + vaadinLocation + "atmosphere.min.js")); + } + + String bootstrapLocation = vaadinLocation + "vaadinBootstrap.js"; fragmentNodes.add(new Element(Tag.valueOf("script"), "").attr("type", "text/javascript").attr("src", bootstrapLocation)); Element mainScriptTag = new Element(Tag.valueOf("script"), "").attr( @@ -477,6 +487,8 @@ public abstract class BootstrapHandler extends SynchronizedRequestHandler { appConfig.put("heartbeatInterval", vaadinService .getDeploymentConfiguration().getHeartbeatInterval()); + appConfig.put("pushMode", session.getPushMode().toString()); + String serviceUrl = getServiceUrl(context); if (serviceUrl != null) { appConfig.put(ApplicationConstants.SERVICE_URL, serviceUrl); diff --git a/server/src/com/vaadin/server/Constants.java b/server/src/com/vaadin/server/Constants.java index a9bc3e5b9e..d0f8507c94 100644 --- a/server/src/com/vaadin/server/Constants.java +++ b/server/src/com/vaadin/server/Constants.java @@ -47,6 +47,13 @@ public interface Constants { + "in web.xml. The default of 5min will be used.\n" + "==========================================================="; + static final String WARNING_PUSH_MODE_NOT_RECOGNIZED = "\n" + + "===========================================================\n" + + "WARNING: pushMode has been set to an unrecognized value\n" + + "in web.xml. The permitted values are \"disabled\", \"manual\",\n" + + "and \"automatic\". The default of \"disabled\" will be used.\n" + + "==========================================================="; + static final String WIDGETSET_MISMATCH_INFO = "\n" + "=================================================================\n" + "The widgetset in use does not seem to be built for the Vaadin\n" @@ -63,6 +70,7 @@ public interface Constants { static final String SERVLET_PARAMETER_RESOURCE_CACHE_TIME = "resourceCacheTime"; static final String SERVLET_PARAMETER_HEARTBEAT_INTERVAL = "heartbeatInterval"; static final String SERVLET_PARAMETER_CLOSE_IDLE_SESSIONS = "closeIdleSessions"; + static final String SERVLET_PARAMETER_PUSH_MODE = "pushMode"; static final String SERVLET_PARAMETER_UI_PROVIDER = "UIProvider"; // Configurable parameter names diff --git a/server/src/com/vaadin/server/DefaultDeploymentConfiguration.java b/server/src/com/vaadin/server/DefaultDeploymentConfiguration.java index 5b0c3fe8d1..d11bd69997 100644 --- a/server/src/com/vaadin/server/DefaultDeploymentConfiguration.java +++ b/server/src/com/vaadin/server/DefaultDeploymentConfiguration.java @@ -19,6 +19,8 @@ package com.vaadin.server; import java.util.Properties; import java.util.logging.Logger; +import com.vaadin.shared.communication.PushMode; + /** * The default implementation of {@link DeploymentConfiguration} based on a base * class for resolving system properties and a set of init parameters. @@ -33,6 +35,7 @@ public class DefaultDeploymentConfiguration implements DeploymentConfiguration { private int resourceCacheTime; private int heartbeatInterval; private boolean closeIdleSessions; + private PushMode pushMode; private final Class<?> systemPropertyBaseClass; /** @@ -55,6 +58,7 @@ public class DefaultDeploymentConfiguration implements DeploymentConfiguration { checkResourceCacheTime(); checkHeartbeatInterval(); checkCloseIdleSessions(); + checkPushMode(); } @Override @@ -167,12 +171,32 @@ public class DefaultDeploymentConfiguration implements DeploymentConfiguration { return heartbeatInterval; } + /** + * {@inheritDoc} + * <p> + * The default value is false. + */ @Override public boolean isCloseIdleSessions() { return closeIdleSessions; } /** + * {@inheritDoc} + * <p> + * The default mode is {@link PushMode#DISABLED}. + */ + @Override + public PushMode getPushMode() { + return pushMode; + } + + @Override + public Properties getInitParameters() { + return initParameters; + } + + /** * Log a warning if Vaadin is not running in production mode. */ private void checkProductionMode() { @@ -231,13 +255,19 @@ public class DefaultDeploymentConfiguration implements DeploymentConfiguration { .equals("true"); } - private Logger getLogger() { - return Logger.getLogger(getClass().getName()); + private void checkPushMode() { + String mode = getApplicationOrSystemProperty( + Constants.SERVLET_PARAMETER_PUSH_MODE, + PushMode.DISABLED.toString()); + try { + pushMode = Enum.valueOf(PushMode.class, mode.toUpperCase()); + } catch (IllegalArgumentException e) { + getLogger().warning(Constants.WARNING_PUSH_MODE_NOT_RECOGNIZED); + pushMode = PushMode.DISABLED; + } } - @Override - public Properties getInitParameters() { - return initParameters; + private Logger getLogger() { + return Logger.getLogger(getClass().getName()); } - } diff --git a/server/src/com/vaadin/server/DeploymentConfiguration.java b/server/src/com/vaadin/server/DeploymentConfiguration.java index bd4bc928f4..23edf8052a 100644 --- a/server/src/com/vaadin/server/DeploymentConfiguration.java +++ b/server/src/com/vaadin/server/DeploymentConfiguration.java @@ -19,6 +19,8 @@ package com.vaadin.server; import java.io.Serializable; import java.util.Properties; +import com.vaadin.shared.communication.PushMode; + /** * A collection of properties configured at deploy time as well as a way of * accessing third party properties not explicitly supported by this class. @@ -78,6 +80,14 @@ public interface DeploymentConfiguration extends Serializable { public boolean isCloseIdleSessions(); /** + * Returns the mode of bidirectional ("push") client-server communication + * that should be used. + * + * @return The push mode in use. + */ + public PushMode getPushMode(); + + /** * Gets the properties configured for the deployment, e.g. as init * parameters to the servlet or portlet. * diff --git a/server/src/com/vaadin/server/ServletPortletHelper.java b/server/src/com/vaadin/server/ServletPortletHelper.java index baf697cae3..c14467a10e 100644 --- a/server/src/com/vaadin/server/ServletPortletHelper.java +++ b/server/src/com/vaadin/server/ServletPortletHelper.java @@ -123,6 +123,10 @@ public class ServletPortletHelper implements Serializable { return hasPathPrefix(request, ApplicationConstants.HEARTBEAT_PATH + '/'); } + public static boolean isPushRequest(VaadinRequest request) { + return hasPathPrefix(request, ApplicationConstants.PUSH_PATH + '/'); + } + public static void initDefaultUIProvider(VaadinSession session, VaadinService vaadinService) throws ServiceException { String uiProperty = vaadinService.getDeploymentConfiguration() @@ -191,7 +195,7 @@ public class ServletPortletHelper implements Serializable { * <li>{@link Locale#getDefault()}</li> * </ol> */ - static Locale findLocale(Component component, VaadinSession session, + public static Locale findLocale(Component component, VaadinSession session, VaadinRequest request) { if (component == null) { component = UI.getCurrent(); @@ -225,5 +229,4 @@ public class ServletPortletHelper implements Serializable { return Locale.getDefault(); } - } diff --git a/server/src/com/vaadin/server/VaadinService.java b/server/src/com/vaadin/server/VaadinService.java index 3b088294e3..ceabaaf729 100644 --- a/server/src/com/vaadin/server/VaadinService.java +++ b/server/src/com/vaadin/server/VaadinService.java @@ -650,6 +650,7 @@ public abstract class VaadinService implements Serializable, Callback { session.setLocale(locale); session.setConfiguration(getDeploymentConfiguration()); session.setCommunicationManager(new LegacyCommunicationManager(session)); + session.setPushMode(getDeploymentConfiguration().getPushMode()); ServletPortletHelper.initDefaultUIProvider(session, this); onVaadinSessionStarted(request, session); diff --git a/server/src/com/vaadin/server/VaadinServletService.java b/server/src/com/vaadin/server/VaadinServletService.java index a12e2b47e2..ba78efa9bb 100644 --- a/server/src/com/vaadin/server/VaadinServletService.java +++ b/server/src/com/vaadin/server/VaadinServletService.java @@ -28,8 +28,10 @@ import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import com.vaadin.server.communication.PushRequestHandler; import com.vaadin.server.communication.ServletBootstrapHandler; import com.vaadin.server.communication.ServletUIInitHandler; +import com.vaadin.shared.communication.PushMode; import com.vaadin.ui.UI; public class VaadinServletService extends VaadinService { @@ -73,6 +75,9 @@ public class VaadinServletService extends VaadinService { List<RequestHandler> handlers = super.createRequestHandlers(); handlers.add(0, new ServletBootstrapHandler()); handlers.add(new ServletUIInitHandler()); + if (getDeploymentConfiguration().getPushMode() != PushMode.DISABLED) { + handlers.add(new PushRequestHandler(this)); + } return handlers; } diff --git a/server/src/com/vaadin/server/VaadinSession.java b/server/src/com/vaadin/server/VaadinSession.java index 844b7ff674..029a384e70 100644 --- a/server/src/com/vaadin/server/VaadinSession.java +++ b/server/src/com/vaadin/server/VaadinSession.java @@ -39,6 +39,7 @@ import com.vaadin.data.util.converter.Converter; import com.vaadin.data.util.converter.ConverterFactory; import com.vaadin.data.util.converter.DefaultConverterFactory; import com.vaadin.event.EventRouter; +import com.vaadin.shared.communication.PushMode; import com.vaadin.ui.AbstractField; import com.vaadin.ui.Table; import com.vaadin.ui.UI; @@ -128,6 +129,8 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { private transient Lock lock; + private PushMode pushMode; + /** * Create a new service session tied to a Vaadin service * @@ -806,11 +809,28 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { /** * Unlocks this session. This method should always be used in a finally * block after {@link #lock()} to ensure that the lock is always released. + * <p> + * If {@link #getPushMode() the push mode} is {@link PushMode#AUTOMATIC + * automatic}, pushes the changes in all UIs in this session to their + * respective clients. * - * @see #unlock() + * @see #lock() + * @see UI#push() */ public void unlock() { - getLockInstance().unlock(); + assert hasLock(); + try { + if (getPushMode() == PushMode.AUTOMATIC + && ((ReentrantLock) getLockInstance()).getHoldCount() == 1) { + // Only push if the reentrant lock will actually be released by + // this unlock() invocation. + for (UI ui : getUIs()) { + ui.push(); + } + } + } finally { + getLockInstance().unlock(); + } } /** @@ -1005,6 +1025,39 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { } /** + * Returns the mode of bidirectional ("push") communication that is used in + * this session. + * + * @return The push mode. + */ + public PushMode getPushMode() { + return pushMode; + } + + /** + * Sets the mode of bidirectional ("push") communication that should be used + * in this session. Set once on session creation and cannot be changed + * afterwards. + * + * @param pushMode + * The push mode to use. + * + * @throws IllegalArgumentException + * if the argument is null. + * @throws IllegalStateException + * if the mode is already set. + */ + public void setPushMode(PushMode pushMode) { + if (pushMode == null) { + throw new IllegalArgumentException("Push mode cannot be null"); + } + if (this.pushMode != null) { + throw new IllegalStateException("Push mode already set"); + } + this.pushMode = pushMode; + } + + /** * Sets this session to be closed and all UI state to be discarded at the * end of the current request, or at the end of the next request if there is * no ongoing one. diff --git a/server/src/com/vaadin/server/communication/MetadataWriter.java b/server/src/com/vaadin/server/communication/MetadataWriter.java index 7119e0ffeb..1a3f0e946a 100644 --- a/server/src/com/vaadin/server/communication/MetadataWriter.java +++ b/server/src/com/vaadin/server/communication/MetadataWriter.java @@ -51,6 +51,9 @@ public class MetadataWriter implements Serializable { * @param analyzeLayouts * Whether detected layout problems should be reported in client * and server console. + * @param async + * True if this message is sent by the server asynchronously, + * false if it is a response to a client message. * @param hilightedConnector * The connector that should be highlighted on the client or null * if none. @@ -62,8 +65,9 @@ public class MetadataWriter implements Serializable { * */ public void write(UI ui, Writer writer, boolean repaintAll, - boolean analyzeLayouts, ClientConnector hilightedConnector, - SystemMessages messages) throws IOException { + boolean analyzeLayouts, boolean async, + ClientConnector hilightedConnector, SystemMessages messages) + throws IOException { List<InvalidLayout> invalidComponentRelativeSizes = null; @@ -112,6 +116,13 @@ public class MetadataWriter implements Serializable { } } + if (async) { + if (metaOpen) { + writer.write(", "); + } + writer.write("\"async\":true"); + } + // meta instruction for client to enable auto-forward to // sessionExpiredURL after timer expires. if (messages != null && messages.getSessionExpiredMessage() == null diff --git a/server/src/com/vaadin/server/communication/PushConnection.java b/server/src/com/vaadin/server/communication/PushConnection.java new file mode 100644 index 0000000000..2db9d42763 --- /dev/null +++ b/server/src/com/vaadin/server/communication/PushConnection.java @@ -0,0 +1,140 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; +import java.io.Serializable; +import java.io.StringWriter; +import java.io.Writer; + +import org.atmosphere.cpr.AtmosphereResource; +import org.json.JSONException; + +import com.vaadin.ui.UI; + +/** + * Represents a bidirectional ("push") connection between a single UI and its + * client-side. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class PushConnection implements Serializable { + + private UI ui; + private boolean pending = true; + private AtmosphereResource resource; + + public PushConnection(UI ui) { + this.ui = ui; + } + + /** + * Pushes pending state changes and client RPC calls to the client. It is + * NOT safe to invoke this method if not holding the session lock. + * <p> + * This is internal API; please use {@link UI#push()} instead. + */ + public void push() { + if (!isConnected()) { + // Not currently connected; defer until connection established + setPending(true); + } else { + try { + push(true); + } catch (IOException e) { + // TODO Error handling + throw new RuntimeException("Push failed", e); + } + } + } + + /** + * Pushes pending state changes and client RPC calls to the client. + * + * @param async + * True if this push asynchronously originates from the server, + * false if it is a response to a client request. + * @throws IOException + */ + protected void push(boolean async) throws IOException { + Writer writer = new StringWriter(); + try { + new UidlWriter().write(getUI(), writer, false, false, async); + } catch (JSONException e) { + throw new IOException("Error writing UIDL", e); + } + // "Broadcast" the changes to the single client only + getResource().getBroadcaster().broadcast(writer.toString(), + getResource()); + } + + /** + * Associates this connection with the given AtmosphereResource. If there is + * a push pending, commits it. + * + * @param resource + * The AtmosphereResource representing the push channel. + * @throws IOException + */ + protected void connect(AtmosphereResource resource) throws IOException { + this.resource = resource; + if (isPending()) { + push(true); + setPending(false); + } + } + + /** + * Returns whether this connection is currently open. + */ + protected boolean isConnected() { + return resource != null + && resource.getBroadcaster().getAtmosphereResources() + .contains(resource); + } + + /** + * Marks that changes in the UI should be pushed as soon as a connection is + * established. + */ + protected void setPending(boolean pending) { + this.pending = pending; + } + + /** + * @return Whether the UI should be pushed as soon as a connection opens. + */ + protected boolean isPending() { + return pending; + } + + /** + * @return the UI associated with this connection. + */ + protected UI getUI() { + return ui; + } + + /** + * @return The AtmosphereResource associated with this connection or null if + * connection not open. + */ + protected AtmosphereResource getResource() { + return resource; + } +} diff --git a/server/src/com/vaadin/server/communication/PushHandler.java b/server/src/com/vaadin/server/communication/PushHandler.java new file mode 100644 index 0000000000..39481db46a --- /dev/null +++ b/server/src/com/vaadin/server/communication/PushHandler.java @@ -0,0 +1,184 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; +import java.io.Writer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.atmosphere.cpr.AtmosphereHandler; +import org.atmosphere.cpr.AtmosphereRequest; +import org.atmosphere.cpr.AtmosphereResource; +import org.atmosphere.cpr.AtmosphereResourceEvent; +import org.json.JSONException; + +import com.vaadin.server.LegacyCommunicationManager.InvalidUIDLSecurityKeyException; +import com.vaadin.server.ServiceException; +import com.vaadin.server.SessionExpiredException; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinService; +import com.vaadin.server.VaadinSession; +import com.vaadin.ui.UI; + +/** + * Establishes bidirectional ("push") communication channels + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class PushHandler implements AtmosphereHandler { + + private VaadinService service; + + public PushHandler(VaadinService service) { + this.service = service; + } + + @Override + public void onRequest(AtmosphereResource resource) { + + AtmosphereRequest req = resource.getRequest(); + VaadinRequest vaadinRequest = getVaadinRequest(req); + + VaadinSession session; + try { + session = service.findVaadinSession(vaadinRequest); + } catch (ServiceException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return; + } catch (SessionExpiredException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return; + } + + session.lock(); + try { + UI ui = service.findUI(vaadinRequest); + if (ui == null) { + throw new RuntimeException("UI not found!"); + } + PushConnection connection = ui.getPushConnection(); + + if (req.getMethod().equalsIgnoreCase("GET")) { + /* + * We received a request to establish a push channel for a UI. + * Associate the AtmosphereResource with the UI and leave the + * connection open by calling resource.suspend(). If there is a + * pending push, send it now. + */ + getLogger().log(Level.FINER, + "New push connection with transport {}", + resource.transport()); + resource.suspend(); + + connection.connect(resource); + } else if (req.getMethod().equalsIgnoreCase("POST")) { + /* + * We received a UIDL request through Atmosphere. If the push + * channel is bidirectional (websockets), the request was sent + * via the same channel. Otherwise, the client used a separate + * AJAX request. Handle the request and send changed UI state + * via the push channel (we do not respond to the request + * directly.) + */ + new ServerRpcHandler().handleRpc(ui, req.getReader(), + vaadinRequest); + connection.push(false); + } + } catch (InvalidUIDLSecurityKeyException e) { + // TODO Error handling + e.printStackTrace(); + } catch (JSONException e) { + // TODO Error handling + e.printStackTrace(); + } catch (IOException e) { + // TODO Error handling + e.printStackTrace(); + } finally { + session.unlock(); + } + } + + @Override + public void onStateChange(AtmosphereResourceEvent event) throws IOException { + AtmosphereResource resource = event.getResource(); + + String id = resource.uuid(); + if (event.isCancelled()) { + // The client closed the connection. + // TODO Do some cleanup + getLogger().log(Level.FINER, "Connection closed for resource {}", + id); + } else if (event.isResuming()) { + // A connection that was suspended earlier was resumed (committed to + // the client.) Should only happen if the transport is JSONP or + // long-polling. + getLogger() + .log(Level.FINER, "Resuming request for resource {}", id); + } else { + // A message was broadcast to this resource and should be sent to + // the client. We don't do any actual broadcasting, in the sense of + // sending to multiple recipients; any UIDL message is specific to a + // single client. + getLogger().log(Level.FINER, "Writing message to resource {}", id); + + resource.getResponse().setContentType( + "application/json; charset=UTF-8"); + Writer writer = resource.getResponse().getWriter(); + writer.write("for(;;);[{" + event.getMessage() + "}]"); + + switch (resource.transport()) { + case SSE: + case WEBSOCKET: + break; + case STREAMING: + writer.flush(); + break; + case JSONP: + case LONG_POLLING: + resource.resume(); + break; + default: + getLogger().log(Level.SEVERE, "Unknown transport {}", + resource.transport()); + } + } + } + + @Override + public void destroy() { + } + + private VaadinRequest getVaadinRequest(AtmosphereRequest req) { + while (req.getRequest() instanceof AtmosphereRequest) { + req = (AtmosphereRequest) req.getRequest(); + } + if (req.getRequest() instanceof VaadinRequest) { + return (VaadinRequest) req.getRequest(); + } else { + throw new IllegalArgumentException( + "Request does not wrap VaadinRequest"); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(PushHandler.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/PushRequestHandler.java b/server/src/com/vaadin/server/communication/PushRequestHandler.java new file mode 100644 index 0000000000..10ef16e11c --- /dev/null +++ b/server/src/com/vaadin/server/communication/PushRequestHandler.java @@ -0,0 +1,95 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server.communication; + +import java.io.IOException; + +import javax.servlet.ServletException; + +import org.atmosphere.client.TrackMessageSizeInterceptor; +import org.atmosphere.cpr.AtmosphereFramework; +import org.atmosphere.cpr.AtmosphereRequest; +import org.atmosphere.cpr.AtmosphereResponse; + +import com.vaadin.server.RequestHandler; +import com.vaadin.server.ServletPortletHelper; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinService; +import com.vaadin.server.VaadinServletRequest; +import com.vaadin.server.VaadinServletResponse; +import com.vaadin.server.VaadinSession; + +/** + * Handles requests to open a push (bidirectional) communication channel between + * the client and the server. After the initial request, communication through + * the push channel is managed by {@link PushHandler}. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class PushRequestHandler implements RequestHandler { + + private AtmosphereFramework atmosphere; + private PushHandler pushHandler; + + public PushRequestHandler(VaadinService service) { + + atmosphere = new AtmosphereFramework(); + + pushHandler = new PushHandler(service); + atmosphere.addAtmosphereHandler("/*", pushHandler); + atmosphere + .addInitParameter("org.atmosphere.cpr.sessionSupport", "true"); + + // Required to ensure the client-side knows at which points to split the + // message stream into individual messages when using certain transports + atmosphere.interceptor(new TrackMessageSizeInterceptor()); + + atmosphere.init(); + } + + @Override + public boolean handleRequest(VaadinSession session, VaadinRequest request, + VaadinResponse response) throws IOException { + + if (!ServletPortletHelper.isPushRequest(request)) { + return false; + } + + if (request instanceof VaadinServletRequest) { + try { + atmosphere.doCometSupport(AtmosphereRequest + .wrap((VaadinServletRequest) request), + AtmosphereResponse + .wrap((VaadinServletResponse) response)); + } catch (ServletException e) { + // TODO PUSH decide how to handle + throw new RuntimeException(e); + } + } else { + throw new IllegalArgumentException( + "Portlets not currently supported"); + } + + return true; + } + + public void destroy() { + atmosphere.destroy(); + } +} diff --git a/server/src/com/vaadin/server/communication/UIInitHandler.java b/server/src/com/vaadin/server/communication/UIInitHandler.java index c3e7119d3f..8275ea3efd 100644 --- a/server/src/com/vaadin/server/communication/UIInitHandler.java +++ b/server/src/com/vaadin/server/communication/UIInitHandler.java @@ -37,6 +37,7 @@ import com.vaadin.server.VaadinRequest; import com.vaadin.server.VaadinResponse; import com.vaadin.server.VaadinService; import com.vaadin.server.VaadinSession; +import com.vaadin.shared.communication.PushMode; import com.vaadin.shared.ui.ui.UIConstants; import com.vaadin.ui.UI; @@ -205,6 +206,10 @@ public abstract class UIInitHandler extends SynchronizedRequestHandler { // Set thread local here so it is available in init UI.setCurrent(ui); + if (session.getPushMode() != PushMode.DISABLED) { + ui.setPushConnection(new PushConnection(ui)); + } + ui.doInit(request, uiId.intValue()); session.addUI(ui); @@ -263,7 +268,7 @@ public abstract class UIInitHandler extends SynchronizedRequestHandler { writer.write(uI.getSession().getCommunicationManager() .getSecurityKeyUIDL(request)); } - new UidlWriter().write(uI, writer, true, false); + new UidlWriter().write(uI, writer, true, false, false); writer.write("}"); String initialUIDL = writer.toString(); diff --git a/server/src/com/vaadin/server/communication/UidlRequestHandler.java b/server/src/com/vaadin/server/communication/UidlRequestHandler.java index 0de9029063..32f9df3eff 100644 --- a/server/src/com/vaadin/server/communication/UidlRequestHandler.java +++ b/server/src/com/vaadin/server/communication/UidlRequestHandler.java @@ -169,7 +169,7 @@ public class UidlRequestHandler extends SynchronizedRequestHandler { .getSecurityKeyUIDL(request)); } - new UidlWriter().write(ui, writer, repaintAll, analyzeLayouts); + new UidlWriter().write(ui, writer, repaintAll, analyzeLayouts, false); closeJsonMessage(writer); } diff --git a/server/src/com/vaadin/server/communication/UidlWriter.java b/server/src/com/vaadin/server/communication/UidlWriter.java index 81bbb91649..79ae8af07e 100644 --- a/server/src/com/vaadin/server/communication/UidlWriter.java +++ b/server/src/com/vaadin/server/communication/UidlWriter.java @@ -62,14 +62,18 @@ public class UidlWriter implements Serializable { * Whether the client should re-render the whole UI. * @param analyzeLayouts * Whether detected layout problems should be logged. + * @param async + * True if this message is sent by the server asynchronously, + * false if it is a response to a client message. + * * @throws IOException * If the writing fails. * @throws JSONException * If the JSON serialization fails. */ public void write(UI ui, Writer writer, boolean repaintAll, - boolean analyzeLayouts) throws IOException, JSONException { - + boolean analyzeLayouts, boolean async) throws IOException, + JSONException { ArrayList<ClientConnector> dirtyVisibleConnectors = ui .getConnectorTracker().getDirtyVisibleConnectors(); VaadinSession session = ui.getSession(); @@ -153,7 +157,7 @@ public class UidlWriter implements Serializable { .getSystemMessages(ui.getLocale(), null); // TODO hilightedConnector new MetadataWriter().write(ui, writer, repaintAll, analyzeLayouts, - null, messages); + async, null, messages); writer.write(", "); writer.write("\"resources\" : "); @@ -289,8 +293,6 @@ public class UidlWriter implements Serializable { assert (uiConnectorTracker.getDirtyConnectors().isEmpty()) : "Connectors have been marked as dirty during the end of the paint phase. This is most certainly not intended."; writePerformanceData(ui, writer); - } catch (IOException ex) { - throw new RuntimeException(ex); } finally { uiConnectorTracker.setWritingResponse(false); } diff --git a/server/src/com/vaadin/ui/UI.java b/server/src/com/vaadin/ui/UI.java index a20c2b2087..162d072222 100644 --- a/server/src/com/vaadin/ui/UI.java +++ b/server/src/com/vaadin/ui/UI.java @@ -37,8 +37,10 @@ import com.vaadin.server.VaadinRequest; import com.vaadin.server.VaadinService; import com.vaadin.server.VaadinServlet; import com.vaadin.server.VaadinSession; +import com.vaadin.server.communication.PushConnection; import com.vaadin.shared.EventId; import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.communication.PushMode; import com.vaadin.shared.ui.ui.ScrollClientRpc; import com.vaadin.shared.ui.ui.UIConstants; import com.vaadin.shared.ui.ui.UIServerRpc; @@ -470,6 +472,8 @@ public abstract class UI extends AbstractSingleComponentContainer implements private Navigator navigator; + private PushConnection pushConnection = new PushConnection(this); + /** * This method is used by Component.Focusable objects to request focus to * themselves. Focus renders must be handled at window level (instead of @@ -1118,4 +1122,48 @@ public abstract class UI extends AbstractSingleComponentContainer implements return loadingIndicator; } + /** + * Pushes the pending changes and client RPC invocations of this UI to the + * client-side. + * <p> + * As with all UI methods, it is not safe to call push() without holding the + * {@link VaadinSession#lock() session lock}. + * + * @throws IllegalStateException + * if push is disabled. + * @throws UIDetachedException + * if this UI is not attached to a session. + * + * @see VaadinSession#getPushMode() + * + * @since 7.1 + */ + public void push() { + VaadinSession session = getSession(); + if (session != null) { + if (session.getPushMode() == PushMode.DISABLED) { + throw new IllegalStateException("Push not enabled"); + } + assert pushConnection != null; + pushConnection.push(); + } else { + throw new UIDetachedException("Trying to push a detached UI"); + } + } + + /** + * Returns the internal push connection object used by this UI. This method + * should only be called by the framework. + */ + public PushConnection getPushConnection() { + return pushConnection; + } + + /** + * Sets the internal push connection object used by this UI. This method + * should only be called by the framework. + */ + public void setPushConnection(PushConnection connection) { + pushConnection = connection; + } } diff --git a/shared/src/com/vaadin/shared/ApplicationConstants.java b/shared/src/com/vaadin/shared/ApplicationConstants.java index 220679e69c..5f23a3dc38 100644 --- a/shared/src/com/vaadin/shared/ApplicationConstants.java +++ b/shared/src/com/vaadin/shared/ApplicationConstants.java @@ -28,6 +28,8 @@ public class ApplicationConstants implements Serializable { public static final String HEARTBEAT_PATH = "HEARTBEAT"; + public static final String PUSH_PATH = "PUSH"; + public static final String PUBLISHED_FILE_PATH = APP_PATH + '/' + "PUBLISHED"; diff --git a/shared/src/com/vaadin/shared/communication/PushMode.java b/shared/src/com/vaadin/shared/communication/PushMode.java new file mode 100644 index 0000000000..f7414a89ea --- /dev/null +++ b/shared/src/com/vaadin/shared/communication/PushMode.java @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.shared.communication; + +/** + * The mode of bidirectional ("push") communication that is in use. + * + * @see com.vaadin.server.DeploymentConfiguration#getPushMode() + * + * @author Vaadin Ltd + * @since 7.1 + */ +public enum PushMode { + /** + * Push is disabled. Regular AJAX requests are used to communicate between + * the client and the server. Asynchronous messages from the server are not + * possible. {@link com.vaadin.ui.UI#push() ui.push()} throws + * IllegalStateException. + * <p> + * This is the default mode unless + * {@link com.vaadin.server.DeploymentConfiguration#getPushMode() + * configured} otherwise. + */ + DISABLED, + + /** + * Push is enabled. A bidirectional channel is established between the + * client and server and used to communicate state changes and RPC + * invocations. The client is not automatically updated if the server-side + * state is asynchronously changed; {@link com.vaadin.ui.UI#push() + * ui.push()} must be explicitly called. + */ + MANUAL, + + /** + * Push is enabled. Like {@link #MANUAL}, but asynchronous changes to the + * server-side state are automatically pushed to the client once the session + * lock is released. + */ + AUTOMATIC; +} diff --git a/uitest/ivy.xml b/uitest/ivy.xml index 4196cca4da..55f682e14e 100644 --- a/uitest/ivy.xml +++ b/uitest/ivy.xml @@ -48,16 +48,18 @@ <!-- Newest Jetty does not work with Ivy currently (orbit -> jar mapping problem) --> <dependency org="org.eclipse.jetty" name="jetty-server" - rev="7.4.5.v20110725" conf="build, ide, jetty-run->default" /> + rev="7.5.0.v20110901" conf="build, ide, jetty-run->default" /> <!-- jetty-servlets needed in .war by ProxyTest, but not by jetty-runner --> <dependency org="org.eclipse.jetty" name="jetty-servlets" - rev="7.4.5.v20110725" conf="build, ide->default" /> + rev="7.5.0.v20110901" conf="build, ide->default" /> + <dependency org="org.eclipse.jetty" name="jetty-websocket" + rev="7.5.0.v20110901" conf="build, ide->default" /> <!-- <dependency org="org.mortbay.jetty" name="jetty-util" --> <!-- rev="8.1.5.v20120716" conf="build,ide,jetty-run->default" /> --> <dependency org="org.eclipse.jetty" name="jetty-webapp" - rev="7.4.5.v20110725" conf="build, ide,jetty-run->default" /> + rev="7.5.0.v20110901" conf="build, ide,jetty-run->default" /> <dependency org="org.mortbay.jetty" name="jetty-runner" - rev="7.4.5.v20110725" conf="jetty-run->default" /> + rev="7.5.0.v20110901" conf="jetty-run->default" /> <dependency org="junit" name="junit" rev="4.5" conf="build,ide -> default" /> |