From ee33e544f9b165607f409602e8f6aa0b1f89515a Mon Sep 17 00:00:00 2001 From: Marc Englund Date: Tue, 1 Sep 2009 11:51:58 +0000 Subject: [PATCH] Appengine fixes; This version uses GAE sessions, but attempts to keep synchronization using memcache. For #2835 svn changeset:8614/svn branch:6.1 --- WebContent/WEB-INF/web.xml | 14 ++ build/build.xml | 3 +- .../gwt/client/ApplicationConnection.java | 29 +++- .../server/AbstractApplicationServlet.java | 19 +-- .../gwt/server/GAEApplicationServlet.java | 96 +++++++++++ .../vaadin/tests/appengine/GAESyncTest.java | 152 ++++++++++++++++++ src/com/vaadin/tests/appengine/img1.png | Bin 0 -> 7433 bytes 7 files changed, 295 insertions(+), 18 deletions(-) create mode 100644 src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java create mode 100644 src/com/vaadin/tests/appengine/GAESyncTest.java create mode 100644 src/com/vaadin/tests/appengine/img1.png diff --git a/WebContent/WEB-INF/web.xml b/WebContent/WEB-INF/web.xml index e1a96e7413..652179b7dc 100644 --- a/WebContent/WEB-INF/web.xml +++ b/WebContent/WEB-INF/web.xml @@ -350,6 +350,15 @@ application com.vaadin.tests.book.ChatApplication + + + + GAESyncTest + com.vaadin.terminal.gwt.server.GAEApplicationServlet + + application + com.vaadin.tests.appengine.GAESyncTest + @@ -503,6 +512,11 @@ ChatServlet /chat/* + + + + GAESyncTest + /gaesynctest/* diff --git a/build/build.xml b/build/build.xml index 2436527241..99c8eba8ed 100644 --- a/build/build.xml +++ b/build/build.xml @@ -239,6 +239,7 @@ + @@ -854,7 +855,7 @@ - + diff --git a/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java b/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java index 69174587a6..31ee4dad81 100755 --- a/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java +++ b/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java @@ -292,14 +292,15 @@ public class ApplicationConnection { return (activeRequests > 0); } - private void makeUidlRequest(String requestData, boolean repaintAll, - boolean forceSync, boolean analyzeLayouts) { + private void makeUidlRequest(final String requestData, + final boolean repaintAll, final boolean forceSync, + final boolean analyzeLayouts) { startRequest(); // Security: double cookie submission pattern - requestData = uidl_security_key + VAR_BURST_SEPARATOR + requestData; + final String rd = uidl_security_key + VAR_BURST_SEPARATOR + requestData; - console.log("Making UIDL Request with params: " + requestData); + console.log("Making UIDL Request with params: " + rd); String uri = getAppUri() + "UIDL" + configuration.getPathInfo(); if (repaintAll) { // collect some client side data that will be sent to server on @@ -331,13 +332,14 @@ public class ApplicationConnection { } if (!forceSync) { + boolean success = false; final RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri); // TODO enable timeout // rb.setTimeoutMillis(timeoutMillis); rb.setHeader("Content-Type", "text/plain;charset=utf-8"); try { - rb.sendRequest(requestData, new RequestCallback() { + rb.sendRequest(rd, new RequestCallback() { public void onError(Request request, Throwable exception) { showCommunicationError(exception.getMessage()); endRequest(); @@ -358,6 +360,21 @@ public class ApplicationConnection { showCommunicationError("Invalid status code 0 (server down?)"); return; // TODO could add more cases + case 503: + // We'll assume msec instead of the usual seconds + int delay = Integer.parseInt(response + .getHeader("Retry-After")); + console.log("503, retrying in " + delay + "msec"); + (new Timer() { + @Override + public void run() { + activeRequests--; + makeUidlRequest(requestData, repaintAll, + forceSync, analyzeLayouts); + } + }).schedule(delay); + return; + } if ("init".equals(uidl_security_key)) { // Read security key @@ -415,7 +432,7 @@ public class ApplicationConnection { syncSendForce(((HTTPRequestImpl) GWT.create(HTTPRequestImpl.class)) .createXmlHTTPRequest(), uri + "&" + PARAM_UNLOADBURST - + "=1", requestData); + + "=1", rd); } } diff --git a/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java b/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java index 0640477e5c..af1847a7b7 100644 --- a/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java +++ b/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java @@ -475,12 +475,6 @@ public abstract class AbstractApplicationServlet extends HttpServlet { .endTransaction(application, request); } - // Work-around for GAE session problem. Explicitly touch session so - // it is re-serialized. - HttpSession session = request.getSession(false); - if (session != null) { - session.setAttribute("sessionUpdated", new Date().getTime()); - } } } @@ -879,7 +873,7 @@ public abstract class AbstractApplicationServlet extends HttpServlet { * @return true if an DownloadStream was sent to the client, false otherwise * @throws IOException */ - private boolean handleURI(CommunicationManager applicationManager, + protected boolean handleURI(CommunicationManager applicationManager, Window window, HttpServletRequest request, HttpServletResponse response) throws IOException { // Handles the URI @@ -1085,11 +1079,11 @@ public abstract class AbstractApplicationServlet extends HttpServlet { } } - private enum RequestType { + enum RequestType { FILE_UPLOAD, UIDL, OTHER; } - private RequestType getRequestType(HttpServletRequest request) { + protected RequestType getRequestType(HttpServletRequest request) { if (isFileUploadRequest(request)) { return RequestType.FILE_UPLOAD; } else if (isUIDLRequest(request)) { @@ -1812,8 +1806,11 @@ public abstract class AbstractApplicationServlet extends HttpServlet { String path = getRequestPathInfo(request); // Main window as the URI is empty - if (!(path == null || path.length() == 0 || path.equals("/") || path - .startsWith("/APP/"))) { + if (!(path == null || path.length() == 0 || path.equals("/"))) { + if (path.startsWith("/APP/")) { + // Use main window for application resources + return application.getMainWindow(); + } String windowName = null; if (path.charAt(0) == '/') { path = path.substring(1); diff --git a/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java b/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java new file mode 100644 index 0000000000..13d01cfb49 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java @@ -0,0 +1,96 @@ +package com.vaadin.terminal.gwt.server; + +import java.io.IOException; +import java.util.Date; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import com.google.appengine.api.memcache.Expiration; +import com.google.appengine.api.memcache.MemcacheService; +import com.google.appengine.api.memcache.MemcacheServiceFactory; +import com.google.apphosting.api.DeadlineExceededException; +import com.vaadin.ui.Window; + +public class GAEApplicationServlet extends ApplicationServlet { + + private static final long serialVersionUID = 2179597952818898526L; + + private static final String MUTEX_BASE = "vaadin.gae.mutex."; + // Note: currently interpreting Retry-After as ms, not sec + private static final int RETRY_AFTER_MILLISECONDS = 100; + private static final int KEEP_MUTEX_MILLISECONDS = 100; + + @Override + protected void service(HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + + boolean locked = false; + MemcacheService memcache = null; + String mutex = null; + try { + RequestType requestType = getRequestType(request); + if (requestType == RequestType.UIDL) { + memcache = MemcacheServiceFactory.getMemcacheService(); + mutex = MUTEX_BASE + request.getSession().getId(); + // try to get lock + locked = memcache.put(mutex, 1, Expiration.byDeltaSeconds(40), + MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT); + if (!locked) { + // could not obtain lock, tell client to retry + request.setAttribute("noSerialize", new Object()); + response + .setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + // Note: currently interpreting Retry-After as ms, not sec + response.setHeader("Retry-After", "" + + RETRY_AFTER_MILLISECONDS); + return; + } + + } + + super.service(request, response); + + if (request.getAttribute("noSerialize") == null) { + // Explicitly touch session so it is re-serialized. + HttpSession session = request.getSession(false); + if (session != null) { + session + .setAttribute("sessionUpdated", new Date() + .getTime()); + } + } + + } catch (DeadlineExceededException e) { + System.err.println("DeadlineExceeded for " + + request.getSession().getId()); + // TODO i18n? + criticalNotification( + request, + response, + "Deadline Exceeded", + "I'm sorry, but the operation took too long to complete. We'll try reloading to see where we're at, please take note of any unsaved data...", + "", null); + } finally { + // "Next, please!" + if (locked) { + memcache.delete(mutex, KEEP_MUTEX_MILLISECONDS); + } + + } + } + + protected boolean handleURI(CommunicationManager applicationManager, + Window window, HttpServletRequest request, + HttpServletResponse response) throws IOException { + + if (super.handleURI(applicationManager, window, request, response)) { + request.setAttribute("noSerialize", new Object()); + return true; + } + return false; + } + +} diff --git a/src/com/vaadin/tests/appengine/GAESyncTest.java b/src/com/vaadin/tests/appengine/GAESyncTest.java new file mode 100644 index 0000000000..0f5c85b239 --- /dev/null +++ b/src/com/vaadin/tests/appengine/GAESyncTest.java @@ -0,0 +1,152 @@ +package com.vaadin.tests.appengine; + +import com.google.apphosting.api.DeadlineExceededException; +import com.vaadin.Application; +import com.vaadin.data.Property; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.terminal.ClassResource; +import com.vaadin.terminal.DownloadStream; +import com.vaadin.ui.Button; +import com.vaadin.ui.Embedded; +import com.vaadin.ui.GridLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.TextField; +import com.vaadin.ui.Window; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.Window.Notification; + +public class GAESyncTest extends Application { + + /** + * + */ + private static final long serialVersionUID = -3724319151122707926l; + + @Override + public void init() { + setMainWindow(new IntrWindow(this)); + + } + + @Override + public void terminalError(com.vaadin.terminal.Terminal.ErrorEvent event) { + Throwable t = event.getThrowable(); + // Was this caused by a GAE timeout? + while (t != null) { + if (t instanceof DeadlineExceededException) { + getMainWindow().showNotification("Bugger!", + "Deadline Exceeded", Notification.TYPE_ERROR_MESSAGE); + return; + } + t = t.getCause(); + } + + super.terminalError(event); + + } + + private class IntrWindow extends Window { + private int n = 0; + private static final long serialVersionUID = -6521351715072191625l; + TextField tf; + Label l; + Application app; + GridLayout gl; + + private IntrWindow(Application app) { + + this.app = app; + tf = new TextField("Echo thingie"); + tf.setImmediate(true); + tf.addListener(new Property.ValueChangeListener() { + public void valueChange(ValueChangeEvent event) { + IntrWindow.this.showNotification((String) event + .getProperty().getValue()); + + } + + }); + addComponent(tf); + + l = new Label("" + n); + addComponent(l); + + { + Button b = new Button("Slow", new Button.ClickListener() { + public void buttonClick(ClickEvent event) { + try { + Thread.sleep((40000)); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + }); + addComponent(b); + } + + { + Button b = new Button("Add", new Button.ClickListener() { + + public void buttonClick(ClickEvent event) { + if (getWindow() == getApplication().getMainWindow()) { + getWindow().showNotification("main"); + try { + Thread.sleep((5000)); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + addImage(); + } + + }); + addComponent(b); + } + + gl = new GridLayout(30, 50); + addComponent(gl); + + } + + private void addImage() { + ClassResource res = new ClassResource("img1.png", app) { + + private static final long serialVersionUID = 1L; + + @Override + public DownloadStream getStream() { + try { + Thread.sleep((long) (Math.random() * 5000)); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return super.getStream(); + } + + }; + res.setCacheTime(0); + Embedded emb = new Embedded("" + n, res); + emb.setWidth("30px"); + emb.setHeight("5px"); + gl.addComponent(emb); + l.setValue("" + n++); + } + + } + + @Override + public Window getWindow(String name) { + Window w = super.getWindow(name); + if (w == null) { + w = new IntrWindow(this); + addWindow(w); + } + return w; + + } + +} diff --git a/src/com/vaadin/tests/appengine/img1.png b/src/com/vaadin/tests/appengine/img1.png new file mode 100644 index 0000000000000000000000000000000000000000..d249324e0c8551477d07c9d566c989e436b4f014 GIT binary patch literal 7433 zcmX|G3p`W(8>eNG6N+-C%UmK}w_Qq#Xt|^}-Br5UW^R$ZrCPbH83(24CPH-8MX264 zU1XahmfO7Qb?L?wDrJ*0W83-vM1MZVvCp>O@A*B?_xV2G=Q*kC*RGs4)nuxMhQ_p2 zKAs!Fef8Ki1rPo@uRZz&+;pORwjI#W(5o7|G|$`W&DPME^K_Lb&5!+>sl+ghgEtp| z8v!N|2;LO%4Gj!qnlCMYVOXtF)oJMNFh|+`niFrBIeEn?l&2+%v&m3VgdH@scDn`T z{Mfr=-?A!jd&WUa4-Ec+q!ZmpJNf&QBu$qZAWl4H@)uWY;!{TJi5<^^oe#0~c}Ovt zmwJ6JuaEFX8cK*ws9Z{r7h9%3OKldz!wm(7OpjRCn3_i4X8)|AT z64P1z0zq}P+t+a4iGx+nTHjF-%XB^xLFAn)n!$T`X}Y%t@J~KBJ2h4K(lJfapXUOF zocflOs3bZ57~d+P5Ns94mS1FG#SKuc$rI=({swyyrW#Dvel|abnm5jXf}J8E zyP_i0sdQqu75X>Nu9TBS);CF?2w7crkQUzaT;~nB$777)AF9P@2LQuZrYx`T2D^ddn~w_OxWdP+I-;B*MbY zs^RZkMnm;nNI$=vx&Gh7tfKZv#=dHzJFMK0i^u06GpP%x<ATdD?(hlO2S08|#IIXOd!7^qiQ_(1>lAV=E-Q5sr2cM+G z!~#KmeN}0MBm(bY5r_|5UC1!BF8whne7bT(l`QGv8AB6i7CJpDbZmSflSf&g3ARe8 zx6OfkvtnUL)XYHE@JMnVSAs3Zv*MUN6R#rCk6JeWvj^>I-#0(`RL`Xq$ z9NXdX^;U$PYxFNaZgw}`1{Lm8LOqN%&`cdCh|zI~;`-$1DG8BE#i1M@-k_B44(SH3 zVUV1h7$nj^fpiwrWZpJ?6FqUiTL^n-lnCI8Lw~Al2^CZ(_`Vdt$s$o|$BDUN`%!)mG0b23bZur-Jf`zM<9OME<^mROi8PL?7(MZ-ejU7`b4Xa z(662moZ@>g=+V;%9rWKF;t6jt6DAws&<;KTXGJ-4c;Et$AziG4KixS6Z@I*Vy_`%Y zQ;K#{@@cDA=g?}J_P1%h0`P?oNjt=)@=rk^_59(48L{w{B8*xe*)F2PXJ2$)x2c`* zlxx)JJ50BgNG8Rr>I~qUFJ#@rPDt{)OB8k$iyS+uO!@`RlX-cJe{*j^NVOG&1P`G_ zM>IZ%yFTTdWGF6ef}Y>f@!20hXx!UT6{WmR@`z5e$AR?$m%En5{N$`UG85nLE+<&^ zt7|gOeiRBC>qWBOR((WQ_H_WP0iFmIx~1)rmEsX*;-13%TN~0bJGcl z>diW+o`X?ExBsBDg-s!s$Fp;2err!mREy>ZgFyzHUoqH1(;8XVJ6rsgu~?{;Ogf9j z>g?$WZo419h<7Dn67SSyOPnEfurxdyRa)G>DeMflttoi%J2`4tPRJL zO35j;;Z&oo|NV}#Via>n<>@8xEc=~}r0Z9cC4Ig6P#VAprp5xZ++(KlvXd-%#itjQ zA0a#6pZD|0=dm~W7?o6d|2s9sI)|LH{CPStglTXgIHVI4o~(s&R=j6-7mYiqDvhk0 zu-?df6g#o$RKn^mgCDp_Ha<~*MeManSn;B-FMFte*)&?y>*lgt$()MzsKe~muDqth z_WQ$~^Y!c=BtCgvU?xk?Zckn}_jHXGp?LU~#~S{{0RAH1Yl@4i4W(ro^yq2N_h>F( znBLz1r^fE_$SC9(5#YZ*eEoWmnCGTsfHeI$soH;fG*+WQ3{1ZQjW?mFjTjLpg)fvhpIDA0V(hmx3Q0wsY{|1 zEe)t|iVY6tHEU;9F1ztRdfy8nI$-sQLpGtnBNqt86a@-4R2Lx`j$bpa&yhb0#F z;KLv;WXj*sKS`d6lc~6~ow=f> zF2{XYVT0bVT?TnmPC>3;%^?Ud0f;kW7z6&w=eQFb_-)SxB0)Wg`eVThRYg*`9wnDu zTi0}HUzO}+EVg{a7t zZBa)kyz^IP8S<^ z!}MBMV}bI633}%F8^S~Pv-D2Ja5X?_{rcUT#0AB-Yr*cPUj8m_u_K=vIXX5;nt&E2 z?NL0QgzQ?pfC9f@lcP#y6r~EAy);^NK|(|WLIt!*>0F6BS|=`p-Qqd50uB2dOcg>m z=-nJ_UHay^Y3HcVl$M`j#?!sI)I6>S0+RiRx6{x8*k+<$Y)EQ3n~){1{-5d zpz{tPgtl#uWco=I5Er@B9j({Wh0lGQ3BTQytMkNoJj@ANO~;ISKw5-5Zl>C~y?jBp zk~Ruz9PsOwm7bfdd-IpB2sw7`@520Iaw*AdPmJf5K>8!BAMf#>(;`qnurAxrKvQMI z4!fRJ7hEe4b@q4p`VCA_ywY7G$;i&j-sJ)s)!FEWx7S&WUpp(_Cv#L^CxZ_)z50PU zF7%>#hjnmbDrG%y;_y5F0*rc%I=|ksZhe1rE-hpA#XXP5nhj7eOxh}A|KQz7)Ws2> z!Z_RqAbZk44uWE=rmA@lAF0VCk}{A^Pb>;k{gmpWZ=`aah59g@1ry-+v2pdJ?v%x7 zx_iLQ%^&9$|g#_BMJfn^d-`WNwcHblu9S$O-uc6i>EV~!}t+jEu@ z^bzI-u&L(H&o#OTk;5lt%yT#AbuLB&6C-jNSE zy1io#|CHe^ohioc~ox6NB1*9h~B$~l==V4yxj zhH+uDmg^1zTLI!mY5)&b$rKCbk6yM%kF_2ZF^4s%hB)+uSRcMIZwP3tQR=pBGscZv zRlCBWzBQnj*~Y$}M+>M;;wG<2lt}pj8AtUlo&kxg9cHEc)pdZn#NnI$0D?S#^DKm| zP|RZeF?VUzPdU(gMB4>C0~ceoqoadxNi~AkS?JwWs|w%{;E$6R681+t?A-e~a15E_ ze%Z-+e4NqR5Q(>Btj5Rdt@6=eL^BGuozmQ%H5Pvi3qU$J71VO|p?pbizGKrt?F@&L zryecyNL)+c8I+i#B}6eC>AEMlExOMXt>aYbI~%@hUOBM=^U7Yv`^JL#5id+dHlQN` zC@|fok66a@sU}Dg2XHy$DK3Gl1Qo`lF{@uH|N=nlO1EHd8x zfj3?Y=<$xG6)M$e5Og4NE?|q7!A;X=1`e-bx-l5RbmyoLy$wmO<9>B6HLyS6!zi|A z)Cn4_ssJT84h!a*gJNvlnZb4S;p!30=uGzLH!~EpBfoeiUd5m{ZJI*2mb0H5T&{gl zN?4)%{kOejA2o|UJVZcc=`uQuJaG;QNz;1CIul4r- zceYwe{E|_}!TZcoj_M;Es!}zYEU6POh63WtxyGOD>g^y-_78YBg+y{DI0lI*6o&u9 zmuDq{r#qk(%2ep_?oEW~B})jC+Z6<5cN*~0(z>V*ee3X6l-Q~&fv4YtajG`Ib8Nvs zq?KXWs7V24pg9c6I1-Xyvf?M;R4Sm=AYe6?&ZYVPop`zORt_x#7%p2f8#%I-NS&{k z7hF0sBXD?d&PHQFFkk8VPw95--c3Moa_N51AlWHe=dXLc(8 zIwxsQu7NmN1h^u8Nkf$4+id(A#d1*3_Tj@aPe6T*z9GX4Si*wFlIL{Ol4_aq#TMi+ z2NM{JXH1>%?d@Z+b;sKzvqqL4tK!TtZ(!J%WmonvPPMomOS_8k_HE;8z2boGBrOdi z05K<&{gk_itQ|oMJ1SmG2%alt3|E@gc1Bht2)(LPB$y)}^!$91Ys`CbY3C0U#2xd3 z?V@`qldV%p^&Q>fsP3Ar`1ngvh+VG&`pSV*YLSd%Os0V(GelIn9ryW<=^JKGNWC#L zoOB0?EpZH`?&m?_jr*W=86L&-(L0WZFMsGO9VpC)%I5h|qvwUOh8s-lIvEGk%yvFc zlXL(p@-Y~$z+6Q#>F8V}nePl|cDOVM&_n5$W3J-RTaHBdklUk1*}SFmJ$wWGGBzE* zjP`2c7R+}4H+38h=Kg|Bet>atd$TQ;HedxWrTh5HSi>r*HV&5=G1%+q!b3LB^?0;_ z%(EM`MP3>|AQYEB7SOLh&5*R;v4>U{{~(v%TYi7Kg@qx?!J7fc?CZRR4`>-3Oy>%h zH=i|oc@&4E8tURKEC#Z5f4Sa#X{o8r?NhYPRVK}yN=!GV5Dt6&g=i7o3pYazQjAZi zP#vw2eXdzuZlT3d4^TutA1Ov}$|y3j5+uSXS3z*< zX}K#?FTYn!#o1C_Ud%N4wOJ*>KM~nB<%TtTxmhi#t+cLhpBRQ2dAoywPM*UZ>eI*J zPSEG@t~2N099?wL;!XXauW(PD3P)^PmjWF2MrzX@I4}+x^Q`+Rvx8|DjZ|_VJT0PQ zr)Jg`1bzmL$Yc*FoOs8NE#+N5M@Hr1t?;R5<(0CoR(C*>nn>G>%CU*l3GV5E1z>W= zXPwMctLp>>mcexyK@U!IPprxKdSkpCSH}1WGs7F8aEBIh=;}7gl>N07rD7C!y~V?( zUb09M5V{TVv!y6O!pj$+eU69Ww4kemX1iXHIi=u=4ICbbvqfKhlR&MQGK6l}qun_-m+WIyX63JT-QBAWh=_z{n%7FARCMcm#HG8x>`Q zpRS`@6;;VF`EF#%n*>6mQYjMnde1@^_d7J+sc-snF!>L|yW=Y|uM`F8?RfVcIOc=< zwPOhm(-ZXcIm&_KG!IXnaI0Gu_nUMYf-B2`*?eDm*5k#;7`=6OprWEi$YysR;T0&a z@xXj#;g;hwD2&kWk7#~>>zB!OeRVw0B7~K+QL{(A@jEf=p zB}cH9t?MmV*TFHp6ej%xd|L~$j!STQo zAMD0E5?m0u^a82K?UFcZpw4A%lnB}m)4>=_Z5SZhbTw%0?}78Izn1(Lr4(+&&)i@e z>hfjr>nXhfe-BvuZ_;epm|QP1o%BVUS5%7gxHjO67>XT9xku!gG=I?)yuF4}V*FNYzHj0{(UQhtYn*IcFI5CzOzU--T924Zqxhw{qHt6h@S@ zT90u+qZ4|fow6|IJ&Vq`@~w+8VFM*|AgGfi2?Qd3Hrl~S!FTgHptg;vGpW>_{Y`G{ zje-ArfwHTyM94*gC6$*c-t%Oh>j%PR{B zaNAFUbNdmnTp-=xL#(hgx@@sW_m<5_Nr(H{ca&D6e}Y7RxBL9lqxpN3!wl?6EV(@@ z9@Y<=^usawn`X4Rk&_`h#f8^18~u=<1|8;nChv@o$)lzGbVn~p9bvbzHZWzwMeZ8Y z3}L{6bF_Tr8+7XDUQ10-}1BM_NU z=mEVoie~ssv_H(;_=TU`%dL3-MZ$&!m;|*0Eo(?8YeX#TmIFP7#RDA5&(n6j%`tl) z?SQ_1_8oEwSjOz^`bifZ><|^y*H#N;N2Z|{u$|CO*M4ZV*oyqgM~fP|iVfx+tl&;M z@I=o|OEfb?A8sS8sym85a|z0Jnhj?Wwtap$Z{qpI(4T(c$NqVUU3MI$eUa0w2x^M6 z&2y4}sx8TNgqa%|hs3oQ7E6Rmr`RnW%-i+WL;=$z^;(tN`_FD=at9J zTb(XZFH+SB`Nhs|ERQrw1%D%{Il9W zJ&77`RhbI0OrMfFDIixrPeUcwrXb%jATmCY*0Cyz&}4R~bPQs`+w5zAk`-x8IBrz675*|51^7yJ=C)N9^) zfE24xoHg@p&XM%VpF+JI@9A_)NvPP zmRX#jgX2bGe-^V0y04q*?d>^FD_`51-t n$;Y3{^1plaXkD)VxH&kDEm!nCi2pfvkG)>ywbrwE1wG+^R?A4# literal 0 HcmV?d00001 -- 2.39.5