From f9d649fc42d295fc80d79e858df003926ed1789a Mon Sep 17 00:00:00 2001 From: kenstir Date: Wed, 18 Nov 2015 12:12:48 -0500 Subject: [PATCH] These are the OpenSRF sources exactly as I got them, checked into the Evergreen app as Open-ILS/src/Android/libs/org.opensrf2_serialized_reg.jar I don't know the exact history of this copy but it is clear that the registry was made serializeable. For now I want these sources inside the app build so I can debug them. --- .../opensrf/libs/java_memcached-release_2.0.1.jar | Bin 0 -> 60597 bytes .../src/Android/opensrf/libs/json-20090211.jar | Bin 0 -> 45944 bytes .../Android/opensrf/org/opensrf/ClientSession.java | 175 ++++++++++++ .../src/Android/opensrf/org/opensrf/Message.java | 110 ++++++++ .../src/Android/opensrf/org/opensrf/Method.java | 78 ++++++ .../opensrf/org/opensrf/MethodException.java | 14 + .../Android/opensrf/org/opensrf/MultiSession.java | 123 +++++++++ .../src/Android/opensrf/org/opensrf/Request.java | 138 ++++++++++ .../src/Android/opensrf/org/opensrf/Result.java | 106 ++++++++ .../Android/opensrf/org/opensrf/ServerSession.java | 8 + .../src/Android/opensrf/org/opensrf/Session.java | 180 +++++++++++++ .../opensrf/org/opensrf/SessionException.java | 13 + .../src/Android/opensrf/org/opensrf/Stack.java | 105 ++++++++ .../src/Android/opensrf/org/opensrf/Status.java | 63 +++++ Open-ILS/src/Android/opensrf/org/opensrf/Sys.java | 86 ++++++ .../org/opensrf/net/http/GatewayRequest.java | 133 ++++++++++ .../org/opensrf/net/http/HttpConnection.java | 97 +++++++ .../opensrf/org/opensrf/net/http/HttpRequest.java | 70 +++++ .../org/opensrf/net/http/HttpRequestHandler.java | 25 ++ .../org/opensrf/net/xmpp/XMPPException.java | 10 + .../opensrf/org/opensrf/net/xmpp/XMPPMessage.java | 101 +++++++ .../opensrf/org/opensrf/net/xmpp/XMPPReader.java | 293 +++++++++++++++++++++ .../opensrf/org/opensrf/net/xmpp/XMPPSession.java | 263 ++++++++++++++++++ .../opensrf/org/opensrf/test/MathBench.java | 79 ++++++ .../opensrf/org/opensrf/test/TestCache.java | 24 ++ .../opensrf/org/opensrf/test/TestClient.java | 80 ++++++ .../opensrf/org/opensrf/test/TestConfig.java | 16 ++ .../Android/opensrf/org/opensrf/test/TestJSON.java | 51 ++++ .../Android/opensrf/org/opensrf/test/TestLog.java | 15 ++ .../opensrf/org/opensrf/test/TestMultiSession.java | 26 ++ .../opensrf/org/opensrf/test/TestSettings.java | 14 + .../opensrf/org/opensrf/test/TestThread.java | 68 +++++ .../opensrf/org/opensrf/test/TestXMLFlattener.java | 11 + .../org/opensrf/test/TestXMLTransformer.java | 22 ++ .../Android/opensrf/org/opensrf/test/TestXMPP.java | 63 +++++ .../Android/opensrf/org/opensrf/util/Cache.java | 38 +++ .../Android/opensrf/org/opensrf/util/Config.java | 139 ++++++++++ .../opensrf/org/opensrf/util/ConfigException.java | 14 + .../opensrf/org/opensrf/util/FileLogger.java | 44 ++++ .../opensrf/org/opensrf/util/JSONException.java | 9 + .../opensrf/org/opensrf/util/JSONReader.java | 176 +++++++++++++ .../opensrf/org/opensrf/util/JSONWriter.java | 172 ++++++++++++ .../Android/opensrf/org/opensrf/util/Logger.java | 144 ++++++++++ .../opensrf/org/opensrf/util/OSRFObject.java | 63 +++++ .../opensrf/org/opensrf/util/OSRFRegistry.java | 112 ++++++++ .../opensrf/org/opensrf/util/OSRFSerializable.java | 19 ++ .../opensrf/org/opensrf/util/SettingsClient.java | 53 ++++ .../Android/opensrf/org/opensrf/util/Utils.java | 106 ++++++++ .../opensrf/org/opensrf/util/XMLFlattener.java | 128 +++++++++ .../opensrf/org/opensrf/util/XMLTransformer.java | 59 +++++ 50 files changed, 3936 insertions(+) create mode 100644 Open-ILS/src/Android/opensrf/libs/java_memcached-release_2.0.1.jar create mode 100644 Open-ILS/src/Android/opensrf/libs/json-20090211.jar create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/ClientSession.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/Message.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/Method.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/MethodException.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/MultiSession.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/Request.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/Result.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/ServerSession.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/Session.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/SessionException.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/Stack.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/Status.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/Sys.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/net/http/GatewayRequest.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpConnection.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpRequest.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpRequestHandler.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPException.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPMessage.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPReader.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPSession.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/test/MathBench.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/test/TestCache.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/test/TestClient.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/test/TestConfig.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/test/TestJSON.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/test/TestLog.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/test/TestMultiSession.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/test/TestSettings.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/test/TestThread.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMLFlattener.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMLTransformer.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMPP.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/util/Cache.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/util/Config.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/util/ConfigException.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/util/FileLogger.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/util/JSONException.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/util/JSONReader.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/util/JSONWriter.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/util/Logger.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFObject.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFRegistry.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFSerializable.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/util/SettingsClient.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/util/Utils.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/util/XMLFlattener.java create mode 100644 Open-ILS/src/Android/opensrf/org/opensrf/util/XMLTransformer.java diff --git a/Open-ILS/src/Android/opensrf/libs/java_memcached-release_2.0.1.jar b/Open-ILS/src/Android/opensrf/libs/java_memcached-release_2.0.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..9f62013fe4534b2bbcaf1efae9d884629338815a GIT binary patch literal 60597 zcmb5VbC74jk~ZAa)3$Bfwr$(CjcIrP+O}=mw(V)#=Dc(F-raro-MC-GcOt6J`6DwT zD=SYtPiAE)NdE+Z0{X`z)8xSMA0PjkAb~)DWJOg3X(i>v=)Wg`fE4~E3IQbjSG0q2 z0Z;d@sMX(r@^}1~sH~uzq?o9(3Z1OjgY49#j5ICXJghV=)y&jvqY}ds^Y0_4DPbf> z8VQ*h2~{A#57OyJ8PE2tNF`)xC1savs_H%=usg7k4oR>QD6IZsPpF>sanZNo4a|R|;bQ9S@?UEI^q^5q!p9pJ5YRXb5D??PQ4{iXF%@z( z6LfMi^pv!7aCK33aWXZur8BlMbaqZw({;dJM*UXnyqC(1P7LShuLr?KLef+!SRtfb ztCz9?b|pSQ5DqlVrL)U8ptv4>S9H>08J2c%a0wP(tKGNB+={G8*On+-qtUWH63ofd-ByDYrrwcQsGIY+H$LI7@_Fs zmF4K;1)gq^8nNzd7(pI)J6p1mES1EmR+z=`W7l@pM9LtgiSruAb>^_S*t8W;h~xni zCWT~NVz-Pa2PY?q*?4{l0TN0EPJsma6f)|lE>T1{EfFqK3z;`|X{w}aip$2#SGz&VY8x?S;+<=4Ih=~k zBP7N~1IhO`?H9mGP|KLAPj=D`I-Q22HACsXC@z1$DDKA`eW)X@rAbOukg>L>sV-We zZm2GEmDF-BDmUHPsi@Sb{9Qs!>K!0P{VpRW*HB}4*0n2!J@^GNV<$|4itCNO5ii^4VlEZEyi7`>( zWLJibN?N5K%Oe#Q=gv7-+O*LSiX_qFy12bl9nY_9_~qR`%?S{9_}Tz_LkSjQ^W=BmHF!)-Ki+=MAsrw%;YZTM;rxI;mQf~|iy)m@N58=@XX-zV9ts&pRE?Rtbo(J0D z6z!F383ymcLf+80>h+Q{roe;M>gfE_KESlhVKwZciuRe|wz;EM_{#IVVYvRbf;Zyi zql`J8StL9IwT> zHr9@7kd?q~LJ|=Afq**!>w*BGNI^|-n{UUq)%i%`6RjscD?-B;E5e7nbc}NyzRd}K zki>&wk9M)+VnfdASnq-knjh16>ptuTJt$46$NooZcJKOVi}n2mR41ZIZ35Ki$mVIy{$8wj1OR!y@`0&AkdQ)PWy`y2yH`adw`#*6snYm#U z9~cNo;4kho{Tmz=wzqRJ^>C3lvNAPx`QJdQENj0gfZ%&k#OjCx{3VwkC!~EZ3eyeN zCZZxFrJSFK*OnLESVY&t?!>0}rY^cKjnhVSSBPNDEw$YwxL?QJ$^CFVow@z>buuas z^s&aCci;yWNfBCU`xE|lt`E-5_43SPD9@XA+?&Wi!#?|-u~p~#rMhG?Y;bMfK*Q(w zHO8yoAbB4{T&wo8U&2*|plvnnCk;MxQDy@<#y#yQ`c?cYYCral>zPS8sqHo1Lm%Wz z;$Rb(V<^m13N3G?_*k=<&i_`+MMa*oSm#wWzH#ee17f<7` z0Otb!ImH&XL|62EmCVHS2#ou8fZ>=cN-ICky&#FAc*yzb zn4&@CPx7QLR(UnvDXRg2eX3T-;)q`UV`zT%5w z@g30fxQ4O7;=VE-k<7G~(yj#Je)Nb9j$fIO_~M3k2dBEvAy&?@Tun++ZaINCz4Fo1 zvkJ(E-7rRD4-2Y8o1V3g6U|1M-;r{E!shc<7OUnr1X8LbJ6BDVO&XKW+3TdPpqu0s z6=IGv7qz-qSu( z%@A^6hJhK=;IMM**PxhGY7)_IbP)b8c+<-iQiefW-pA(J+TI>KzZZTWF$dg7oGqmH z2!;B?@|E=GNQ)_xOE6Of+J$aEN2Ok|-lZlC_6Y4u!9!upN^Mb}~2+~-5 zG?3N-AH&E;YXbSHzEc(2NO3+Zgx(qA%I=`9mpUWfMw7u{!Qt%jE3!9xFtIe)d}I@C z+&h}&XRTfiZ(5^w##(_k?w5$gtZG-9i!kaKnMeT(0(`kdhX68zC!}hnY@yAg?}{b4 z^owIF$W=GH-^cL((`95(>E82yT}Ai<2#E6ExJ<^<&h&r%n7VTPebqQ^CP7-!MMxaA*7b{h(ohV}!XA zIbE)LSmGj+DdgBbD^v?{({8r2x@egV{=VP5?Ws@xfMQI5SFWo`)s(UjzvCvnMGBUC zyh9>)>aBrm-b<$KDoK^w`ko8NyKuUOgUo^?Y%?xFD zE43+3xJ0UyCTRLyi7P(+1az8_D=W_VT&cYC|EWg0bWyN}eYeF{t3c z5~78ZaHRPa3^0L!_1uOcAniRM>1tDAdgK<iwZ=I;!xS!w71kpox_VC)_WZ2~u?Ag}2IB!uO{49|wvU3|6R z@rxB?GjRj}PfC;eI8`}B5QC-!)VrJqN8JFaCy0fn`;JJWsV_wOVljp`x>PVSYadoD zCSVi8S=*!-X^AU=*qe2<-?Akmdu{9+5@*ZBCj*E(suRxyym|H-rVd09&gD%P9SbLu zqAMrV>t%rF>#!nOZ3gsU=?_q0=?#-ZX-`+`eqgg!^;cpMY1Hh~!rC8Hg}UF?^kv;2 zjv3%$&Ft}_-dMi^;X_T)99ucB$V5r(#@;fP)jctv*p6k(hxb@oCWgv8>J5X! zqR;2=CxlM6734#2vOY@GYXkf!!HU*`u7DTG25c9Zjcr;5wm|?pdxc_KWVwkoc;ykL zG}`RdQ*Z|=^90ASBBsorI3fY0#crh;r+hiZ9~ns3f0zY++U{ zUk#9-^VrE`cO|GEwM<{uX;?`Sr1`Df8z*mv&{z)<`4-wskZVbk|7{eoBhy_h0IzIM zJGWUzPdk@NNJ5_b4kfNX&c6d^J}rIV<7xV8TLe!Jb);owZTWJijhW+(y&rmjp3L>q zuW^;ce2=FQ_H=I^R=F%^MrfanO}>9BAo{8vej#e8#%0*l6@)FeH@$g@$0uh0#?8X* z1Kv~jhwXE3Wm93`0fMqhA-j(?jIh50#S8{X9JbQRK(|pNQDz|t{?IV{s*WHlS>(zY z$-OPYwe@{}NUD9vdV5Hb%+7q@Ri>-wS5bL{_A8%j#~G4ccJ$t^LVY7`*hKp-(Ht8) zc=Hwd+v253g*lu7cKW#(Ym0I$YSPMSi)ziiZP{;sOx>yTK{c(P2(b#a_gzz4S7v;( zjYzvxh@BAvuXKTYJ|Ur>T!(}BKIXNC$G$4C@lex_R^5?S{Ld|N1e4BnvGJiAfBdFK z1>vp;pVjGq8zi%PP~++h^rHsfARZ&D|73r~>FA@Y*U(gZB(u6MbUTGR=Ma>FTRgv@ zye3{sB=6t*uOHtu6QrZvtZe^UOzBrum1*CAI%Ta@b!c^Y7qCR#?c?(C(icMmKI&?k^gTm?H|76Hw*t`*-&Z zlj-paA{#{v2}hH8Xib=C(`YZE71SR0gWu*yb#6BQ;(eU7Ki+(EJNY*hE z+D4*al*WNE%?N;8(!NUlfXr5A&dW-dLwd{FE-5j8OizVSHeNP)0KpVnKs9E%ZAW#I zBiL=&ls_AbGvl>GVIrWAGM$V$;{)7eAq>7P0M%!QkozIy~D z+RrxUoA#3|?-_Tzm-`w2-8N_g@NdOFLzeJtiSoL@TVhg5VXXw=Qu8)Tq>Even)hAW}&KR7SWmGfMSl|r|3 zmKH6EX=4j@=i73mPUrEAn{;ENG49GneU=&$mUogmme#`M=8FPpA4;be7HWd`n!Kf* zPBPEns73vglyx|W%{n?Hhpiu*TPtff7Y;(J2|6zK+BLlwSd`>ktBWTf6U5`!DJ0gJ^2cFd-aW@2G5AHjS{B*jIWfV1($4%>+* z8b*R{H3Xa1aLFmg>ZHJ9@Wu`=JvFgDwi)xNrb2^3fEg9VrTgPrJD%?p>!57kY8o&N z{>FkQp;?rqD+>*MO4CBqB7RB_r5If_#%fIuPU#d&NvSC|?-JYIJ#)!-{uIL~V`N4W zofz5Gn)uNGyGetD2c2;?0}eepYEqcBy2Qd~65|e8GH61CSZS`tGlouGXu%G_McAQoOfGStcp`V!&h3jIXILLD^zoCAa{RZs06cw5DO7?xi! zbCV0G4W##m0}hfLVU6`q5VWHRzPC^7r_gc}Ih0M_!&Qxh6gwYA}F3xZ>B|FhD0}TA9kPdXN3JI&^pM#aWInraP41>4{88x@6qrMgQI75qe=`J&`CSGEmXQY1UrlJ)TDe3=4f(oMlE ziF0FPF+gJIZFjS)efu;!TJo~bPy%WOdMeiX-^}sW!1=@EPKlJpY8?v0lPz4J?b!vJ z|Dc6$Bm*kfPP?2L1i76$B#qXFfO?19bdpmvk*znjERc2T(2%)bx{Y=mclF%ib*Ui? zOR|INE{k8INVhJG$xyI zqZ^JtRYI02UX9*Qe@r~^t;!v?F8TwqU8KJ9d1ug)YqR?dRpu8>Q~_H-X)JajSFI`g z)FoOM6z;ip?xLf-&X*TYkI?h2S-FnS>jF#F)Ue6=mF$gfgT78%Mo3%Wbk>&OkJdoa%7(e~Mv6o7$Yo6w(eqM897_sKW^u~)kmf~-=(tK7 ztP3Lcx@%jT_MrQIPTULhv$hCWdnq!1G0Xd!_rp@1>r za*95%{Cp{t0#%fJU6g|&ivC0+S{OyZ$uJjR#N=C*ZtN%pb^%;Rgxw?Xwambwv$-$F z#I|u(ZJ2%N;V-}e)w^nz!X3x0eNVHz6T86a#M`KLcwulYz)5K|l7;gRl-6-yg%;3? znAh0VzNp9iU|&_`)*6c|susJc6q`AsmfUy>YLbcKQGBbOXrA{3>oE|quADgi9;hGh zsg|P1DOEl|mI5sk5mmcRU1jgr8X$;B=CvhXcqvbNY0ps+UfgEXnFt_V0GT=*gASkr)rAyJf8RYit^ znOi^ot@O`Eu>JA*ly*EIH6IKjV#>gCrCA4w)tEJYO_t=DAwDBQ44P`#PbE_A5&?XU z1`a7&7z4o;110~W>zib3`#6Z!L0vE5+767BjmEw6qZ&Hd$KJUtDuR34eb4 zPoD9+1Q2NXmt&CpWq-{7e|g5g>ROtxJ}S$}e{$K8ce2w{;enB1h+wc-f@2AU>I93B z6p)b>@`%co4euqu$e10?fJ@vq?V8W8TeO-xpgZ@q@-;hK=xAXGXpJt!o1Z7I)IRd9 zt!=MbuH8DXYbuF8`0dRGNI=3f^gV96yl4ONjX%6ESDVF2b9>mko%TA$V2XTRZ6>!cfLXR zAd1oWp|`|5r9ppihGOsN+5C`(DA&wyyL$BJM&)7sN^Wv54$vo_3#|Mp!-4dauKrXk zO$vS@^(x`kCiQ<{x?3akj#N&3VEcZqj96RE-2}4S@51=iN`DFR>%lt&_`~n<-=@($ ziDUo49P;^;yn3g{w#WEvJ^#i>eboC@<^qNuENLE9;*HLpH&Jh@AH1@R80pxaDO8arx+ZH(qK!NP zMc3oR3g{5ryokw|P#rPpJV@{0B$$ za$l@0yg-8U^r@1i%+ zE(H&2aomdv9h`&BtIh0+a3r=tgO%%4QLT|u@8DGot*FstW(j=vj#O0P58OP#?Yw@| zX+L|d8U+^voy9)yhnU1?ZYZM9nX^#6d%F?!8GesexgJlhoe(q5q}Xe>nk=7sj6yJT z$FAXH)s$_x6= z1}!C3j*kMDBaxnvwRc-YWEw&4@IBW~q=m-eROdUCXhi6Ez|kc6OI2c!69Sb}MwsgZ z_h4cq>MQ7+4t+GMV&NArB}^pVZ^`H#Od^^%8dOp^od^w+BM*&O3zk#Z#OdPQ#JP&L zQ;uS);-qogWz@JDq|#c+b2;j8G^;xw;XUFwJJ%3A&|JjQ*on0Z99h{K0tywOY-7wR z9}qVJvR@O3Qzx;sYaC3fJNS%?WNAS5LC;*8dRf;uQ%R}pn#8O1PsWm+R2`v1OH4Bs zk*+Sz$-cMkNTj~(;EjJVH%-wxiN00>>0XVAn_^b(zjk?XTFeT zpGOQWaiN}+smG^>;IxZrc>uZ+%d>)h6?v5(Yr!u40tbx*0xy7J^MQruYS1h9SD_+n z#5o&M$3er%8KWfUbmno2aiR1OgKwg8Ez(9!rkx*i*1%)mXc&(Ve&nEWr0hRSr@gbO z()JwBH)mZKJRWAQLik%sn{bUOA{FMots~Y%#W}bWlQ?AjeHRUjS0q86zYI#bMXn6e zxU(3KXRTUSn$7fpViC4~+5E4^xSunF-(y0l!9o)q^^MCc}(=b|eMr1?>#R(~PY5L&}grGYr( zicDT!C$r|gh3yD8kzP~=Avm*rc-+4;`^_7Q*<0R}br=a5;DE2z{e&tknW5WpzGYRcoGPr~RzKzU zoQEOYar!8&UkX~hjNnQWmQx3d&KD(C zla3fT!zejLIC#jy%70Q=!zIA6fm;WGXA1R@kzWQ2>kkcW7_6ml>gZ{IlgQY5!jyHh$3YSDGf_|xKY{p#BY-RPUrYQ#6!c>JBzU8c?Ia5&|g*3w7!nlP9S4a^D! zchpi?T0(OhaOsL-WAC|nf3exIx?u<#M{h{leZ%gf>{EK6gXf6n znjRGi5AcMJiAOhX6$*N3*-*xPjS2;XHNig zylDgMKBJ5%{-Og!tT7W?qElVcBbv^vOIyOi*DJ^0rXBP<^H-1uJ@zC12>Cmd+CwV; z8Afq5V^F^8N(oePyCK=b%pq5PVJd=IG@81j=0H$E&l-aO+F5iOhWo}q&>z;0f&&`r zXe2@_4S~V+8&F6;Dhmk^lLloD*el^K`U7o>pF{*(HoV#hv@Ngb#?&;up&N{SUDG7i zwaX4*n~ts;^^6ejFB|m;W6g7)u+ivd`Dl@bm4wdKK0%=)l>8%l7lee*8(*_hsFhkB zr{Ht#A??VqOka6Ius9OLAK{4NM(^Z^a`g{t2C!Q9pbtAUKk4)L7|cFwx12Gq*uFPL zcBkx`>s5C|+oNrXkarFz?Dwk%F>YS)N7jW_S{bGrc17Omv!pq0;n3DzH;qeTEcTl3 z+;x8IVQ;=Y20i+UGk~KGjy&+QedA#m;$Eb=DwueHpKP?{s`=c}W>uu{|3~hv<)s+U+n*9q7|YoS9n9B2{qBpR}zmW*E3FxW~JzB zk0)C`0#OOwMzk#8hV*CIVWlZ`lph_h)wKx`ugdJ zqiv8`t0%c4kMZR1Ox=7*b#6*SE!3|}E`IP{@Wm|NbQnw53>z&fc&d05HG6M%uYpxuFl|}{ zSew23jujL)*;jru5voIeuih(V%$p5jf)`Xlh{pqd zk6Oe4M|na0j=_~e0rfYertLwl%_UW)IvYrREi4gR_SXBKes@%ai#&ho)Y+|Q)kY_c z?L+vF*23OX8{>#Z{QO44ULv>l0`UMtzS&2>MM1%36Vf^>vGiOpb(h`r?zoxls$yG4 zTBM;~C}Xck`x`1*nmWdL?+D8S6&-W$FzPX@&@=azgX{WH*`A+U9yof)714}smzwkYy6jEQwrQXs9hgX}|`ntkmcJKCI) zWSiEfKQFapqocRFTfh4L{QU3C{U1j+8%6;~pnsc&mz@87N5%hhLh#Rgy_y%&AZdA? z`Nnq@j}rp2PKVB2Vt~*oR2UKj0xXc^*ROEt1_72-PJ||{YIj<+N>#**0Pupmyo6s0 zJdrC_%{#3vJKzNx@Cb*wp1ZA?_UxGmXZ8m#-Ogq^U7pU}_7{!4JPp`Dq2tP3b^0E> zkGFS&v#}(8B;NT#MGp2<>fE=)(fGJ;@iVb7xJ&%h0oK_x^Xa>~G>Er=ngZn_a<}`MMjZTNx5h;wSnRP4Ll)9ehBED2d@D!?G>kh)$TW@x zq;f_{$nC_lNQtk$T_MzIGvjDE_Isc(#Jfn!G>pmRIc5>S$};9!a?b!E#z7=zTE(HR zj3d<2@O2%o9CB3cf*~Ylk2xK`Ddyqwlr;MY&=QT)kT;@Xlu}Q_e2t;l#dgzr(wRDu zF(hdZ!{RL!Ge8~sgoi@9#zd@XiiTZ;lXRA4M5B!29%^#I1oR2OYDDXAAkr#YM{q&I zN-t>_^dnKzIL45wX&EI`&K%{FKLF1*4%t5v&+wOUvDRKSHuyr7dY&hJFd(q;cdH)|%bw z8boq;I?yd?+k}P>hy83DMZ!*Ze#zM9leTFXGK1z#Y1>7DzeQ@fHLjHBp}=bx8Z22m z4%w#BjFu5cZnbZ{rSrR~8zTPP87H=YcJtiByi@nPJU$kuy-Op-dx;<)lJMIai*44s zEAjv+eoiBKYn0`-3^9Lxcar9q_IwW_&1za`?t9`r$xC+`M4-37())FpM5Mlb0Z8?Z zUaV6*wuLMs*lF&_8N*H=(Qt;tqFtf+)uB^(2sn?eK(oNt&43$f-e9PJS< zF?~)EhAdB}gjkOfojQ}F&5VmnQ1dW15wR2zsWjV2IQrUpE;F6WWT@z>s^eEwcJedyll(RTu{Kj2Pb%PKF1J8QMVUTkgKrj{C^$7CBFp`F z`dn{}G(#>#kkviFnZWBoxyBAx*NKs}Jp6s-`jw4zPXjOS+X zXO3D;FG5F}$rYC*S{PI;vRG~mts|2suL&zh=*xLKj$Z<^Vgy3q8e=^i%cL8{@<*Uu zxyVw|WM~Z7qO-N4%2Q`d+cw2fY>kUL-$QxA4m4MdBQH0wSV_eSt1574?ltH{8q>*S zO;?vMo#!@i477cY%LFLcHerGyLqnsj*`hTshg3{lOwPjQ;^>Jwg+L>4EiI*}D{TU9 zhq$zjAS5kaO=WG{YUp~Ynu-ERN`#B@O*nZ|)zy>c56_x(P4|lPLjmuf6$|G~tg4me zH8lbdXbVwHgvWeI&BQko0I1H0>_gNYg3 z765)7ZEam{S`}5b{-56DA_+HG!p>2SM@@&2B(3Q`Pmp{QD<&a~r3^h)Jc{X9AE%Jqdr<`;?p_YSJOdlO?eo}U<&t5a$zL)iyf7d-Fh z9`94exjAVX(Ql#yS~~btyw+l6V)cP6Y46;*!G&}>{NvsFluL2=jf`JRo8sKFG%@iK zw9Y@=iGorV|fHYkn>wlh>Zru%(V!r7vQ3oK)Rbd?p{cKDk!gVB$hc8cTRg z45044*J&w68oEgf`+5$?jpx7DWtau4uOzK5Of9|Y1qdY^iU|34 z7It}!WmR;9W@MaiygO(H!@f~V53)WTEvp8LR}!q3peCs=G81!E=8f@=Y2j69+vSGD zK!y`gxVmV_-f?CEyV@Xv;T$QvG)J6Y&D%8XuV$?(==I-A`jxzYWZ_6T(VxXy zl=u?!*fLjGjHmA{cnzcbhS<(8Xt@y|wm{1S4l^Gr_8K`|>dEq#q*uog3Y!jzG_vU^ zBT^NI&{<)>{t2}Eaexdv%~a^M%|x84vKYZhN2Talc|M42Z7p00z&f|aI*0V+P#Hf+ zxogXVK50K<6)vHpNJHhXYf4ZTpQSPduTpp{yO5?9np}Iz$0eiB@bOai5v1e#W5}f4 z@P58=TXYUqki#NZ3gVMK15G69658Y!tSW8T?0m0$fF0}Qv|OgfHrE-&8ZRs)WGZaN z(&(EJ5C*uFQZgVx6>SDpEv~Nd;mZ9}p%fet(V-ncYyI}P-bWsW$*~UnM5W9cY3=4{ z(|K5@Dl&7xwJAosJ zr?JBi3YH?0Q+oMQ14gb)aa^|+zP74$JbYLLyP+vV<#|BFuV=(O^wCwc%}VpjiLVw` zdfZJVvrSdh5tph4 zfMz?Dj8FE2e84%l2XcKoP(t4Et)W7}SJ*b~YGS^$DUo2s;%d7KjV0SJN_kUuGws>l z8+xB5g=*i$Qj|l*m8sJ~tE{Dko?CQ}$}7Uj3&X_0f+OO#HlJa|J70Hr6cG)dy87xk zv@~$bM%+q_{JD}d#j!GiqlO|@i~i(CbqLcPLb&22wd@`0dTrT4#X&Vc*yoi_dBMU7 zKt9%vpa3wdr8Te6>SH7t0=P%3Q$ zUil7nKA7wpFXbp|A&DC%sH2FJw$*3VYiuScNCd>P1;+xZ3>wDLRR?lNn!!su-}l@^ zqCkp+svx62Nv1b1cjelvRLn_BlRCJ;NiPK~e;? z0JyP^Wk}-pEt)#}Qc7rPQspZL+@1m0(Wv{aE`Lrq`5Pnp=sP21;+F%4-`#=8)Ppf0 z|495r^nqg_zI@-FA5MR4{3k|8p=94lk_;xxMyxT%wkg**q4MQZWbH5?|4zuu>z#{^ zkX2blegRQgLF?~@^9_P_lzyxkw-nHe${@F;Y|c59-Phq(qh5Uef!J}27IGlAVRyef z{X1TvmtsP#Dqsvn>VDNB5V4iWz3<=X{D9okLo$eXL@$+j9;8Hv?!df9u{f|4^*Z`K zp7hYUcGQnNwb@9B_u(IoachDhxx<7txX+RLLx>ysB(?g$c=yst1N5Zz>Q?*V`sWJ- zs4234*0Knk0+*%JFSiIpm->sFmy`RIc6xqu=64uSoR%^1+Y#zXB0pRNA&bq4vNI4)qF=>{hF)h<`-v$bn#HXdG zBVOM(7_1>48Srvu^@DLi^Yoh62OVbJzaI%YflaCh2Tj^uF?rk?^j=adelZEV0md9{ z*5s_*q>@q@8-tdmXGBi1*5{oH_O>`Syqhr377rzQZHKqBhv?VW={xHLz-|$;QC2No z>NrVU)Ja(ErAbzCnsi_CWC`JG%W28?b%ZSf+I_SsA2J#zdcFgzpmCVo_MQ46KMdYDAYkz|mpM?y5+3y4w&VU`?*i5e>&xTXjx zCfTaAQKWaisZK=|Ioud#jb;sC?&zW_V9qEtZEq5uGD3!Z1_y_Q+L)(McaBw*EGsf_ zvgsL)W&f>Z)W+3W1%hGgTtn(?0OM}EeVOWeE7p5CX_e>yP+kVPoDgc5R}V&++AkCAs%`rq4!3qrc> z9h!m=es$iZ3T^PNFzy+uiVu)*6|du*=hw2Qsbi{{m^H+^Lm|Hq6V2IV2y0*yIYQML zVys**gNB%ax9eOeTa(ohkHaZ3rl1~gc|%2ZNAGFfesltP(-2~So@vOH!a zdT~g{n=ejZiS3m8Gg5WBf6w3RqKuWTM|k@$Ga-a_d@SvB4h%KjTV}4bSRcI5`SwOnZb_AWB<*UaiWikM?X;n1azy140T*u)t`=xl zmiZk=RNV21imWCvr@=X@ddh`KYZ`92Ba(ykqtZ^7Ka*iZ;txF}sj$a;5_lAIf*Hzi z5yf{;N5Q*JkiVGZTjB~Jc#Jnse%N}X=D^Z(Z6|*UOm`wRP36(!zA^pVO zRi~{KUK|l)SiyBk4n75iC_Vj@kD4H2D#G!Us3eCU{WJ+mi613_s+&400BizRO3hkV z;|+0-F&V8KDvJGR&wsDL1jYvk;`a`naBBAbVE(!Ex(R9bRg3@kYE{BHREjg;7Yo1N zmDS;zKI`@1&6vwtG$7g(HkozW@0YC#vET1F0}&M*SF4XYo^;V-b6`D0kqrdaA$s#M zp_OK4;*8{Gk2=kHD(qDozKrdLM(ly+%0M|LldbvtLKKMVZ$vsBuR|cE?`Zs3w?% zX<+dV#87e>LtIwk`S#>42uJDkkG9I9vFKBsg<FaKo?71s6kJvu9MvjR+O z$ViafF*QDtF)0C~fis>pk`_kjeLf3|ii6@pD^oACeIC*uCcD9jF+QQM+Er5_o>csmEO4-f2P$QBx6!xju=#*j zdz7u#T`IPg8w~TfSR^emRC!-pVUL9Ed_<#!^`$gedNcbDb)j0(kT6BgyQJ0iBc@Cz z({9e$Grdf!qT=Eq*7dDM{u@ab(eiuGb0jJiZ?!fzKu5b&+Z^yQNZ80-9MMk=Z1X6GF-_x)7Osll_$37(! z71`R=i0RP#O6%2nULxXUvNf>kYPAsJ_j;;(?}W}~=oP^Xm9>RFP4g?hNYqRFW1@;; zX8Xi(MGdfs_$+tih*#`NmW-ty09%-mKk#hnoG8#uYi`p>zBWp69uat2MniWJHayy$ zy#ac4_*6gy&|N+u0>w3XN-g1^o>W^vx{7V*H;TF)=eY{H&daf-9oH4H1s!`8tBTt$ z%h+Xh?dSH2+XofwirW_zH^m*Zs(&)h{nUN&mlGM%SNnU)y-K-m;AXgCxy43|xj+~O zRc}O|W@^09E5LQ0YE*8HE8I)H;MKdymt&omgH&#gDqc#w@YTENmp|IizsmXgRA-Cc z1=M*X&(~|b;FiJL&x6YOhE(yTo(^=M3?FYGIdqc6Id0--yisD*ZWwuzcG@}Ax<@&m z?l=Uy<%beabtM?cjqrHM6>Y2Dov`{6mqEb3tnE~V$rqV1Un`H{1%qPy0%I9-t^B+h zaFUw@q|~&o*e+ueUKmxS z{Zi7KKxC=uJbw0JF`EKR$x4drnIb>y&`({2y4JvzPEA|}{N}Rdh7@ohj2S0MISy>B z97DS~kH}_t1`g9SKmw`AW{9ZdBkVpw%nxkkSRY0vt3Cb?VecHASrnyv$F^kZ7-1BIGQ6-PhIeX2jHIp? z2ne(%u6#^Qrc+=!)8mfzf0vUVNzQHa@rqlv)6#b!4B@y%`VA5itJcM9i?y{xrguRN zIrl?%z(%TXIUY@uHzbsj3>b?95k>t2tT}_IAM*;m4A3ENY~7ZX&N+qaywM*gox=M% z---kmsEgdq(I4fnlpTNInTqgPXmN>Ykpqf+K?ND5d)q?lb8IF|#stHO#oZHHkHDqK z5%l3VB(xw_VL2iyD-kQBa=5~xl12;^x6-(#vKP4*;};ik@lDF?2XL%IF0ZaHC(5Ou z&L15@)ZG(s>Tzcc@a>gP4vtgZiE2Fh!%|ka_0eK*rAO2=2!*gjuMQyXPR~ zlY+DlXHos9IQKqa8i=8bu7;5&KlHOu1y82Nv^4 z;0yl;l=JAFi{MuXK0taqT=)RoCU?8sqXJyQk~U*8hd7k;V;PQby#EJL2Sz5S{hQgP zVMXoMYV^MPBx(R1BlQiLModyj8K+iC{Vxh1M2q?A#+*mU!aV~S#R4m{S<(p>;PsDv zeh6Kv)k9JoE3+NaEX7lbD-wih_9mkQTb=|tLF6zJ`*3=hyLD+#sH{tzlpB6pe(0aa z18WfIM@qFU2Gm@XerD<=)Lao+t#bM6m_GI3eng?GIikpRkRCbHCGC(o@-u|LlJhcA zCW}_j?_TMX*W;}z`dyP5NGZD1y5VF+>>=fU53y|%;Wv0`(uPs43=UBmrAcMYDYw%4 zIyy{EcsWnK#&A(KBq&buM=J6PFIF74HPNysnX3QiEsj$|-?r*ZGOe_!o9k)iH@j)@ zXpU25TwLXUakx&xBy!n)B`9|8|G$2nV&`S>I>un3?Z(Rt<1zXRXlucSe-U|h;iT=W z1+z04`#2D-V%2C-LhxJ*fOu_%Vrw|W3m%eFgQ;&3MB#j z97D971&B5$e&#lRrEC*LauwBYU=c|7EzKS2uI#52dos+zat2$|460;u#9d6~Yh0F3 z+rm)?J?-?EC{DNz#rsaC!LE!Orgg~>1EV&u*%@8Xz zo2p<&0X)EUUs^2f+71{pFq|ExCaw#mo4jBrp|LERq?%ilc3Gps2JY+)tS>~m+EpIB zPf7=?T!k9Ua6e-_bPEJMu?zP^AZkCso!`XPdWrvr;i^G})z}Jqe$6fR>O9!oBa9PJ%Bd`e?AkgYSyoS+WMoZ1fEW2cDP!&(iZu~q$AJ6%;)&LdD|F503BMXO-KI%(P2~xDmJi)3 z8=)?XnU{q(P8AB!e*+3;hZ9dEzT{tr3s29Lq+XNxO^4u6bjL0>=i)H?$4=h!up0hF zGtFkqm=-!z*f7pxgpP(k7HLwlLZyz1T{{LIg7Yc_&?%$rO&uIU^U4uqlv)O7(??A; zHH##svY+_)#^#9-2+K01pr6q3>V8R{qJLvgNFQ@_$`>#crbXOK>Qd|_x7bL>+A5z; z_PQl*Q=i!d@fyqjthBvw`-zoYX}@DBqnr6mqeXb$aYWSve>4%w49=VwqF~n@EvGHc z?RL&wvGmEJzDR5cy0**i_qgmrtW3C^Tdtw?WcP-=g z5t$d(KW+i2Vljb#RCpd40hNs+$psHRCdY{Mr#UVX5&1N$6@0~3fm4E}c^2-+mzaQ_JJ zBlU)3xQb@^272nu_*hQ575+D40VfW9q~aAEf9RCOFYQ5(_j26c|Nswlr?Z)t8q4Qk;< zda=`{8jAH%My- zb#r^d(X6$;3)$PKeleS6HMGc(xqIH6`PL>|z<(Pwp!Oj~5GrwCw7I7OOnWBNIfr`q z;y-3QiwSoUK}~du(79MoNGn~7z!%Ron~Tz)-a%#geN zQB+P;%*9BSQ^T2YQIp>K2_z|7d|Q*;;7wdMEV@@EIy|EUemDroS=kfr63jYW*vZ4H zv7nZXC*^Fs#gSM1%X2`Va#ASuNQ3hv#}tHxjG1w&%`dEl5pCzCd)a!ih$v>V%5CJm zWzLI727q==VNd!8Zk%W#N0$(zND2>#&Qoq>&xLUGq^Bk1YZgfogx&TWZ|Rxuo&e>8 z?r3JZ>o`x_LLPm)v%R+(EO#;MSnmAIc9TYW3Zs36zx!G5{?a^9snE{@?!SB&iwD|l zW?h=*@kcKcn*UvSuUJH21f$P_(SpVX*B?6kmtJp|#??`9_Z{uifp!0JbFgIDN26nm zQOi7qW6a@XR2U7ZO<^?{Qrc(|NQPzbGYfKfb2jWLM#n{xo*@fRqf7EK97OHiTTqm& z9HM;6qxT{w4KIDT1Pb(B&#OAUsYq$Cb8%;|gNUyz_Qr3bNm73AMMZahJbVcohEv+- z8q%~cj+##R`f((+CG>b1GKh(3gG2BiO$U_z=2GhANN?gK@+WAl3DaiYRZQ_tD!MbL zn?sH0+?TCwDVSDVmYJe~!oi%S@zJe4s*Db6sKoU8Zt;<_5{aoB$)Kr_dIK0Ny-+fX z891yN2Q*qF90kHDova+)q+Q6@GguE;LkehZxV8&w1WdyrSWxQ2?;L?r8Lf9%962x! ze2zMt4?Wm2yGk)?M_7nf$OT1xh~Bnk(`YZK_(d@mBY5%9-w0NA)24}9NGF@wD!()b zPxLrz7wz_q>ad7bFHNF;gApxj4Yh3g0>Zq6da6IMjMjpn?`*M$GdIftabCEc{Mtpz z-DuD@!2_wSD;*;GNpY>59Xj1unRaWY`Ci}!l{?YeHdhmVyRr7I{riQj`T)=yuK<;9 z^xW2NmrO5ip=w8sp(ZsaslCGbQ_5-panJC#^S%$B$G2P|9TKgYZHaJ9)l7T58Fz9YPi8` zsTUf?xFWDkFgsGx24PEjGdd4Z_{!)XwhktiX7zNth)?G>=1C+M2v$$F^b$w#{XQT5J*-GB2s*6-=?pQ)fSu=g``)lp zpP2iLbr@Y%zQg}(NO0FiInp~b{fhiBFu`S7fa-M>At@*{2!FdB-6^*p-Kmfmz&ZFv z)j_`2kfI=zZXtJR0y?Vz`vOjz;BxlybMjD229W|%%|gnJe7o&n!a9^nG|s^w#GHdf^~=@(m)b>flqbH2;`2gESg7uEWUO(gY77s*N&W;HLQKYEs+gH`2D z&Ak~m+GHNPL)g1p0wTa}AoE}YDYxU}xpb*@BVmR#ylMCQT*;>>o1bH68A9LfQ^3Z) z{vZrcRBCU{H6)JO7A)tahOk?P<SNRcgFLLJOJgmf>|5TWXN0aOra364MtGMZ>6^qb^`2ZC2y?M%bY2^%pmo9y zV^=s#awCsZ9w@1Gf}d4;LtS%EIJ5KxZ@Ky0sKF0{Z6`dOzDe34{tn4yFTF2nIKX8X zW!ME9yv*oh{5*8lh5Yv_=tWmqOi_7|LJ;%)iV#2{h=_fj4?yT;XIkqvDc{A!e>?)X z2}ZA71_G#hnRiQ{haKNUw#%&#j^1DbeRAL9eH!@I1r==^TVcl+=jA4+-3 z2QngPU6fF`{q@@qTt`g-QZ*;!vf!$@AkV0;&NE|GVJt~%_`$;cYQ;?%k+KyNQlaA0%guc}={J>Tp*I7d*13f>ytn37&r@x2I5 zHI(YVLuI>=l@@RRAu3REdMyA}nzIwL4r?)hEQahFF5o9<6&@ERw^e7-V21t0*`y7W z$zv{RupO;gR}2eKY2N0JvpcvOhlf3Ya_6AS#wsCACe z_#{2A6-MZNbaWBumA=@*j97*-*GAi!SX!pL{bvK zW-&mb@KxU72H6{&3>jT*ldh-UkAW)1Y!WJC_XJN%W|mYZ-UC&J>4>qIsIP9QEpP}E z;@_u*eJJ@MQ%m&2=ytv?N4&T1!DnGY-Xn$2TjYh?(%E}}2x9GX@Ah@$I9o7Zez+jJ zFnt9g_)fHWTI+qo;dJiyX9a<+r4H_?ACez?t!Ynz0drmM>w#s^i`mNoypO>LfHr7` zgvobhlpXRMv_bDgPp?nurdkQ!Eu*UX8>RZ#b^YgzWh=CS*@!Qq;xR!GO6&rS>mo3?cnsVgI;P1xtPCCNfMGmx+%Jc+ zfmta!0SAkLR4Hnok3{IkbJgQF}D;_tVxl88HIRALqmlRjBKKTbBj(Y&Td5R1tRZZ!#OcdX-L^07s zD0jLd`^4MzVCG~+_DEFS3!HRuW8^jc76&o`v@tDZ>CwiV0r6+ITmtz!+!;L$Q67># zP?Ikf3Tx2w1#gzcmCN8qLro1fpWjb%dCId1wMkVKjb=;@?+?^H6UYeieZih|x!+-C zl4j4nxHs?JsR98gOM(T%`{b6Rx)PR_{3)iU)7E>3962voZtae)c>@^=9@Lg4pHU_> zxTI{J8zv)4AUrUB51uMzhM%1G+<&EInBUO;a!N1EMn@8cC<%kUy8lk`%l!(3-lN=0 zOH1p^P1Atm+>`!#5Jq)t(*hTq#iZa1C87o4Um)MpGe0TcIw-(hxN(_}72&63+P43y zZj$V={*8C1I}0q3RL!N%MLvf2;E4H){S%=x_T@H7Du9Uk@S#(P9L+IU{PJ??n~o5KK|cyi6KSkmVU}p zN2eL!PiJ!lYIjm)P<)F}g>*Gjr%8uxks07|{`g#=?Gct*TKl2V5ff>GfB4!u z4HOP?48xJoPxEbNB^DwS^n^OIpTHuXc^kA$+^x~{F(lVIV06dEIxQDY_h|1}sobED zJn03$lFw0T<2aJ@hsyx0bBakJ`Ywezw@%Rz0`acWc;OEeW+51J&7vc8UX?O1!v5q& zHAp_8ERu>}BGOTlWb>*!>?1gs_gN#)#dOC_`=n~8$Tj-6vt@^v7L6lybT5lKy%Lzf zz!##v28@3@&|iARa&oBBy<**WcV$&P9T!l9qND6Bi35^}N*~@DO({@$mRBP&$tjIV zKN4PaPHo6@hjE;1$W?c`ybtxC!C5vEO|IXFkwt`&Og|h=VkDUJH6Gc<7SCYE)G&tA zD0mm;Eg`tmj(Y@KtcHYd;Qf=0*_GhP7h#RS?&}S>EiE- z=UzbTIf5cr#+Y4xxI#E_L2jGMt=oD<>{P%eWgGcyJ`PS>vGq7s3|L#?U32mea$E6T zbF;IIUWXCRfK=q}u$ufGD6mq#w!9t&wQ{Besr>2e4eim?-xBrt^X{6~hsL$9r4Xrs ze{a5lDyd%1QbB51=mhcJaCqQ6e?;WNz}JKPfd3=xPm~%K3pk&L3>mAV#hn7#tCdHF z9rD8pxWCjr#WArfFA_Fi3>YpOV64@ZmxD zzcZdNwzJ{lqv*aQ zPfzYm!$9!rCl{etg?9?jMSFwH`Ef~A!ZVcFa%W6BrWTA(lK48lTEL4CZ?Kpy=t#rCI4t4{fW@V{w8riTXa0=+Imgr+UFyU&* zCGC490n&6j{A6jt-{O0HrTJfOd9|6H8PnM(-&HE=b09JN!%>vqi`h{^R-sB5c zZT6}-&KKN+bL}U(Cw`vEE|U4|6U%N7enl3(tne;$+>B!5$Tk9Y*7I=rDR|@DHk3D- zCw>e7dbWQE$(>ukR0!@aD`|NA6uogi0IiR^FsyJ^uqs%`t{wnk}`vLN885HfDLwrDe zTKQ5gTf-te?mWXRkta zzMSs;Z?4>)yBSeMa_QTTDMAT0D6V>9*!rF4)YNSONWMzHl8Q-Lf;=%Onb(kcsig;a z$F-!dps-^i!8f9iKRrHI6i9SeX?6#o0_W$|w{mUCUnH!XC65joq=R)F_trzJW@Q;R z$EF{nxV#Y8#M=*=x$(2|PR4GC>|44d4#Fc&=aEi%4*g#{;%HelO1 zE`4=ojQ&EI;a1~V8cR3pVRDhNj3tR|ZCQ)NS@W(wtX1UK&fz6DfE{rUnQ!^28`6cP zKJ7c=r67&F!X>1t=fOJFieG!1!+v@b!x)i{OgVAx9b@IH|}`Gur} z^Js=mU;^L&1B3PF4x`-mAA>y|9|VN@f1w~KnR?iIS^hVoXMzrFfW`^xcd&o&PG);< zCJG4!%8>1P$bklio;g}cF)R_Rs>G;bb9e-$d@3U3pXfT8LscNdRRT28dcAft+-RvD zBhgclUft?%yE^GQM@M`5mARhho7=6-6Kip{y_o>_K=ud1q)%hPF603RR@qV64(Y*O zI4}AR<-sJ(4+&AV!B03M6+lr~6ox>piHfL%Bzz=JnMMZy#yqLo#T?n(ANM0|PzWmN z7vR8ktiS|N77O+f-&BbsFuQ+-P@sA+&_b8@0-5@bD^Mvm{6Ht*GZ=@hN%K+;$82Pl z%3PxYgwyxej38$2H2n&gjfKYfh#j;?FtP^NLpihfqz*dc^o;I1LiJk$kbdmBlJq~T zk_4Lom>zbIJp1O?iGA~lU1Wj}eT0sEg#K-<{)wa~YotqcsNjPa;eCLR8X!Bi|InH4 zD{(&1;+z=ni*`SLtP66CW8AOXUhGONzh;PYB)hw-f_HU?@YaxIUl->hW~5)SM00r) z(%`A7zEQ-~y^Ol2EZ9-Hqn+^&Dy5REdo4SLBl43}R)&IhfM%JpiJH5$gQKE$RVBa1 zGMbV8d{CENM)={+r-LJLK||HyROTgXG(#}*njGz@)hN4?GIe*c@kMqAKedx1Zl6_l+?e$U=fM~ub0oCmvOHdXH^Qn@xU2L(#znx*%!3=QZi*pBB2 z4RO+Df`M>flqzPJ|E7|WbIrQ)=}Gt?2n4wKq$#oDBEeZ5Ojg*w^3Aj@*-)&cSqL)0WaAn6^rx}H+u`MBv@acU@IA1>DGCBvZh8*j z+{{GB0^MsX{X6_mQxuA6Q-3kGi`N&mUFXm`TE%peXmh9o%xdy<6>h@9Un}Ie9NyZ_80T z;kh@X`B}cFmJLtm|4ugbQrsnu+Zb+-mW@Lz3s_+-nk=0DWx9@^I?{4XczX7V#3vs+ z$Z^*qnj_+DHT@+*(hjs)L$GfmFskSU>+~(6Jherf$_(^O+xjT4C}>INj3U3P+_Fze zq)2tV{1CU$5Qo-c)2Tav|Hy5siu~S}AM?Y(A#;Vg;v^Ue#@!k)NsVj(aGIa7=!~0$ z3|b}lDccN@=Hbm*`=6!K9*~s|F$!qplshJ{pR)7U!%F&a9-voK!G%}A_QTk$hrx3i zgxBFQ4VL3E4WHlzMbmRQjHlz(53S(U54PcXkrW^JZ5JI-ZPy-PZC9I8ze?xQ{0UFw z%%LF**>d53h~?Z4<;TSzED9f#fhy%-)k|aJiWJSJni|y^8-rDN8S~dd#mvEB?uo7X zGXlY?Iy`AJOB?6Ae)1@O`nMD*;c;YZmOqOK;?Hm&ji+*kxMFSn@u9VDLQhnJp0%$> zz&l@+aSH)oTpLo0X^TP^5RD8l|Q$a%~8zJic>xmrNd# zFSeloM%(82O%~@EEsMDW`)>=qj!^95>T31^AxCOvLkYx19|!j?*@f^vHf1P%BvS&iG#2CS&UbEz6gs#eG8TuJU4e z9g9&uJF0TBR0;DF*!!f!ky#?n6RGFD@}0+6WDaZx9#p6f)YXwvqNV&i5U#?cNO@ul z<}8^<8euuKA(lJ|QpB#xnN$2_ZZt0t1h9ExKiEUjZCxlAB!2LR^5F@7IXGkP$G?$mp$=g-s=h~dhADDcKV<|~rZ6R<36qXZBnV!)+Z zp=Eni6j9GgKt+2Xme52TmSwA+c0jX|MWmEv>u3SXSlDc>GG!?vRAt#m75vn3@;M%6 zF8f5&Dp;s&D{>+KU`(Z0P3abypq$XgE9PWdgEB_A%Cf&Jys7`2JjXP71^KLC5u#1` zEml_b4=&30*97Bl3D`ReWRfXW!N5-+G*2HmMV}nBF9gbaUAv~wwCgjJVR)S+?2CKf zn3NB;^eauec~H7x`?SAnuEM({)6R#kJ(4-jN0>e4v?HP0Vi-`dv0$MhsGsl{#c^3o zXz;@wS|?-nB}IfBZ-)N8vnzA^By?NVso6XDMYj8fY5_??%zNAc32c}X+U=%GQJSp@ z;ig_~9NkhmjFv6`Rw*G>tte@4G106jRbC9vtkM9Y9-}PI%T;8rLsMO zGUd`T+|i>8fkabS$g>TcMaGt-?(G@b-1#S)Q1+NuB7T`p~tqnimey4koQaGkvhB zdg5YM?y@IaZ^{f(?tIATj!yD-T8>Aft-fXO`Lu>m5k z66uhoyKB0nKH2ZPF`_mzvNlu$*w?IkzX|rb%Oa0|ok3N>!Z@G8kz1ZH%-}eMuFjG~ zxnp&4ZJIGRsl!%tlb<8TSxCzzPst^JhSb85OkHu|d#_>F;_>h4T+53}dHKYmoiUv_ zMCgrZ9E@JW^3xWIaevSJ+np=N?AzLfvqHl#+%w0WU9KI?;`terTex02aUZ(JDTHE; z^K#rF{o*TJF=}4t3^lquTrzy?WFjb(beA|-8Z2~HBZVOkR$S>AS{n@YixpoSu@X2z zvm8tBhE)AYMgKhS_F)}OKmI~_UbA1mv91E?7v;IJfTw}KzwNZM>nmNovu_oT3fQdi zW+SzvCPTA8GkFNQss?pf(KS7C=TTc2i)T`$!{WS>eHUH;fgd79M%YtczCs=IZK+t< z4Bm(xPQ^KuDya;W&;uZ!*^e_8ooq!}FAO;~<(+!G-2A%rSKwVvYVOh11PkLSZ(d8R4xS6pKhDgee8et#LM)Wl3b4@~zrS(&%;Qb@G0pPfL2r z&JfY&`>1VzSU-$dsI+J)x@sDh@k%dg_p?{s+fo?RwsCin9H6}t6WHayfhh^>qx*K&exsbe!t#G zmC|QyDIq-_{U``%Y}0*&=yrp7@`}r4#ClTDc!XsrZN`w$mBL;`!MmnnnovivS9zq( z*EOS3G(xhwMZwyrbNq~?T;lF*U1cDqrtIlC6$CKWaP4L%i1RSnuhas{TbG$zG&&^o zyq3YQ_&%Z?^aB^D`yWkZOLLTtG{t##iJtxn*tY8R6c`;}R2gPhS*!nnB;f3g$c=1V zCwk(%UDls+a`xY{{iszj>SWyY>M0J^uB#P-OheaTH^+xp171DD<_Zi#9ZRD z`nlRzY??~yMdx@?$XfQW!v8feE7aa?)pEv{)U$(qGK?kOdguWmUP9-nN{qBQ} z3ZGN~M~K*)!|S8Jaq_L!b|eu<@&n<#51VA4>#9?1uWA zY`Xrv#)42Yn7za*ULCxXl35S1-*Z~Kk1`EPC2gr;J-S?>JMyPj^zB@rYkquVtzri2 zVwzEnSND?i4gZP}bA|XDCR6+pmc4aWq*~b#jT;M1F|o*({I)Up;uu@1u+*BBATzVN?S@C$yhf+hWX zhfDulOa3<-33X?4ds$@_XJ-cr*8eQuPtdSeMi<2p6ff4#kZjZ=Dp3J#3TcJ}B_@d} zPUf&3EDQfvcYe`fxPF@)i1UgZ#Bzx|$SlIKc=ttfv{Nns2IJ7|ZtZ->`#h7K{q_2O zgdKw0v$`0wh(UCLGi;8?OGu8RY$`TRQDEbfGnAHyp+E?Ak5|Dt!$N`WxQ3L0gVqUD zj9j(UOhquH+)B4MEv4&4H1ecLWuhI-SG?U=^k(0{`{P&$iXgSAr_vX_~L%LKc^ zV!z$ip_`Fl$#mIlrIohMRNt>Tg`00gqVQ)UQ6@imBGT=sobf9(w-2M(8oE3DG0vM|N037qu^=(d%LQW=6C`L)8Mh+e%{I1)t%S21MCREIq-=R=sA1 zsNo8S0J{sk;c-AGOQq@R0KqBlnA2||@OfaEsYyyfZB28+ra=a=w&h;ULOBvMVh@-s zKA)l5qHEbGVKY@SwM8cIx&R=-hK=}Gl0@pi)5m7P^ps=@u%of<7&47Sl82I9L++rT z*t^)ycC3aV+URoG0WNl;q~Xn;J^YDR3cF`Wy^|1Akk)O{x02P(c&uuD78Yi0m7Q0| zVDF5BrtK2Ro(^{;Q}(V$Ura5^{GETRz&)!F>k+!ZFs0&n)sm3i!udkc{uMt8-fzgwh^*kGYIDX|y-f+)TwGCAQ$9ASI|6_5S#a8UqV{Z)Yi#E!{(o#oyGqPZbNqrNesbRr8^62 zeu`so2t|^{Aqy-V5e!PyG>|S+bRn*h>K@KqFLoCG7EbezeHnm=?Oq?b2yL1hplb(^;ZTh9gVw-^70J!`{vpm_tPU!p&&5S&$R5Y7Mv50Xw$Q|f@kDc*FB4m5c*D1Dd4B>n zl2aiY2jR!08&*2i+O=dI%q^u!MpoP9DshPivLd z-NkVR-XN0N1yVBdAhXI8@YRpbRS#z%J~|dQ;K=|NZ$>?Ag4}NIoru8CtZ9g@bDQ#e zv|^#B)?R;`u}@kF6D4!3TMu2h~A8>cnz&u=kL!xnL#i2JxvaS)UyRog;CWst>yH zMez7F5aAv%{{F5pN_a%*3+A2H`izPOX}{>TLjgzi@$wL5o9oH8WX?u(*NE)qz}lAh z1BjL9fK6?mv&54M*yQQ+tw6gI%5(mLeE1QHnhWcQBbA7;HM*Qu_e?Oqq}ZN(2fI;! z0wf-MRh|G+>nGpvcr6_N4_%S9QBtBd=HR*;TwGM$A0i80)@MoIyL+uq!W6M z9DRj8-940!&KwA)i;}qb?Dn`~M7~*|^w*n0i~Vto*cqQ@-ZNa3ark$O!FITIWZurP zNxuFIAof2nGcLI!deZ;QX_N^6_rb=0`J(@``+q?*|I(xSo`m0DsgK{vEo)vEo%J^v zJC2+6`p{&SJL`iwy4_0JiL9f&RL+jSIT|P~OroKqV%y28!2g7r30AO~YV2f1hpbdctsmE&Xw~*oqjo0vt zttavOcE}$%6(ENfB_DPFfQx!LejrsC#(p_&910AV2$?o{X#>220nfMV}u#Q z9@M<(!65I867|&s5>TuTbK@^aiTXxIk^t|{Q2>^{f9TlPW1-yu=IA^2&r&#Hn8PU1 zkLZS=8Es)$Q&OUXl~U1nPr<*EXXHa{(T|j8<@*!$`2AlbL8CpN zIo!$*00iWI*u!?Xet1(#`7(gN(PwCs^$(xKvv-`!SCX%Z$AW_cTdl*wuorgne&* zN)(*7n=+9yZmT4Zk=Z}V8;jNnwFi~J#91!~Yvpe$zd`1x8wIVRX78xZ>1&y#<;2+y z4>HaXZ(KiG6t7z~Ev+AVx?T5_YBQmIGV6r)v$Y$kc!bkvwU&ts;QfM9HrBq(T1k$3 zPUUqz-#Whm*#-qNl+E-Ia}Gkd4a_Hvm6d0Q8Sv5boL<{#Rp*LjwF&0rKeYS?2o&@f z(h1p9v9m59?skSnym)?+P9fQWmQtrTFWIM9QJF8kmZSn$_jLK>lv3kMq(v;z$ulxK z1gy}zbi7D{i`4$=(c9iuL}A0HInL!YfIhLkKZ1V^X8 zsRM01U(iayH1&=B8bl1zkaTs&<0ql)6;oKUU76Q-^!?EB?!CYs za`crzB~T~)ZbDXGpHmGRIdX|wRw-@}aOd`F6LxAN9;qGps(~Lhm3!(Wq3PMc7m$mc zc;}_i{q`kWHr|B5Y|?pGc$qpPd5R+pzO*D7b(pjff`8P{Y|cc{x{lKjhbpve&B+m_ zF2stu85-UA452HxUhbpo2#Y~czOAkt6DY^~>J9YJMt=J`xysocb+SR{4cQ2M+CY>7 zoh7#Av&ZL@j`)-p+ug3N2!dH+dCP2me}(z2RP$`p9AiIT-Pm@I4ez19}zjJPFfiG*Vb28t}}0POt>(DeOp?ShXNR zwnjuNq8s^Hn`m1JqLMglODLytO(>=@tym86v^Y4*3VbpKy7VeG3C>-UXgpPVmgR@{ z0u3PGYWiEHJgbpp zcMt7pX9%p)GnSHbmAIP=js}DB-2>YE)a0dpj z^y{2<(JI>O@6XW^ag#TQ>aRh;xl5q(~gkE=~-k%c1M%V*#2>m1rC>om~Nxhd8-^mS=33{04RCVdRVb z0Y|VL>S}?3?m&DgiVaV-G~ia;pVQ2YiGZDwnT>7tlB<*%8#dRxls&aF?V{iRIC|9S zZBisYmKPy{*@9Y`#<(H%xuWZwl^H6iw%M&6qK~kNOh!bGaQ^XaYh#!S< z`C6ishv3TV>Ev&t(r6cxWK49`4yNSra@?w>ir+x&{-W~= zGvJku2pn@jSm4T?cxX(^{h6klNpG`Be>%@#Uu4E_66Z4I?=`+_j3vDWd5o3B@*5R< zf3%}c2`|(pZ-qP8jMbv_voudj^P;V@CrvbEN)yu~EltLJls+?mObxdu^|IA8Jgv6t zs;Ta%tmew7{VgwbRo>rX1jUD3Tu-%sUVmYwV4lEn!l~_6onxO{Ryz9FrVvtA%C)p; z7Jj0}kUYIWraY|X3)N+T?RTQvA>l<%rHye;dwcPJTYcNnT1{F_Wyh)#?9h7rbko+# zGQ#Te(xjhShgoce2llm1Y*mI$06Vhj*w|l!OJwuY44Yw54vy1>321MdD2vyrZ0OF= z<15L8>*Wt=LYuXpzN1{2exnBO=Y1L5@179T0ubj;q@PvNLxu$0U9fP2frNE5K<&;)NDzb)uK%fAIbk3tBNf`*AScm-Vi zo+Lk`7H~21kRYr<@W2xMRa|l4+fgDff9U)z|2&2P$3V_Z{y{KW>Vp738nRC3b7v8& zthz0dsFES08i_MX=1a?G0cCHAb#;vMc6@@9tZh=$lhZt!zD{Kvb#;8@S{eX;o9R3v zeosg6(|Jhs6-$6_FJb&x9~d5x3p3RohxdxqB-f|zkbH+0c<;wb;*6qy!gz@G8q}m1 zBw|1$DK3I8;XiD7qp4?rqi(Wl{7rKctu^lZH^77svn9^I1hn^SXVbsU7xlFYui8B~P3 zd%;#qt*q9CB!&8_^)MGy#Nk27JP~&Y3T4jIGFe~l-sB5xln0enL9J6A=mzrAPI)w$ajpWYJdmqE0740`h8nHq`e-*88(2^ z`r*J|V1}54(Y3x%8m;GpD(&T2lKvmAamY;M9}7kkt{N}etrt-><4J_Jr2#UAlR3!63gkx9 z(HHx91gkp-7k-48q7&m)+d@i=ljvF0!Lp!F6p(+^HN1io((iHe=!3<}pP$z$zC+@U zTQGFyk5=v*a8kZS^uExwr?BCtHrnc`uvE3Zz?~=vJygE`2050MYq$u@DBX0pamo!J zV5?R%N*_?Mwiu*E%2BUG<@V}5@Z`Iq?zzQoK6ooHZ3ZaY?;MR6IBmOMPr}wgetLL$ z42^bIX}M#gXo>!*QJYbR!urM5TIH5kOxL8QX_{U=rl%S8WCz9d%g;dzu|sC|J}(^S z>CeJNbupxTD?o@bfY*ZO158fFCq;lo_F5l6s(n4*O*nR`jR;5>zK7Xf*YE^Exp0mDDqonPfsE`JQ6rG2L~=nZ*En+ zR(S&a{OsJHG~&qzzk#mq>wet)TD*es_euOW3P@*)lkt~BV4*cs)?>C;IMX*q!R>>i zdN(GBT26q4zq_oEH^0GJS0HUL1IGC5qGB$R{JjFQL^epbu}TwHR;BIS^$`s>FWya$ zcyjzM5+B=Rf*j{mtCG&Rs-Ld65H*(Ho&vh+o#UPY_>3y7qlT3Z;q9`9*h1mjz}xJo z@ig#SG@;xzV;M}$S#S7lgZXBsHCJ241m2o)#;IEkb|BqwVK|Zv3IixV%dFFb*mF_ViZbIRTpy z1f<>cpJ~beZ-(hVh3IGitcix?y|?GH|HG~V6)CK&i453*5*zG+&`>c-y$D*e3(Wap zvO-FzEY+L>PY*JRv!tORKtz|*h=vA2N5fzvLUVESsc+uX+mm_65EJ6Q;qv)(c97)d z=KEd9d->Cdc%{F(zPkRB`|S{DEC>%GJ}LtJXeC|sGMG533Vm%hetyA&nXltg6s9`( z3x#e$c&d(gIoGU5bj!p~{a2%Ev1+lpY2}cpBpjv^of>DsDM+S4jJNB+nLvI4Th)X@ zXr-?ftR!KY$UzS=rr@u0Wf-W5pZPDyswABNF}m0)nu?*DknEoD0=WxY5LJS-q3IWs zw&jngoJgL1F(Hl!Q;e92Lp{3KU^+y7zg9D{oa!gL?oww)6jC$??d zIx$ad+qP}nPX4iNoESH|d#kqgez{vUQ!_nN)!(}N>9^nK_wuP79N;-%5Z3-UXv^4& z93fh;^itnr--96y@vqXn& zXl52(@uF9`4Y;6B|L~<6%r3rqXEV&bu5s&26k?ot*U!w(zqUUhC^`#<6$mOWr)$oqTb!y0h*|hiXzlp7W@jnWfALz(M5N zg&?5FS$9C-!B`Wu;|t@*MSSv~jW1I_^)8-KKl5e-?i4t|OpwwMVZc;&gsyg=Mo&Ha zaLU#fe5qM_#S!92gIOwWX1%8|^2U$u%80OL^{t+%U3~e6;0xM24=jJ1X#tp~zQyFz zIk_nB369Wq@y#6Zlk0HooVm&S7i#^1sbZh^6;1si=ZlH%cJV=wZC^0zRqj&9|9VhD1?a{9fbLN}*(f`Z z%;qnCxMFn(9pNe4mK>(1alQCZ=iVuNC?zL}<}Z9Ocj-w5;K<89uzqFD_*Flgxb(=M z^v=K9yY%RvKuTcrF22HY`_1?3??YOe>#e-dBD}09q}{>UH2ZfPB{yPm(rRmU^~3a1 zqTT;T3YnKT-Cnz_U~OrR|1BgAc3TBV{|K{uWoga0b_m^IYO*Rg4sLgUJ>1fYKws=b zhYH<8)ooI9u+LY6l|1bez}hF8F2_N8ZZeOg$1Usb{JDuWWjQ3UCuO8w)y4m~!PDk8 zN!2Tp)}s848T7zL$M6!Yh^m4z)?kj#tiivLM&abn_|51vFjbWA&DxZirB7Hq;ZfncN1Gx1sejI1Tc^r3e9Os!L1g+2@yy zE}?5$=X6yz56R)G^EK3yiTtIhx^}aPfFZM(Y4k=bD_ZLlx$ML6 zHk13Lif~xRZtSo;E?0ReIpPXmCLW1fAG)5p*@I`@-QjT?WILHgb5-!C5J%$$i}#9+ z4@3RRofCNGIZMv_)d2zPr)H5Hhme0Nnm5l<3Y8Si!D_Sr*bXvjiHJzM!58l0_7MwBm2Zgx;u9q!;gq{v2OOD2Hd{Hejv@A&WyHW^AW(e`77E`>|qVdiyHL zKur3Zo2|6z4vKDj^1`Ce&llDw0GYN51{2{ zL(aD89$EagM^+C~tO`Tb%pRo{R=}kAM9mAo9uvo*gUi&AH9)2&w(rO~5;Z~pOUf!r z#7c08pc9>fWekeWT0@Sv8fy?fewTzmmb$8_JcN}ry6sQ$NX0U;pLkOt!xHM9U&E*; zlP8>C8QM%(U&DGFYsU?%u|`y*O%sm7W&_7LuQKkzPI#c!WG>3%sdmXS=79kVZXzp6 za(POvDh#IRA(E{~U1R|$ygVMZVHfgJIMa4!-bl{{K;O8{u6y3INORE`ul-g*@{h50 zBZ#2Bv1nfVGU{qpw#>3($y8`iikG4<3_gm+vMd}4s<&hCNL{maq}x&W(jzwrj+M38 z0F&J=h`zPva8`t&F!e|o>&=98l0JdO632UM>M6e{5 zWXT%KnC$`FU_8LF$B{R$Qqzj>mO|vz=C(OcT=;i74pc%s#amy&5JX1X0C$7YMzTX= zDVrWyv%djA+a6DV>#oPz#d^puT9SRQX@(-2a6klxju2W{`8@Wy3D$!Q{L*LWY4w;$yCOr8&FD1_2d1b zqkaJ~O>Q(Q&I>xMU}yESjHDK1$j`uijOXGW=wDuWMa`^V@*MZI6-yh-v-ehCKtfPf zkQrw@82UBkU|!NGN_0A7M5PkaEr@tcA3kPm0ZY%%Hy*YKFGh8r_SK6{W4K*;$DhZ` z`|scJJPM<5@jy_~HD#%7yy{ugyb$ur1}tj*w|*4pH|>gV^7u3Zo>IIW`eR&c4LP6#Z5N?f~tINFTymrIATWl8r8A(RI^ zmnp?WI0PC6TS$?l?BusJFbezcr6r6UJdN;ssZl6ite8&XvjE>twwhsr`!`$@*p5OB zhHNyP4$?tSs3AJo|!~c7K=zc#qkZ6fsi1B>a>@hqT0_NQ{^$!tB#RxgD3N z59)9oo0i6x(y%W|f0Au%TFam8LVjY3OHmhghlmf-W2C^&n)~)y|VXB zKmh`je$l-j4$wM zozY`1&9J?FOeIHZbn7+Mp{3lcOK#Wh#N-A+obSfO@8A#wGvTZc=`VD#YPm1<<3IPk z^BIIhA)?k1XB|f|;J5%9I zNYM%F@8D?Ul7P9*?d!m?8SSxPh4u`z@wZe96#~HLJKM+pa%K<7EH-9bKV^L|ovv!LQiCOri#ECdKd`ZasAi!CEMHK+q_6kYACJ&N`%%Q@PgpB$OX78^0|Yf zlUnUTcCd^wu?k^8q*IZ#0=6RCMpzaOi~2ByvZ2L`4yxUllh;VzF>~3u<^#P$DJY$p zYlkiLLMm8Wz$DIKPgmwPVW#Ga+!=wXEfBcYtS?=rg!WCWYV=RhT)$cpZO!rJS|YDT zD0M_QB_PsWhFncdDr@byP)tk&NP_#3G?cg(0kOu8QL(;)*>B12TIYP+7Q_f8w z0i>rt24HHXdG&Bstv2ffXXtLTC^@B$E*i6zPawKxmunbuw;?92}t_Ol& z!<8S2%$&yWj&Wni>V&jvh^3`n=0lt!!$>w{SZk^xD>a|Z>?`6 z=*?Y;1O7wZ!NS^nnwEqmZ5l^vKITmfk>W3Ar;jFI<^Sz()0oMHiLW62VS?$O$Z;C3 zOo~;=n+N-#Ms}-6G%8YE^*2DX+~6P9e1Sn$744)`1TPx>dm0&w#Eiz--MC^CfzLWM z)Z}SOilqP@92+{=hE`Y`#eH@Gnc;nB08i97gU}E2EOu=GV4Oz4BvzyAK=@+5)b5Ua#PNgm3Qknj&Xlm7p^o zI8MS9*j0j(3gh;Ddc7ixS)7w^G{?-J!a5^fHHigoLoek6p);srjKqZawEa51W6QU~ zrkU{7>AKMyy1X>I=KKt&E=rZ%#O=oAK%LjPq*{W|U?y_trr@J+;?X^*4#pUQLuA<~ zs522!O6+n`a3zVx{k$fK(bD0*3|<$7_5 z_$Xi4iNd|@l((Xd+VCE^`B)t{KPDizOSFs-XwV^1=#^-T&9%S|EDcLvRBon|;XZe8 zALX27fgM(w$b>_~Q#S=MLol2K4jo61!;nCHY0TGx`Aqja%(LJ1(4nm@d$;qFA-NR( zXj3woyn@)c^Ozw8!{{UCe!>knw-}2-%U?q;o@ARFb|5cbWP>?TYo3es;T&-rQ|kXVaW ztK4$5y?ie)N%bqDm!2f~Q8R~;vWDjvMR%~Sex!4vO|Nj`8g`ngAb`^+TxYDD_&~aX z#{Pz}nzJ}U!ZuDn#&H?_FaXba+I;I(bTzIwllgJ^r9n>rk^~}mQ%Byl61@A)uM;Sb zpYyZ;G8fOeU8t8NfgeW*&H2o*n|3Zx3D?_|Df$dxmwNT6J(ZN^M%hz3(p9?Wm74bK z>^*Cc9`E+T)nceX_1>TvQ`NcWUeTG^JXz(H=2D}1LX2PWFx^qA`ON$dJJr!D*&3_) z%y!{xwxu64S=34Ksy7y4qC2(OKp*e@r=zs~P%*?Wa1H6ghw2MHGJeP50~h&$59M2I zxGQ#~mkIjji0Q_74^H9q30|ARU{W9G8RSCYPEH|W7(pR28Q*}m7G_$&s0 zmue;;2|q4Ebsa&1qatcQs->TYsF9P@bQR%L3a!CHiXELx#ksKMdf2P4*@5yA0Ng%o zgn4-N_rn%t4qIaec)d(r!2i7;0Z{;l7fg50@Dw3dfR7i(XOD?0gpLu6Aawo~tf3CQ zWPm*lT7JOB9p<>-RRfI20J>tE*&QO_7KR6vnj3xvAi@rPLDEsevetvNcC3NuiQzzuzGIlFo=~jI1yKpux3hN!4ocj3!@$~q6Sn!om{k`hC?DJ z6v0hL6eJ&6q@NzfB*7fzm5{y;i5gG@c|B2(x@D23B?V2$1Keq-!%_~3I)2{X$cWz3 zi9mk(mj55TIA&p6pts7he7V1Qf}Z#&tJGF%CXT7 zhIb`;jVKdpCp+OLn0Rwq6QlvhA*nMH*3)sd`eljOfq0eO(f)jkOAuGELOMtvX;x%* zVqlW_KZtEiNHpb6q;m@hSclht8^XcQ&M?jTcclw!s1wB+2lGgaou5EW8VgaB{2xPq z%Mx)P)lLKUa`V9+yxvQ87eW=^w&CM#0$cKeJGG%YdCoTRQ6+!DxB#6tfxX{-<9~=V zPlOTmfDYUV(}v#rS^0n}+=;V>w-|F%hc5fMQn+(TJxuN`@hOOPXaez@Y>Hbes9@N2 zZ0mn0$%}Oa+L!ce(B?ACT$f6lUYJ?!s1&Y!MReWu^+p^rW_3>=idHdJy;t-S6lZ3!Bo!XWZzv$E8p) zrz-;b(u~l}bREdTSIDlahrqR-4>-C0>(SsxIkf`}TM%o$n1BPG`#foSMFn3`wjA3| zM86IU%h89T<)aAsYs3>VD==|(`a=`)SAH?~ZWqGs&@&y|+vWJ%c@$shvUuLJaKw-= zhsmPSzp+z-Tl%T09cXwj+2%h6IqQHJ2R9*9(^O`Gh8l?Zc?J5nxzPONpmorm(gdL?-Iq zwkVea+|Jtgn0P5km`zF~c^PoQV7Jx?MNKB7Y)yS6nO%iGlzFm0`)Ux4B-+7GiE?ao zVVA_M%+GD+cjgBV20$$XGi72hvfBbpYbLBk1lPM+a}p?TZ0tpShe(*%SWW>R3fGgd zqq5Cqm^9`%2HA$=a};FHfU$6dN$^^Aa4|eYY(;~{E?v_k8!Mv)-N&kALQK$OMthb; z4g*l*C7_$C4XZw1-fW?9T+!cRUE=y{gL!>_Tdl)0M(jW-zsjEG*oTmN|JL!pS-s00 z%>Hm~%iRHne93kQ+W{AU`7N`Tse$Rsa4~UXY<@d!gW?Cob}rZw*4oGh@T2R^*_tA( z&pN?M8@?V|%x$Ef8@V=eE5yeO#%1CgV&@GBWM(Il)gdel2PI}ZpyFIpJoT62_=GC+ zzMLy8vlKUs7-7fv4+jK&i#I%ru`1+igw7O&hS20# zS`k6~7LK%*l8&~s8prhnVjaE}dI_jLxeDkK)_at(&#Y7jN9=pYhT%I-XhCfQC@g>xSFTmx~TH>BN*;wm-hn zi<>M@`Yi?AqfF1mG(aB%@NyJeo@NwFtra|&AlMdCY*}Q^NA@N3%XI!&LKKdlUc z0NROhZ3Y$2L(qtENxKPveYMjV%?;6hVR69q!?3kx;GOY{eriV4JLvTv{Ai7O__U$m zAH#S^)%@)jTs_!=`@-kS;}>naU)zZJl6eXLh4CKLJKeK;b6fMFn={D23;Gh;73DXC zdB^eU@nN~$$DhpZ4*cc;XI_--$c{Ra&$dl1N1f;deb_p8DoSuyx3b%_IEugA(?=&{S2?5IX zM063Pmp68BC>WB=tA&-#R39CH!(=0WkRHcyUOJCf+ufA2u zGce)?-o6hYEcu6-jTnDlpF%Pe?M*k0NmD1LKMQ}` zA-7r~8kZB|lulaVWe^XX5;CpxDA2oTP%`}C0otQuIxZb*l37%@TT37%b33Eiy)I|N zP}8o#uh7WC%ofFwbqrYg#uGccdNV$;C*j!3(4JNkTAB>}lmVEfbo4r@74^PJ2PnJ0 z2bWZ(K?5T$6|&#AM8zr}j1Dq(S6F(8$aphIATua5zmVBAXz-9wsOvy3Xhm>7go*|! z;(=$wWZWg4gHuHjImw)MRp;S*(6;vY2f%m2a~yQg80kG(LQe;sMEG4pu=+|O{j0Rl zm}05SCWA7!NFeHYM6o-?;S8^gsCcMC>KTR{o|>`xVB@YMW$QIa@bPJN1f=jD=Vo6LXK=zQ4daa= zHLLLk{;5W9s6u0mykb@EQ(+mA9R4SX08`UNyRVLdwU#tCHY?XZM&Gg#<&uFM0ZOTl z!sP$OL{Z0O#NW$mv*#=Qy&N4UB{&aiacfhVK2U_?aT17Y;;vLr$Xs>lLFze*8tTFl zqH7MXd>t%3rPywgyErA&0=k|X7|Ol7=bFSBjnTP>e2==MrrbeD)zb|TolAei-e=+M zh1QL81^M-R1)=;1rr#%Pccn>~BTI<^~R7W9VIVRv8R!WI(A zglg##71j)v!+J4g^_XH!%wOu(e0?vNcGJK}T`W#sof`7UKP}1*29YntW%`bm?}ogh z@MGgpfv6nad>z0x^tQCWx5a}G#amyfSJ#R4pKxCvc~dctq;i1Pls<~-H|zlZUt>Vw zA{%>FOiHO&_=i(!X4fjY{<^TDEiv)tS3<+Oov^N{8>BLaql|a7Mgs)@!s&V5%LlXy zjggv4K3^tnXsymSZuz{yv~O*E;kE`b9xC=_vHagQ1Fq?T>>T;v&;_Y5iLofv0>rCA zxE+yoNa=%r?@Iv~R`MdXVQV8RcWvttzDR09dLwanaUDrP9Z@^lw9`r0>uUDtQ)sWv zl!tw6QrdFqkPsUlM0h%tc&F?zV(WN@3T+bIqiiwc^C~wcTqA95Y61N@n4N3DceGC> zfWd9#+4|Vw+O^Vsj!WA5+)r&hgP((+%P{x!%Kc|vdA$A4;mQxVJDgm4y%XX)c75XQ zy~^Rv50bkcbR{1U?sxQ!z~gR6nzcofdz;L)k_TRmp&Y@?=0@sJ+-cwF1DkkQ{69+%$GVFsR1T%)CJj+F z-DZW6S3$mJGR^YFz5Nhxe_ypb7ydyRFtTYFzbE5d@<>f<#0(<;uCxf6`1AOSv~B>+ zPoEIy-){>%OkSirLvIM?Xcf^0V{kfDWhSVyTCWj466Qd`Gw0M7zqA-on@Sw64Ai zN9A{^RILX4N>;XIX0|^nUMFni@9X`FcC>3J4Oetx0r}?%pKrlUlE;x4aHT|G>j7;EP++B?-DNAe?ub)dGB?U4Ec2aBER1_AdVV}e1kYoyz>z# zN_aEN-N@(n@pi9fiYG8V43Eh?C6s#~?T#tL4uVB*#he9<=&={vXWis?I?9_|1a+11 z*%fS%nK|dF=o$8ovQdnESH;Ta)IMyHo9U=Y$cGHX>0O4}(HAG}i$~>D136`SWJ-|A zojRw^Jxv&{5WwY)>0yH)m{J{54HBzk0|zF>E0iP+TA1=qsZjLs9>Gbwa_t(0D#$rw z^Y3scoitiL(qg=dpd8$fbH0lU%NvIz;cqQS;ZQLJjmBOKAC~*AFG+QejX!*#%W8(jOrTCD9x<;Ivl>(whtNAN`_@8TV(6to*`I|D>yV z)7JUN(bn)NFvIF`W`B#am!}U0SzEt49Sv`QsSD6Ia72o763EFznEwvx|6NDbPFW`L z2k{K1ztn`t^D`Nx&s*-9*}YyhF6eX=M4Hz)9l9bpb_LQ8O3_$!!6 z3SUOCi&6Mauz3NhF^Ph;?+8ihqYi|$W?8BdZO8f04C7E*-(I*wSZ0wX5WB?ERJa?B zUUydC0Qwn3x(yR+pm}L9?bXPHB0c4&&TkDaBPcz~WhLr;KgIM}UK5p7B{H5K^O_c!9QHWwDd4(VGKCYLakES&%hz0oK`bCB_-Nu`yrc_>w-vBN=Rh~#umQ1Y$S1^|chd{L*&3TBr41R(ywZjd!;>f}s7=a`Z zl1Y`zqF4_#5BB6SjJSyG@}TVufmTU~s}^E`JlM zSJ6)Bq=P_D-7fqU;W?x^)_&~M?EcI)h4ktnZc}lP_zCEW3NBInLc!UUzmLp3m3m>% z(n)%oDbb&$U>xXpkoWCK-S~O z9nfCa$K35CA$(0B*60Q1NgW%Yy#|6labRJ$VNlZmia{mcFJ#K26Cu z`WtB67pr|TMc+;G!gV3Et?m7C2RD5)|C2GK0>S-4#S*oBztfSe^T?Y*mfXo(z9XqJ+ReiXr10Gl$|cpfa)E+e)YUE^N{NTqh3e zbBxA%V##e^;U9H9hA-K1cC16UhrM<`gNJ$cKv7m)UsLCS2{S+oj828cDnCG3kF3WO zdi;5h_^Cn$8CAFfNM3*`Xh)4|XiA5G)CCXM9*_@4F-F>tlS?Q2(o7AVvE&IwS7i%P z#n9vOY36iQjaM9otNg`1-E{U6$6%h?EzA!UR!6QrN86r{S2rpo9tr>bVk<^wP{Fwq zI!dxrOMGY2jW3oaXVN=Hy=R7LPm?z_US@8iDo7vp<){> zf|y)MKGZJqhUo+t(U>kyH}+acJezf~yrf?)UTj}~HS{_`z3W8R4NM-YF_dX!pK@J* zSFVO{B)3VUYcPy?-#bcc{RtxSS@eSa)*g&z-u@u{XQK3-&~rV8D5fu(61mXYR@Zq+ z@$9npwncV6yksQR?{DNbq&Hc5IG6gR;z8$^`XPkAeBFzK;FdFqJdJ&IAE#aD*5YPo z!7sO>o5^9%XwlZ>QEE!^(50RYd=N;8r1JJgpm-mZb2-mKbh7Y2}F z{Ja4F&VRs%+xAG+n2}Djv#_mca@bp*_Fb~TKy<8VBGEjA{yvGoA|PD|L}zPnS@0DT zhIpokq~9uxNS!!kA61KtX0RzSEXy3mkJL*2Eo5mkc)+huK>OFPv&?LA2-y)1f0Zd% zv6?g4cS4{Fl7_sYM$Ibu)tM?D(+oo$>2A(y>}-!~8j;YYBIN ztfF|s*|`Nf)I@NUedtf(5X8E;F-4 zbFEPM%IJcTTb46yUjD~DkUJN03H(FNh1gF;cc`@@@>ThK_C?PHn!jk}0a2CVJ8`}5 zJOAy)(Is7B`AA4TQ#1kx$zqz1#Ix zv(zcaGj*ZLPD$w8?1IBX|Uv5PJ^J%E2F3!7(uK+LV$8qwD4X1dBDNRzEVS3-) z$O%X`e1OsGxMHB65Y)-KfTP<5Rwr&Ef$=p16L~Rvv~7?e zR^J|+D`-?Qi%JX1@q}LRO|TT#U_T04O*z)hej^WUZi&0E%YQsQ0sTcGySP?d5Owsj-(KG0qJEB7$JcOL>V|`jx)dgdB>H!P@9{BW4?xPs*M4kd-)(VUxJw2S*YGq?*&%>6k)dDo&)pd3ZF(rs zR={(>`50$-eT8*K5a=Ps=#3S@c+AES5+kTiio^%8JYsspB(VzcrSJ=BI%9M`6N#yE z$6n-K^Xr7`*m4^2_5LDw!_Uuca*0pEz=FjOCSga1h9Brvxg6t!Fpj4fQQP97Hf;>U zc{HK&1%b|Z0cG_=wg4#=5z{><|SOhM*Malu)6 zcFcUud>M{7co=-Vb3AU5-tF|XEVMAjJ zQxgW~AF9ZIH-${?j4l4JZ(rToSw$WFOOAOwTZi65hQL|E#vYa$Iu=e4XB}FF2o0Jn z2!#No`pE2<@y~d+*QnPG7zkfQbj!cbnia@(txAh(1$+`fgncUi$6m#xstetB+hcC8 zGaaTiw6D3D5*USLV}j4qn)yxR&im%a)A_JEP}QKlbJeggH^B&x3vWOnd>vzcT~Z8d zQHZgkNx|58ouiJwBsd6)pt{QcBz1#znkT)OB1mIQB(oaa8e8^XSt!)s8X>L zwH&pBmcJWq%QI`-UbkYGydpvL{wFuz-_fOU88kwbi_ijhooacJyP}1ALwLgZJ4J69 z&5t{Xsu4<7Z>7AIJ7$nONlGE? zpw(9G;>-ypeKPa48UgXy`1pgBq+|!GWGqCVjcGU<(4gsF!wl4*o&`@>)Y`faxaJnM zcK=Y{{v$(OBrRX6JjjLIgdStaO_BSY6R0%RY5ZrU#6e@Pv3_kUCu;xe3h!ED;TXED zxT*D`tZggsDDiF%N1c+gayucjq)a2q-|==@!4os(VezTQ;Rhn6zbF;*={*#oY$=(n zjy!hu@8+arQ5Zxu2QlDtjnM1C*u1rfI+%`BoJwN7!1>8rH)yb{IYv64;6Z1pZ)L%AMqA;$an0eqA(J$oCag5S>G&%$Py1+En#rwsB$^%jV1u25oKJc7*s}ny#&;|#^UQK6*^=G}BJ4_$f%4EmKD62}tRv4^HZ!R$z&uKc5dSkRTD`sP}l`E4u`sJ4p zlHMc3gfi0#XP$vVX@>y+69)X zDrLOF7VVN`_LcE_Ge=9~&b>BLQ$6TQ6_AOWEwz$I?ngX&-TV;Ma*iY3>oO_@o3aHz z#I~c=qi@IsXQTru?d*BoJj!#JKCs~;efl|5svymJ4J))?g}5Gz5(Fa~V#>b=)%Sm3zS`gAO@ zRq@Y>ZHU-c1DBQA_0{*~e7CEPQZb~Zqp^&dhhFE9w+D3tDtEP7??HyRNZ7E5su++v z>-^`aSa$U`N1K5q;ZlOFYe@pIM2#BD(nYU4L?XJ%T(^GpCaH-nHBjF(Hwib_pT1mc z4E}mFAtTUK9x8Az;?fOL)WaAhrQ6pRWgQ{4wV){=Cn%>Q!8oz4PMve5T5$lr8t-t& zFUPMZAAbYbO4?%F!OzzKckAAq^S;4#9Jr*`JBVsYHIx3Bm3Sw9-|tX@6;Y?%M_Z{i2x|XOr*F2GWG)|n||z?SwkrE z`e=q{Q1|47T0_XM5JSa4e3A%VBPKBsT(Mz@sBl>TDf@w7J*<}~&H;%qZ|LX1D6k_A z1+jd zUFstH0-Gs2k29=r)TEhJ+6!Z?=p)qG8rP|+GpM!zmJ;zj7b|<&akQWNXa;-GB=r%< zXKQXRc?a@)qksEhs1d6q#uIEG&J|_RL5Vk77fUc{$6qrFWzkVi10XwTbt49rD5)Tw&YRFXl_x}R|S=W~6 zM+X0)RY3y*G5@dhfy$3sAT#=Jkx;Cr{Rd4I>1#%hInm{>4ippcpo2a;bhsiYgvhj% zfiQ>_Njw-s;+C+hfvanJx+p!26LM{Fiq@N%ri!N8t3a}n%AdRFAmGQ29uVN=x-t?d#9qRV<{$jDt0d zS;j$EjqHJy#H|`wbCMuEOw=$CF@rP9-RpC};7KH2Mpe1n|@s-jfC5P=>^4 zj8@Z+I+b>afy}y=w)|Oam6yx`77NX-QNJ&cJd4&! zMEDIaJY4l733#=u&9m!?>>svYikurgfJqNJ7NmJnZlpU};Y=fgYw+yrIzmi|ZA~AT zSJ5T-)3jrpn)!I;iWFC*(p3b4WXEzIClMlT(ji-VZFsoO?i7mR&;fP3s;$*c#?l-H z4x5O-VxfnL8l^{RUaLr~@N)~S1q{X|3`}DYw2SL{ZlOb7_d&wrjn*b7fO%|&`RD8v z=ir?4R{K0{80!?nWhHO`QAz%;Hb@p(b)*C1{Fg${6NyH->fwE`z&@u zrA4LdE_#FDpqO_l0;AoxL8{Y7xbTxF=sj=?!$RSCAG+7SxDpQ{BqnbFN_)ONr-w_| zdF$Pzt7DSsBm?1 zmPmWVUp>Fu04)S)mGtbST>J{~1YTr6eCRnh_oq<(th$-gq==mtLeUulgWauehS8}B zvsamm&d$#9C2fsK|10OK`Va3s0yUp`09}vn0Z8YN8V$>0%b_=N@1cw+9m%ZFqb zX~A*2nn<*B8P{_Du|a5u&x$-uVWJbP`i&!gDZ8>du@Y@5vSf9J80#`uv_=Ivd$fDS zA2S@bonXpNs*RUOpcr>J1V02A4H(R5s)FFe9-Ps+=`Ha}t~*A$pp*d$A?f_cpC=|f zT`=Rw=jfdFF#2osg_gjSPN3^8&11fGS+N|Pn+UCYUSSK!0pzc<*r$=q{qh^+_NWpB zs~S%l4Q^K^`kwYzO460F>O1s1H`Ot6x6Bp3%o;;Ya2IE0O&?+5fmD&eO&fo-s-aPY*F=* z7g4K-=;Vwkt2zC2rQK2F9U*d#%Vo>j$7%O*-q?!Rcu`45YYYED+}DS-#kBOsm0FoL z=Ruj4X+9rj8TA3Q;DB#@NT-?uw&dc0=4pSw67KJ{~bp!2{f_cfwAomunL zhqrKAEKe8f@p*~>)Wh`DuF~~C@HPEzoh{N}YSG@9vpN+M7a5^i2<;fJVyrkLh2TPC znvfzuDGB9tsLLi0M+`5FlWQ#k;#b)BSS>dQn(E~$6CtEYSRgkYVtJc94(iz_4FOWv z(?$&&l|hm2>Jzt9$<0R3UjjJKmUP)puLqFu8R9>^=`;{>^v&~+kf4qH=B=V5`)!={FGY&~1d1(-EB$@p+Lh?F>= z^p0wFjI}*A!t;h+tTU~`VtVV8PVE_&)9(V60Kw3?#B4QR&SX;{mSs~!x3rbKY+3LY z!jHzwy@DnO&h$&^oqFdP>8thV3qK^5u=(XkUTYZXMhPzlXmF|mu+V0xJ zJP_N%$o_BEsAGLz%*D^eFF>*J;>N6!2J`=j3733Nc?89`k4#k(EIs}eiKZT;xa$j&1yPDZOqUjv%rFHOvC)bpETHXlQy6r);HFM zy-&r`%j{Z@!&po_dyIMHj8#ZV{s+~$FwqJ8@WinZS|d@gS@ATyA+BF3>}!BHsjUx3 z&%ZCn*SwrK4ErJk$Ko2aMJhPrhu%xuuK<}=;5_jXYR}3z!UG?*7_?N$ zf{|5IILG^8n48xpuoT(^sa5S`jYezex+*xrF=+|QMEm+n=og8NKT@;J%bZ{jb>O5X z<*{3lTPO|TR%%Pl;aV$d4g`%zK%?SHmsy21WeNUUhY&@mo*)B0@VGs32!;=c$#GwJ>KYzhSkqqDWQ9G?)^BR%Q-m zTmMGGnc?yzzq;UI2WB;kKTKLpwp)-lou;;jlpDtnw%v+M6VdQWd!pSWa|rzT94Ws zwT|$0-Cy8PGH;J&?SP8=P3#!L{T7C@%zX~<-tP!LsSFDFmlgjnYEjSxz3C;MY9sv< zB5Uv}i+bwhg0YsW@h!o9k!&8=tUlaamE?vXyfd9oU9eX|AtPeuwl{KFr9Rl3H+I~Q zM6Q@KHrOvOU{Hsv$wp-!zjh_<1)r2P7yBMl0Wc6dXicn*4BPAi=X`Pm%fxTOr{ICX z5hpb+Pb{bFXh0#$yr+65XzZ@}ay9ne0`|7^Ki8Bc@HP0NAJKmK|65c3L#zM9`HcTF z&i7wy%Cy?XA5>GcZ=Y$?#VZoqb*IuGp$)W!5H+f%Ba&*O;U-aKS~~YNyWWxwJ6Gd| za_}RPf`EPW8#l%N&cK6T*x2~`-gmvXGpyh$EOl?e^H&L0S>X24%*YE1E7BDTyLs2Dtq#0dtsbKA@ri^;=orq)Vv8y= z;T{IOj8M;$aM;GEaatEU(ofM|i)GXn-TzNxX8{(~*7b3^yFmfz4(aYjx*McHYG@FF z0ZHlZZb?Cr6p(Id3BjSeWyo)MZ+I`C_rC9$Va|bP)^DvH=RBOV_uBsuV4WEw>o6ze zDcM~0J6hDNTVEoD3ogksErR)C?(91EK|JIsH(U zOz2*zhgbC^f*S{e$qUAwXrNjcEnNSrL59Is9(83>!WcP`G9ozkGp-~ zRaR`_N0(E9AfOOapDSP9vUi@Bcnw>`hZLy+{147@$ZSkr@Q?+%%a^%Zs+Q3oo2U9a zG#KeE60O-H(BH2Rdi);f=^QW97W62j!LXy*OQqxLLaZ|sMWmBq+azRuMRn1pw3bRU zg~3kZt`Ba8{-zg2q@x5!IdX#-K4--h*@XTA$0W`_e^!(}B_usvf;i{|4`2GAF1Q;1 zWe_eL5h35x$!{cM#CDEvs6zCavck$4^1?{8pYKW4D-Lh!f3S?+3V)&Bz+$ITrfrIh zXQ}BIlUA#xK~=@G;N@C6Fj;fK+&LIFSG66&O+#{kjv2W_8!YU0$56=-EG$94;N$Kp zlWaY?MQo*O=e310@tH{Aw~dpbClJZuwm7?#+$1WX|_RkYPrl>GPS*^1K5= z!F7euW^^q>Zw89*)bcvgy{;dtBNw6{zr#UxDO5ks_A|lt&||AY&p6u5ot zggH3uwN~Npu?UOk9Oy5!vtR`?bUxsmZM;Tx5&V2U@W_;9di))pgRUsZ@K|_G9!fA# zGW1E%e({6MA3ty@VQaSAEAhAM24^!#?4%2b@=WM2ke-jk8u%o znLz+EJ!}^&p_qX@*V63@siL6m$Lrw7LqF=hD_Yl%86MjR#K0Gt5E-tTx^VNexYTgz zf=u-?MzuDcoN@}0-X7El=_Yx#y0>570{2T(vOc;= z>2l%dyw>8E8u>J{awD4Pp4{UC?Z2$THC|2ewC$#pqA9VF1g;kC1R@pHDTdTgO859} zk3wCAq@YX2cz^?vIUUKkY`VBagv^E9m#~V$a_53kw=g`*lme-QSLxuZ0Aw4aBR|0mP;^f-So3+g?i2H zZ1dQS^y}Jn7vn&|ccs4nlzd_4>iYYA zgu0>%t{UF0vUNzFr5ApAE`1pNUOoH!FiiHIU^q#bCi)s@UI#WyXPX96!@eG!Nh$M% z@5YNYV^ZucW^P@bIm=E@dt6Im@NBpo_xXPaZZvOX9xRZzx4(ps?Q}zO2!qe-NR%}s zLnZ}Lyw^tBh)pzBAj74|lvVF7j7AMk#+PmUNKGNsZuRXQ%M9ykWxWmn1-zxtrR7SC zB#dtB3a_#%Ex&H$f)eeS#Evg~J!BI1^---QP(Wh{Sz@C_1B;hunlr!N#@dj=m&wY( zbB>k&*ce{RX^H_C7@5C2+~|?!XI`a>MRP}aAs~{&Qf0u;3i+S!xnIvnRYc~ROIyR| zJ%#7qg~}UGf!-^_+Gm?lJ?RYUD)QVHZxt;}e1bT>?}zLqTS`b?TUjYtv~JC^z~KL| zH$lfvd~OvrmVLgvy9a+2Ez0ebh^0WttDyIEM-E?LJK@bBFIuq6xR{^-n%h7D>Y@NE zHqIT@>p&3En9JNJEM;Q8KxkOHr={i*W9~It$pfL8`AGyV6f+-9^kQ+66iIJASNvkM z6KUPZ)p6x-TOF3UCB?-?`bef#>=H zaa%SAf8_+}ofu~B7fHA+Y=OL{{Zy2ga&*Wz4vyOk(EPV zhE$MVmZsRnJrO?3;bBCcDv=*p2!8vRTGU=){fvvE0sVz(d)6`i$a8FR z@50l@DVJgX$Z3pVl{ws~zC}964Wg5=EO()1$5sWPdtF0`NId#|je>`PSb@7rXQ z+!lm+ESuZDr2{-EI@ifv>b^eES;qO1#uO6OH|L>@EYqHK;bxiZ(3H9zNaoO)X7#vD zOP8or0wWk0j5A>?Y9>+imvbShxn^j7n-7daPbi>6ge@qjsi8a5;l{*Z5^IRYrsb{T(Vkg-x9FT^v~#I= zuRvH?@80p=qxOBiat~!`<&^f`x_a~M)O2~}lwCt|#LK&rrHuZeRq30yZ-^@`SNnkd zyX>2`!`bF}W@7&N@O^@$8UB9TA){)S ziiCNupn*pYh@+YOpu(dT`MyxDrME840|uFzpA5K{ijO_$@Q?W=1h|&+j+N<>4nIna zOk8v^`b&XMvQ{l~VwIzsHO~cYc@Tr$>=z0EPSLvnnQyjah_83Qczqb4=pFE1qW6#H zD~q@a4(5(7U2rn-9>x{*P$xBcLS23oFz}QP^O%f|8z~aEg*Wt6^gUr#OU#domtvrp zfls}`hBnda(Je-aFEv*8Opk|Ioe1q;hO;~10Xa>|k9v#lVxumO4Y=B#fd;wTB$}%5 z07*SLV;6Q&)o~wGXuap80&>*B`gGt%HPFtgw{Bm_Yqx>b(Kjoj#~V_%vl~89PO9LO zx?LIUAK2slgqr69;K*y5D5rQ!ktnC>3xXu4sf*Ht^Fs%Tn#(~Fe`!#)-OJ`4lZlHR z5`RgM6Uv<+*KIPcU7Lo-_51oZs#R8s-rxn6RWgbnDB-BYIPqA3oY(^te%94j6m-Fv zc*g;;h_8jHSA8Yg*jCLbL~G(;qho_+*3tCvjwoX>Ddn>|GX14=6TRMUK38Om;PZ5~vCVy2{} zL5+iQ(V6=6z3}?Z*Twan88DSqYNTBaed@8@N;5{NChuWl-aBk2HVI&F*i&LlIHqm{LsnUXfz2PB201bCMiM-fXSnXi(p@W zz+*}sm@5vIW-Ak&T$3SaTuf@Zh}4;tZ&enEQsoyF8k-2_9ja4y2tyK&as}`wMf_xIu`fEz9Kkwk%6;NM3kqmC3f7gD_{dt#{n{kT~QQSEE=|3 zh$!roWYH8OD~MO8V4#^z(WHdKX?8EJYLVp;uNlJU)5L)qZU;S`KD2<5^FGshrWzjlWKLF^ zd2n4+Bw0bP(J7KJ%DB5MCF`xA@Y;8s3v$+1NhU$C(MZ;;mlQXB-FK@q*pwl%j}lzG zl(NiZp(k1Z2V%@P=$g{7&sBY4j1$6n5zNmccv^38I&K)-SHAlnWe^Ld_ae%@ z8tlGX|4_-7o*?6FkzUv?-Bc^#wu&CyO!yiHj-q3S6PxDzI7%mctN$hUt zyXOU$1{g`Z%$8uG#}dKZ?6fL!l>Rf9bs^f*E?}?Jjx+k`K6iv2p1Xa@4|FvWG)&AXMy z2xJ9=a!E6nU5=oAi(G#ZQdifIKy z(seQx8VW0{YfIT;Z_(3uQa=vD+*Dcx>ZpW$zwr&KH3m6=cLmyYj*FJWSWQHH)r!?f z=BA5=GzJV6KB}*%GlG@0(qfPbG>e`d$heV-9lr8k*D4UGc%_Q^I@&ErIrvT1hTgNv zi*M9st4MJoLY9GAXyPzP5>bfGrD3eEhbjv<%XrARd(88WO1a70dkU7km;px?$QN3+ zc0hl*mDQ-TPU4%m!v&&icra(zlCHRnMtKqm;f*0+o-O!Vzwh9+5Zfv|1skfjbf3M% z64r5YtcHYgP>oU;qZF<>rA2z|mV_0m3rgB2a5>CDSyJUrqK}r|syYFaqMQQnb}fwcYmBdE8ez zrD(UVGsBi`BHZpMF6rK?=}&c8&T?tTd`X~xIt6*_olY5?vea!Fycqu=0z2u+TD1yhEz+v}n8;%ZyKhTu30Ymg z&U=SMS2#lMbJ-V6b*s^?#ffRElMUUW99{r)RbQ5jTD{Wuq*rF}G4cfqvDh;(>Siln zHSB7eCr^re^s3>zYR#e>efVGq(YB{r);dY+YV-)!J@QnEjpvwQl`aFM;#LBSnU!sm zY6v2UoJy?~H}s4EyG45(3un13%x2#l=S+}#7u42fm zV!h-KRoG)2x^wT7Cdz!=YibQvGXl*nM%Ff?rLJBcuF?>cd<8FEp*^}ZHsQ+T!X>pa z=h~jAE-n(x@ZbrTAX%EflsTt<8Zo|s9(H+kSr8Z)$Zd9VrL~1Yoc@$DgbR!SG#ZX` z_N=kSvMdgcTVOrG-|3N9%AktV<(S0l;*+^k=XF`d8h~0*--~Zug^fRhQ)3p(NQhLF z&7hT)W11MovB(!?$9jWsU508c_{Lq;4)=5SWN)r%p&f1^LC`KtTU${-iO{=80-Grx zu<*FfTG^(<1Y?yHiL$ZxfrWM~=r?|>8<$0*(^cQDqS&Bn_xgSB3e_tj+p(!u;M)Ag z^k=aa5KLm|y_wUkuMm7{-OZ8WrWtwmPNIkVl%F0f54o^Zg6uijrx zyb{v?cw83HXGBj9eP|3s-8Mhv+r;%U2%{Txf*lA6JBT-d8;cRoI&Mubj?2OvQyBvg zCv29Sa%QcKYM`-pYIA7&zrl;yBboyxzB6*_?LT@_wi6TQ5iDfsG9WBr9-Z*cJgi5^ z`Hf+AtqX}~3WkC4~mltC`aZRL#zB0t>&8Y z^%~$1i<#;4CT8LSBgQxEm;*bC7i$eRt{2qCk2?W}`b=vz4}P|l#LLcxUn==CL>c## z`G$j3BTJzjrr4x}os{-wUXnd`*8GXU-xN?Sujy#YXwXo#c^AP?P8wxGRNH@r)HDmYsiOQEn_M5f7ax9V_SY@ zN>FS|kU9-Dp}*@-z@CNvjD=Ko|6>>wJ6kay|d3Epb8F@Z&T51bIvHT~l z2~NL45o^6qcJOa`#AczQ9D_gc6m_J_`xg;Tn6YSY(R}jp)>IqbcfJa>QKo%6*Oi~X zb8a18J_(~C$nMZ_Yd}%%fHifFrgoEbn@m&Q4m-hQn-8!Evon?zp=i>y0H#|9q$8;& z5#8-ZrPZt(eEKSmYeP45X&AOG6t>;PpgUoMV0uHPs96m&ZC}t?4)YdXQZoU$!j5QK z;?1{M_=;(mWXu`?=seV#>j3;d;Mpj%ZZ+)mBL-caR1!nm&hnkZNzC#5XPWK^tVOR$ezu{cmj-Au6YuM`>#2-LW%z_%|M{GY{)|nq zfs;t(Xg&WafzR-i!`yl!h2&>fs3056?|a4pi?Z%Jr2k{BfOa*qIL6o2^|M++qyFu#O5Eve8YJ3i=z-Y9TJmzZ8fx0!Lia` z8{smHP%F@Lu`ykH zf*a|54v?|x^`eoi4kfpVY|ixP))QZ~G*5(R8%5G11$8%a-H65SZqa6|ph2?X0Q?e+ zLFg3g(APLsp$3akV!V_QWa2DG(z;zK>a^ABEc5)mTwp`>r98?c(i(4Ywq?Fu(w$g< zC}-5Of=6(YO?u}|oRWB;Q|S*--IdOr1p@w>4(x(pIV-KctPJhGtSf(p)ixlNAO5Y6 z;Imu%nyTt{e~R9Zw(FK&9-mKl`LaAmHlwphhD$K-M7_O;8BOyApR_x5AnzBxyyS87 zAherkc1iWM5ne>aB+qB3`O?xddV1UxXWVA|0{}j?c^tPhL}k2k&X)BO#xmEzyMMJn zQ_omEKKEoE`dv%mIv*ISW?}*#vIcr4jLSAb!oxZ3$CDV;G#+r_?BSleN}RougNGc zeyE8Gzp44*H#4_{>_3)Mh!?PLg9L##4dmZcWq73IfFYSY%Yo!~73MiIo16+4O`huJz>O~BKG4rPLy-#QCgaGXN zkHKW$`-$95?_xFy@`{-|H~YJcRdz7SvOCU6-ceJ<40QL&IoC0;$L4Ej0@4kiQ=y_g zVjymC{ITVJC1%BOhCZ{msO|1MMEU6kx&PEVxK6ZS`wF$ZjgVu`3!ddNR;G zB@M*enDxVc$6+vHaioB&>TM&@diU7v&K5uUSWA~_x6J?tx(qvpm1$-k(>l!)KVr}B zcIcf!=W?B~J50l1gXV%dgNhD~xd*CnzcDUYw=RoTtLRnOtP%9ZgvMdD;&f=Q ztx9O``dmdym@)Mw#n%ras8@+~W{1At>qSv7pix)a0^_$$=ySeF8C?);clwtE8;GHL zBaA*H+*e4lhF;v&wDH5>q3!h_sO(%6e32c~=`BPawByD4;=tL9)$Tx57;#>ZYqWch z)G>4;{`_nUmn|_I(egXZUgeqtKAq0&_8zV{g7Y2u8eD7V=dqI(wH@1>{^D!f1l+?&`7;4Ouv$u4e`c zL8&>GJQYI3$bt1u?RQ;CMd@rxQFOO9!t6~CO(x~9i5+BD>GrnTVS0>2!gPkulc=tE zhVOx=9(jh(FLvtA+Di{T<8GLTc>QX70OdkPg=2Y?s+aghdO`F1>oCuDC7Od+LdP3c zTOXaX`6f@Dq$aUGgX zT;wJJ@fqsP1msIiV3pFCJF-A#t_s~IC-UVBph!yY4z8OdP$Vb!QCvK!Y=2TNl~0c! zr_)#BW)^XizLR*c1?XeM!mHNt$qZRS#le$qq4_k;E=R6i3q_t?a<`EbA>O&6l9rwE zJeygQeJKaUd=#>n;H~2R-N0>Hu{f}*J@oKOI9Cjv8a$yo{&Pq4@-_k1kUE9jCWd;L zz%uMWTkeHhX0Ho@Y;lLfYNf+6kBdp3Y{d;1hO8KCUNnlKwfaz|P0Gz>l&btLLnYb9 z5J%8!_6Gf?l$|YVwtQLdr<8X%m-uW&qQ@LQ$QQRHj8PE zK2;rd<+Z8W$oBBtyTHg;lpE@y>h_Ua5z z?gfTlSA8Tp7c`}9kkN}LgiiS|w^+N+IfvIOom-TfLT}v25jXjMP$D93VJ}I?oHz!D zVV!=F9d${){d9Bc*%n~DVN>3jfm+o-Z!B%##SvnLUEC zh`~4b70X@5S)?Jf2`_vUO>q2hW7)Tm&W25YC{;yh7(7_Ws6S-8m+|}0!S5f)P;kE= zVL|PQ-ybSTYl^eTE6cFm4nY0I6bVWZV%o&;h0F-@B|FFu-u<_K-wFfo#fSeiRT5X0 zmyy=cWL1*+tMhxp9~SrfKU_mW-8*lB{N+B;=-(`UE5yC+y~V=s7Qdw?{nhSaF@MH- zZwCkc>y)N{BM{#^{m*t!;Q0rF8)OLWF9rCm(w`TzpOgZQBR0}k0222!K`nlB9iI059$94%g00)8Dc zElQg2ID|m~sQ`w*F?Ar7^s98UXSR-({~GFjljmKJ*N8)Ex)zeR_T+C^C-i^7I$2x) zD#&}9pS+H|0VKdsNWuS3+sFMiO-WM1(#-MKMNRrgfyM4(?9rHE{*`&Li|ue!sh&&uLFp5=dY^w&%C~e zzW#q@Q{?`-l9{fq>bzh3_YMr0ZYE_W?hc z(DyLrhtoxVl3V)!h5YA;`NPR05BU$r;D7RahyEA-AN2tbxw=C^J%YRjAlK3<(EV@! E2lMmuBLDyZ literal 0 HcmV?d00001 diff --git a/Open-ILS/src/Android/opensrf/libs/json-20090211.jar b/Open-ILS/src/Android/opensrf/libs/json-20090211.jar new file mode 100644 index 0000000000000000000000000000000000000000..ef29094093d39ecf5a50c24fc61f10c8c5b6b195 GIT binary patch literal 45944 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 literal 0 HcmV?d00001 diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/ClientSession.java b/Open-ILS/src/Android/opensrf/org/opensrf/ClientSession.java new file mode 100644 index 0000000000..3ed4908296 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/ClientSession.java @@ -0,0 +1,175 @@ +package org.opensrf; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.ArrayList; +import java.util.Random; +import java.util.Arrays; + +import org.opensrf.util.*; +import org.opensrf.net.xmpp.*; + + +/** + * Models an OpenSRF client session. + */ +public class ClientSession extends Session { + + /** The remote service to communicate with */ + private String service; + /** OpenSRF domain */ + private String domain; + /** Router name */ + private String router; + + /** + * original remote node. The current remote node will change based + * on server responses. This is used to reset the remote node to + * its original state. + */ + private String origRemoteNode; + /** The next request id */ + private int nextId; + /** The requests this session has sent */ + private Map requests; + + /** + * Creates a new client session. Initializes the + * @param service The remote service. + */ + public ClientSession(String service) throws ConfigException { + this(service, null); + } + + /** + * Creates a new client session. Initializes the + * @param service The remote service. + * @param locale The locale for this session. + */ + public ClientSession(String service, String locale) throws ConfigException { + this.service = service; + if(locale != null) + setLocale(locale); + + /** generate the remote node string */ + domain = (String) Config.global().getFirst("/domain"); + router = Config.global().getString("/router_name"); + setRemoteNode(router + "@" + domain + "/" + service); + origRemoteNode = getRemoteNode(); + + + /** create a random thread */ + long time = new Date().getTime(); + Random rand = new Random(time); + setThread(rand.nextInt()+""+rand.nextInt()+""+time+Thread.currentThread().getId()); + + nextId = 0; + requests = new HashMap(); + cacheSession(); + } + + /** + * Creates a new request to send to our remote service. + * @param method The method API name + * @param params The list of method parameters + * @return The request object. + */ + public Request request(String method, List params) throws SessionException { + return request(new Request(this, nextId++, method, params)); + } + + /** + * Creates a new request to send to our remote service. + * @param method The method API name + * @param params The list of method parameters + * @return The request object. + */ + public Request request(String method, Object[] params) throws SessionException { + return request(new Request(this, nextId++, method, Arrays.asList(params))); + } + + + /** + * Creates a new request to send to our remote service. + * @param method The method API name + * @return The request object. + */ + public Request request(String method) throws SessionException { + return request(new Request(this, nextId++, method)); + } + + + private Request request(Request req) throws SessionException { + if(getConnectState() != ConnectState.CONNECTED) + resetRemoteId(); + requests.put(new Integer(req.getId()), req); + req.send(); + return req; + } + + + /** + * Resets the remoteNode to its original state. + */ + public void resetRemoteId() { + setRemoteNode(origRemoteNode); + } + + + /** + * Pushes a response onto the result queue of the appropriate request. + * @param msg The received RESULT Message + */ + public void pushResponse(Message msg) { + + Request req = findRequest(msg.getId()); + if(req == null) { + /** LOG that we've received a result to a non-existant request */ + System.err.println(msg.getId() +" has no corresponding request"); + return; + } + OSRFObject payload = (OSRFObject) msg.get("payload"); + + /** build a result and push it onto the request's result queue */ + req.pushResponse( + new Result( + payload.getString("status"), + payload.getInt("statusCode"), + payload.get("content") + ) + ); + } + + public Request findRequest(int reqId) { + return requests.get(new Integer(reqId)); + } + + /** + * Removes a request for this session's request set + */ + public void cleanupRequest(int reqId) { + requests.remove(new Integer(reqId)); + } + + public void setRequestComplete(int reqId) { + Request req = findRequest(reqId); + if(req == null) return; + req.setComplete(); + } + + public static Object atomicRequest(String service, String method, Object[] params) throws MethodException { + try { + ClientSession session = new ClientSession(service); + Request osrfRequest = session.request(method, params); + Result result = osrfRequest.recv(600000); + if(result.getStatusCode() != 200) + throw new MethodException( + "Request "+service+":"+method+":"+" failed with status code " + result.getStatusCode()); + return result.getContent(); + } catch(Exception e) { + throw new MethodException(e); + } + } +} + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/Message.java b/Open-ILS/src/Android/opensrf/org/opensrf/Message.java new file mode 100644 index 0000000000..6bfe1eaaa7 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/Message.java @@ -0,0 +1,110 @@ +package org.opensrf; +import org.opensrf.util.*; + + +public class Message implements OSRFSerializable { + + /** Message types */ + public static final String REQUEST = "REQUEST"; + public static final String STATUS = "STATUS"; + public static final String RESULT = "RESULT"; + public static final String CONNECT = "CONNECT"; + public static final String DISCONNECT = "DISCONNECT"; + + /** Message ID. This number is used to relate requests to responses */ + private int id; + /** type of message. */ + private String type; + /** message payload */ + private Object payload; + /** message locale */ + private String locale; + + /** Create a registry for the osrfMessage object */ + private static OSRFRegistry registry = + OSRFRegistry.registerObject( + "osrfMessage", + OSRFRegistry.WireProtocol.HASH, + new String[] {"threadTrace", "type", "payload", "locale"}); + + /** + * @param id This message's ID + * @param type The type of message + */ + public Message(int id, String type) { + setId(id); + setString(type); + } + + /** + * @param id This message's ID + * @param type The type of message + * @param payload The message payload + */ + public Message(int id, String type, Object payload) { + this(id, type); + setPayload(payload); + } + + /** + * @param id This message's ID + * @param type The type of message + * @param payload The message payload + * @param locale The message locale + */ + public Message(int id, String type, Object payload, String locale) { + this(id, type, payload); + setPayload(payload); + setLocale(locale); + } + + + public int getId() { + return id; + } + public String getType() { + return type; + } + public Object getPayload() { + return payload; + } + public String getLocale() { + return locale; + } + public void setId(int id) { + this.id = id; + } + public void setString(String type) { + this.type = type; + } + public void setPayload(Object p) { + payload = p; + } + public void setLocale(String l) { + locale = l; + } + + /** + * Implements the generic get() API required by OSRFSerializable + */ + public Object get(String field) { + if("threadTrace".equals(field)) + return getId(); + if("type".equals(field)) + return getType().toString(); + if("payload".equals(field)) + return getPayload(); + if("locale".equals(field)) + return getLocale(); + return null; + } + + /** + * @return The osrfMessage registry. + */ + public OSRFRegistry getRegistry() { + return registry; + } +} + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/Method.java b/Open-ILS/src/Android/opensrf/org/opensrf/Method.java new file mode 100644 index 0000000000..b708d4fa39 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/Method.java @@ -0,0 +1,78 @@ +package org.opensrf; +import java.util.List; +import java.util.ArrayList; +import org.opensrf.util.*; + + +public class Method extends OSRFObject { + + /** The method API name */ + private String name; + /** The ordered list of method params */ + private List params; + + /** Create a registry for the osrfMethod object */ + private static OSRFRegistry registry = + OSRFRegistry.registerObject( + "osrfMethod", + OSRFRegistry.WireProtocol.HASH, + new String[] {"method", "params"}); + + /** + * @param name The method API name + */ + public Method(String name) { + this.name = name; + this.params = new ArrayList(8); + } + + /** + * @param name The method API name + * @param params The ordered list of params + */ + public Method(String name, List params) { + this.name = name; + this.params = params; + } + + /** + * @return The method API name + */ + public String getName() { + return name; + } + /** + * @return The ordered list of params + */ + public List getParams() { + return params; + } + + /** + * Pushes a new param object onto the set of params + * @param p The new param to add to the method. + */ + public void addParam(Object p) { + this.params.add(p); + } + + /** + * Implements the generic get() API required by OSRFSerializable + */ + public Object get(String field) { + if("method".equals(field)) + return getName(); + if("params".equals(field)) + return getParams(); + return null; + } + + /** + * @return The osrfMethod registry. + */ + public OSRFRegistry getRegistry() { + return registry; + } + +} + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/MethodException.java b/Open-ILS/src/Android/opensrf/org/opensrf/MethodException.java new file mode 100644 index 0000000000..f87e638cf2 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/MethodException.java @@ -0,0 +1,14 @@ +package org.opensrf; + +/** + * Thrown when the server responds with a method exception. + */ +public class MethodException extends Exception { + public MethodException(String info) { + super(info); + } + public MethodException(Throwable cause) { + super(cause); + } +} + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/MultiSession.java b/Open-ILS/src/Android/opensrf/org/opensrf/MultiSession.java new file mode 100644 index 0000000000..a312aff9b8 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/MultiSession.java @@ -0,0 +1,123 @@ +package org.opensrf; +import java.util.List; +import java.util.ArrayList; +import org.opensrf.util.ConfigException; + +public class MultiSession { + + class RequestContainer { + Request request; + int id; + RequestContainer(Request r) { + request = r; + } + } + + private boolean complete; + private List requests; + private int lastId; + + public MultiSession() { + requests = new ArrayList(); + } + + public boolean isComplete() { + return complete; + } + + public int lastId() { + return lastId; + } + + /** + * Adds a new request to the set of requests. + * @param service The OpenSRF service + * @param method The OpenSRF method + * @param params The array of method params + * @return The request ID, which is used to map results from recv() to the original request. + */ + public int request(String service, String method, Object[] params) throws SessionException, ConfigException { + ClientSession ses = new ClientSession(service); + return request(ses.request(method, params)); + } + + + public int request(String service, String method) throws SessionException, ConfigException { + ClientSession ses = new ClientSession(service); + return request(ses.request(method)); + } + + private int request(Request req) { + RequestContainer c = new RequestContainer(req); + c.id = requests.size(); + requests.add(c); + return c.id; + } + + + /** + * Calls recv on all pending requests until there is data to return. The ID which + * maps the received object to the request can be retrieved by calling lastId(). + * @param millis Number of milliseconds to wait for some data to arrive. + * @return The object result or null if all requests are complete + * @throws MethodException Thrown if no response is received within + * the given timeout or the method fails. + */ + public Object recv(int millis) throws MethodException { + if(complete) return null; + + Request req = null; + Result res = null; + RequestContainer cont = null; + + long duration = 0; + long blockTime = 100; + + /* if there is only 1 outstanding request, don't poll */ + if(requests.size() == 1) + blockTime = millis; + + while(true) { + for(int i = 0; i < requests.size(); i++) { + + cont = requests.get(i); + req = cont.request; + + try { + if(i == 0) { + res = req.recv(blockTime); + } else { + res = req.recv(0); + } + } catch(SessionException e) { + throw new MethodException(e); + } + + if(res != null) break; + } + + if(res != null) break; + duration += blockTime; + + if(duration >= millis) { + System.out.println("duration = " + duration + " millis = " + millis); + throw new MethodException("No request received within " + millis + " milliseconds"); + } + } + + if(res.getStatusCode() != 200) { + throw new MethodException("Request " + cont.id + " failed with status code " + + res.getStatusCode() + " and status message " + res.getStatus()); + } + + if(req.isComplete()) + requests.remove(requests.indexOf(cont)); + + if(requests.size() == 0) + complete = true; + + lastId = cont.id; + return res.getContent(); + } +} + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/Request.java b/Open-ILS/src/Android/opensrf/org/opensrf/Request.java new file mode 100644 index 0000000000..2d72e2df65 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/Request.java @@ -0,0 +1,138 @@ +package org.opensrf; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.List; +import java.util.Date; +import org.opensrf.net.xmpp.XMPPException; +import org.opensrf.util.Logger; + +public class Request { + + /** This request's controlling session */ + private ClientSession session; + /** The method */ + private Method method; + /** The ID of this request */ + private int id; + /** Queue of Results */ + private Queue resultQueue; + /** If true, the receive timeout for this request should be reset */ + private boolean resetTimeout; + + /** If true, the server has indicated that this request is complete. */ + private boolean complete; + + /** + * @param ses The controlling session for this request. + * @param id This request's ID. + * @param method The requested method. + */ + public Request(ClientSession ses, int id, Method method) { + this.session = ses; + this.id = id; + this.method = method; + resultQueue = new ConcurrentLinkedQueue(); + complete = false; + resetTimeout = false; + } + + /** + * @param ses The controlling session for this request. + * @param id This request's ID. + * @param methodName The requested method's API name. + */ + public Request(ClientSession ses, int id, String methodName) { + this(ses, id, new Method(methodName)); + } + + /** + * @param ses The controlling session for this request. + * @param id This request's ID. + * @param methodName The requested method's API name. + * @param params The list of request params + */ + public Request(ClientSession ses, int id, String methodName, List params) { + this(ses, id, new Method(methodName, params)); + } + + /** + * Sends the request to the server. + */ + public void send() throws SessionException { + session.send(new Message(id, Message.REQUEST, method, session.getLocale())); + } + + /** + * Receives the next result for this request. This method + * will wait up to the specified number of milliseconds for + * a response. + * @param millis Number of milliseconds to wait for a result. If + * negative, this method will wait indefinitely. + * @return The result or null if none arrives in time + */ + public Result recv(long millis) throws SessionException, MethodException { + + Result result = null; + + if((result = resultQueue.poll()) != null) + return result; + + if(millis < 0 && !complete) { + /** wait potentially forever for a result to arrive */ + while(!complete) { + session.waitForMessage(millis); + if((result = resultQueue.poll()) != null) + return result; + } + + } else { + + while(millis >= 0 && !complete) { + + /** wait up to millis milliseconds for a result. waitForMessage() + * will return if a response to any request arrives, so we keep track + * of how long we've been waiting in total for a response to + * this request */ + + long start = new Date().getTime(); + session.waitForMessage(millis); + millis -= new Date().getTime() - start; + if((result = resultQueue.poll()) != null) + return result; + } + } + + return null; + } + + /** + * Pushes a result onto the result queue + * @param result The result to push + */ + public void pushResponse(Result result) { + resultQueue.offer(result); + } + + /** + * @return This request's ID + */ + public int getId() { + return id; + } + + /** + * Removes this request from the controlling session's request set + */ + public void cleanup() { + session.cleanupRequest(id); + } + + /** Sets this request as complete */ + public void setComplete() { + complete = true; + } + + public boolean isComplete() { + return complete; + } +} diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/Result.java b/Open-ILS/src/Android/opensrf/org/opensrf/Result.java new file mode 100644 index 0000000000..80b71dd138 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/Result.java @@ -0,0 +1,106 @@ +package org.opensrf; +import org.opensrf.util.*; + + +/** + * Models a single result from a method request. + */ +public class Result implements OSRFSerializable { + + /** Method result content */ + private Object content; + /** Name of the status */ + private String status; + /** Status code number */ + private int statusCode; + + + /** Register this object */ + private static OSRFRegistry registry = + OSRFRegistry.registerObject( + "osrfResult", + OSRFRegistry.WireProtocol.HASH, + new String[] {"status", "statusCode", "content"}); + + + /** + * @param status The status message for this result + * @param statusCode The status code + * @param content The content of the result + */ + public Result(String status, int statusCode, Object content) { + this.status = status; + this.statusCode = statusCode; + this.content = content; + } + + /** + * Get status. + * @return status as String. + */ + public String getStatus() { + return status; + } + + /** + * Set status. + * @param status the value to set. + */ + public void setStatus(String status) { + this.status = status; + } + + /** + * Get statusCode. + * @return statusCode as int. + */ + public int getStatusCode() { + return statusCode; + } + + /** + * Set statusCode. + * @param statusCode the value to set. + */ + public void setStatusCode(int statusCode) { + this.statusCode = statusCode; + } + + /** + * Get content. + * @return content as Object. + */ + public Object getContent() { + return content; + } + + /** + * Set content. + * @param content the value to set. + */ + public void setContent(Object content) { + this.content = content; + } + + /** + * Implements the generic get() API required by OSRFSerializable + */ + public Object get(String field) { + if("status".equals(field)) + return getStatus(); + if("statusCode".equals(field)) + return getStatusCode(); + if("content".equals(field)) + return getContent(); + return null; + } + + /** + * @return The osrfMethod registry. + */ + public OSRFRegistry getRegistry() { + return registry; + } + +} + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/ServerSession.java b/Open-ILS/src/Android/opensrf/org/opensrf/ServerSession.java new file mode 100644 index 0000000000..62e5133058 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/ServerSession.java @@ -0,0 +1,8 @@ +package org.opensrf; + +/** + * Models an OpenSRF server session. + */ +public class ServerSession extends Session { +} + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/Session.java b/Open-ILS/src/Android/opensrf/org/opensrf/Session.java new file mode 100644 index 0000000000..15fd352d5f --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/Session.java @@ -0,0 +1,180 @@ +package org.opensrf; +import org.opensrf.util.JSONWriter; +import org.opensrf.net.xmpp.*; +import java.util.Map; +import java.util.HashMap; +import java.util.Arrays; + +public abstract class Session { + + /** Represents the different connection states for a session */ + public enum ConnectState { + DISCONNECTED, + CONNECTING, + CONNECTED + }; + + /** local cache of existing sessions */ + private static Map + sessionCache = new HashMap(); + + /** the current connection state */ + private ConnectState connectState; + + /** The address of the remote party we are communicating with */ + private String remoteNode; + + /** Session locale */ + protected String locale; + /** Default session locale */ + protected static String defaultLocale = "en-US"; + + /** + * The thread is used to link messages to a given session. + * In other words, each session has a unique thread, and all messages + * in that session will carry this thread around as an indicator. + */ + private String thread; + + public Session() { + connectState = ConnectState.DISCONNECTED; + } + + /** + * Sends a Message to our remoteNode. + */ + public void send(Message omsg) throws SessionException { + + /** construct the XMPP message */ + XMPPMessage xmsg = new XMPPMessage(); + xmsg.setTo(remoteNode); + xmsg.setThread(thread); + xmsg.setBody(new JSONWriter(Arrays.asList(new Message[] {omsg})).write()); + + try { + XMPPSession.getThreadSession().send(xmsg); + } catch(XMPPException e) { + connectState = ConnectState.DISCONNECTED; + throw new SessionException("Error sending message to " + remoteNode, e); + } + } + + /** + * Waits for a message to arrive over the network and passes + * all received messages to the stack for processing + * @param millis The number of milliseconds to wait for a message to arrive + */ + public static void waitForMessage(long millis) throws SessionException, MethodException { + try { + Stack.processXMPPMessage( + XMPPSession.getThreadSession().recv(millis)); + } catch(XMPPException e) { + throw new SessionException("Error waiting for message", e); + } + } + + /** + * Removes this session from the session cache. + */ + public void cleanup() { + sessionCache.remove(thread); + } + + /** + * Searches for the cached session with the given thread. + * @param thread The session thread. + * @return The found session or null. + */ + public static Session findCachedSession(String thread) { + return sessionCache.get(thread); + } + + /** + * Puts this session into session cache. + */ + protected void cacheSession() { + sessionCache.put(thread, this); + } + + /** + * Sets the remote address + * @param nodeName The name of the remote node. + */ + public void setRemoteNode(String nodeName) { + remoteNode = nodeName; + } + /** + * @return The remote node + */ + public String getRemoteNode() { + return remoteNode; + } + + + /** + * Get thread. + * @return thread as String. + */ + public String getThread() { + return thread; + } + + /** + * Set thread. + * @param thread the value to set. + */ + public void setThread(String thread) { + this.thread = thread; + } + + /** + * Get locale. + * @return locale as String. + */ + public String getLocale() { + if(locale == null) + return defaultLocale; + return locale; + } + + /** + * Set locale. + * @param locale the value to set. + */ + public void setLocale(String locale) { + this.locale = locale; + } + + /** + * Get defaultLocale. + * @return defaultLocale as String. + */ + public String getDefaultLocale() { + return defaultLocale; + } + + /** + * Set defaultLocale. + * @param defaultLocale the value to set. + */ + public void setDefaultLocale(String defaultLocale) { + this.defaultLocale = defaultLocale; + } + + + /** + * Get connectState. + * @return connectState as ConnectState. + */ + public ConnectState getConnectState() { + return connectState; + } + + /** + * Set connectState. + * @param connectState the value to set. + */ + public void setConnectState(ConnectState connectState) { + this.connectState = connectState; + } +} diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/SessionException.java b/Open-ILS/src/Android/opensrf/org/opensrf/SessionException.java new file mode 100644 index 0000000000..bd90a7610d --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/SessionException.java @@ -0,0 +1,13 @@ +package org.opensrf; +/** + * Used by sessions to indicate communication errors + */ +public class SessionException extends Exception { + public SessionException(String info) { + super(info); + } + public SessionException(String info, Throwable cause) { + super(info, cause); + } +} + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/Stack.java b/Open-ILS/src/Android/opensrf/org/opensrf/Stack.java new file mode 100644 index 0000000000..3e7e6061fb --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/Stack.java @@ -0,0 +1,105 @@ +package org.opensrf; +import org.opensrf.net.xmpp.XMPPMessage; +import org.opensrf.util.*; +import java.util.Date; +import java.util.List; +import java.util.Iterator; + + +public class Stack { + + public static void processXMPPMessage(XMPPMessage msg) throws MethodException { + + if(msg == null) return; + + //System.out.println(msg.getBody()); + + /** fetch this session from the cache */ + Session ses = Session.findCachedSession(msg.getThread()); + + if(ses == null) { + /** inbound client request, create a new server session */ + return; + } + + /** parse the JSON message body, which should result in a list of OpenSRF messages */ + List msgList; + + try { + msgList = new JSONReader(msg.getBody()).readArray(); + } catch(JSONException e) { + /** XXX LOG error */ + e.printStackTrace(); + return; + } + + Iterator itr = msgList.iterator(); + + OSRFObject obj = null; + long start = new Date().getTime(); + + /** cycle through the messages and push them up the stack */ + while(itr.hasNext()) { + + /** Construct a Message object from the parsed generic OSRFObject */ + obj = (OSRFObject) itr.next(); + + processOSRFMessage( + ses, + new Message( + obj.getInt("threadTrace"), + obj.getString("type"), + obj.get("payload") + ) + ); + } + + /** LOG the duration */ + } + + private static void processOSRFMessage(Session ses, Message msg) throws MethodException { + + Logger.debug("received id=" + msg.getId() + + " type=" + msg.getType() + " payload=" + msg.getPayload()); + + if( ses instanceof ClientSession ) + processResponse((ClientSession) ses, msg); + else + processRequest((ServerSession) ses, msg); + } + + /** + * Process a server response + */ + private static void processResponse(ClientSession session, Message msg) throws MethodException { + String type = msg.getType(); + + if(msg.RESULT.equals(type)) { + session.pushResponse(msg); + return; + } + + if(msg.STATUS.equals(type)) { + + OSRFObject obj = (OSRFObject) msg.getPayload(); + Status stat = new Status(obj.getString("status"), obj.getInt("statusCode")); + int statusCode = stat.getStatusCode(); + String status = stat.getStatus(); + + switch(statusCode) { + case Status.COMPLETE: + session.setRequestComplete(msg.getId()); + break; + case Status.NOTFOUND: + session.setRequestComplete(msg.getId()); + throw new MethodException(status); + } + } + } + + /** + * Process a client request + */ + private static void processRequest(ServerSession session, Message msg) { + } +} diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/Status.java b/Open-ILS/src/Android/opensrf/org/opensrf/Status.java new file mode 100644 index 0000000000..8026c7b8cd --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/Status.java @@ -0,0 +1,63 @@ +package org.opensrf; +import org.opensrf.util.*; + +public class Status { + + public static final int CONTINUE = 100; + public static final int OK = 200; + public static final int ACCEPTED = 202; + public static final int COMPLETE = 205; + public static final int REDIRECTED = 307; + public static final int EST = 400; + public static final int STATUS_UNAUTHORIZED = 401; + public static final int FORBIDDEN = 403; + public static final int NOTFOUND = 404; + public static final int NOTALLOWED = 405; + public static final int TIMEOUT = 408; + public static final int EXPFAILED = 417; + public static final int INTERNALSERVERERROR = 500; + public static final int NOTIMPLEMENTED = 501; + public static final int VERSIONNOTSUPPORTED = 505; + + private OSRFRegistry registry = OSRFRegistry.registerObject( + "osrfConnectStatus", + OSRFRegistry.WireProtocol.HASH, + new String[] {"status", "statusCode"}); + + /** The name of the status */ + String status; + /** The status code */ + int statusCode; + + public Status(String status, int statusCode) { + this.status = status; + this.statusCode = statusCode; + } + + public int getStatusCode() { + return statusCode; + } + public String getStatus() { + return status; + } + + /** + * Implements the generic get() API required by OSRFSerializable + */ + public Object get(String field) { + if("status".equals(field)) + return getStatus(); + if("statusCode".equals(field)) + return new Integer(getStatusCode()); + return null; + } + + /** + * @return The osrfMessage registry. + */ + public OSRFRegistry getRegistry() { + return registry; + } +} + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/Sys.java b/Open-ILS/src/Android/opensrf/org/opensrf/Sys.java new file mode 100644 index 0000000000..d65f8a4ffa --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/Sys.java @@ -0,0 +1,86 @@ +package org.opensrf; + +import org.opensrf.util.*; +import org.opensrf.net.xmpp.*; +import java.util.Random; +import java.util.Date; +import java.net.InetAddress; + + +public class Sys { + + private static void initLogger(Config config) { + if(Logger.instance() == null) { + try { + String logFile = config.getString("/logfile"); + int logLevel = config.getInt("/loglevel"); + Logger.init( (short) config.getInt("/loglevel"), new FileLogger(logFile)); + /** add syslog support... */ + } catch(Exception e) { + /* by default, log to stderr at WARN level */ + Logger.init(Logger.WARN, new Logger()); + } + } + } + + /** + * Connects to the OpenSRF network so that client sessions may communicate. + * @param configFile The OpenSRF config file + * @param configContext Where in the XML document the config chunk lives. This + * allows an OpenSRF client config chunk to live in XML files where other config + * information lives. + */ + public static void bootstrapClient(String configFile, String configContext) + throws ConfigException, SessionException { + + + /** see if the current thread already has a connection */ + XMPPSession existing = XMPPSession.getThreadSession(); + if(existing != null && existing.connected()) + return; + + /** create the config parser */ + Config config = new Config(configContext); + config.parse(configFile); + Config.setConfig(config); /* set this as the global config */ + + initLogger(config); + + /** Collect the network connection info from the config */ + String username = config.getString("/username"); + String passwd = config.getString("/passwd"); + String host = (String) config.getFirst("/domain"); + int port = config.getInt("/port"); + + + /** Create a random login resource string */ + String res = "java_"; + try { + res += InetAddress.getLocalHost().getHostAddress(); + } catch(java.net.UnknownHostException e) {} + res += "_"+Math.abs(new Random(new Date().getTime()).nextInt()) + + "_t"+ Thread.currentThread().getId(); + + + + try { + + /** Connect to the Jabber network */ + Logger.info("attempting to create XMPP session "+username+"@"+host+"/"+res); + XMPPSession xses = new XMPPSession(host, port); + xses.connect(username, passwd, res); + XMPPSession.setThreadSession(xses); + + } catch(XMPPException e) { + throw new SessionException("Unable to bootstrap client", e); + } + } + + /** + * Shuts down the connection to the opensrf network + */ + public static void shutdown() { + XMPPSession.getThreadSession().disconnect(); + } +} + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/net/http/GatewayRequest.java b/Open-ILS/src/Android/opensrf/org/opensrf/net/http/GatewayRequest.java new file mode 100644 index 0000000000..ab3594612a --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/net/http/GatewayRequest.java @@ -0,0 +1,133 @@ +package org.opensrf.net.http; + +import org.opensrf.*; +import org.opensrf.util.*; + +import java.io.IOException; +import java.io.BufferedInputStream; +import java.io.OutputStreamWriter; +import java.io.InputStream; +import java.io.IOException; +import java.net.URL; +import java.net.URI; +import java.net.HttpURLConnection; +import java.lang.StringBuffer; +import java.util.List; +import java.util.Iterator; +import java.util.Map; +import java.util.HashMap; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class GatewayRequest extends HttpRequest { + + private boolean readComplete; + + public GatewayRequest(HttpConnection conn, String service, Method method) { + super(conn, service, method); + readComplete = false; + } + + public GatewayRequest send() { + try { + + String postData = compilePostData(service, method); + + urlConn = (HttpURLConnection) httpConn.url.openConnection(); + urlConn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + urlConn.setDoInput(true); + urlConn.setDoOutput(true); + + + + OutputStreamWriter wr = new OutputStreamWriter(urlConn.getOutputStream()); + wr.write(postData); + wr.flush(); + wr.close(); + + } catch (java.io.IOException ex) { + failed = true; + failure = ex; + } + + return this; + } + + public Object recv() { + + if (readComplete) + return nextResponse(); + + try { + + InputStream netStream = new BufferedInputStream(urlConn.getInputStream()); + StringBuffer readBuf = new StringBuffer(); + + int bytesRead = 0; + byte[] buffer = new byte[1024]; + + while ((bytesRead = netStream.read(buffer)) != -1) { + readBuf.append(new String(buffer, 0, bytesRead)); + } + + netStream.close(); + urlConn = null; + + Map result = null; + + System.out.println("Received " + readBuf.toString()); + try { + result = (Map) new JSONReader(readBuf.toString()).readObject(); + } catch (org.opensrf.util.JSONException ex) { + ex.printStackTrace(); + return null; + } + System.out.println("Converted object " + result); + String status = result.get("status").toString(); + if (!"200".equals(status)) { + failed = true; + // failure = + } + + // gateway always returns a wrapper array with the full results set + responseList = (List) result.get("payload"); + + // System.out.println("Response list : " + responseList); + } catch (java.io.IOException ex) { + failed = true; + failure = ex; + } + + readComplete = true; + return nextResponse(); + } + + private String compilePostData(String service, Method method) { + URI uri = null; + StringBuffer postData = new StringBuffer(); + + postData.append("service="); + postData.append(service); + postData.append("&method="); + postData.append(method.getName()); + + List params = method.getParams(); + Iterator itr = params.iterator(); + + while (itr.hasNext()) { + postData.append("¶m="); + postData.append(new JSONWriter(itr.next()).write()); + } + + try { + // not using URLEncoder because it replaces ' ' with '+'. + uri = new URI("http", "", null, postData.toString(), null); + } catch (java.net.URISyntaxException ex) { + ex.printStackTrace(); + } + + return uri.getRawQuery(); + } +} + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpConnection.java b/Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpConnection.java new file mode 100644 index 0000000000..32fdebc212 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpConnection.java @@ -0,0 +1,97 @@ +package org.opensrf.net.http; + +import java.net.URL; +import java.net.MalformedURLException; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import org.opensrf.*; +import org.opensrf.util.*; + + +/** + * Manages connection parameters and thread limiting for opensrf json gateway connections. + */ + +public class HttpConnection { + + /** Compiled URL object */ + protected URL url; + /** Number of threads currently communicating with the server */ + protected int activeThreads; + /** Queue of pending async requests */ + protected Queue pendingThreadQueue; + /** maximum number of actively communicating threads allowed */ + protected int maxThreads = 10; + + public HttpConnection(String fullUrl) throws java.net.MalformedURLException { + activeThreads = 0; + pendingThreadQueue = new ConcurrentLinkedQueue(); + url = new URL(fullUrl); + } + + public int getMaxThreads() { + return maxThreads; + } + + /** + * Set the maximum number of actively communicating threads allowed + */ + public void setMaxThreads(int max) { + maxThreads = max; + } + + /** + * Launches or queues an asynchronous request. + * + * If the maximum active thread count has not been reached, + * start a new thread and use it to send and receive the request. + * The response is passed to the request's HttpRequestHandler + * onComplete(). After complete, if the number of active threads + * is still lower than the max, one request will be pulled (if + * present) from the async queue and fired. + * + * If there are too many active threads, the main request is + * pushed onto the async queue for later processing + */ + protected void manageAsyncRequest(final HttpRequest request) { + + if (activeThreads >= maxThreads) { + pendingThreadQueue.offer(request); + return; + } + + activeThreads++; + + //Send the request receive the response, fire off the next + //thread if necessary, then pass the result to the handler + Runnable r = new Runnable() { + public void run() { + + Object response; + request.send(); + + while ((response = request.recv()) != null) { + if (request.handler != null) + request.handler.onResponse(request, response); + } + + if (request.handler != null) + request.handler.onComplete(request); + + activeThreads--; + + if (activeThreads < maxThreads) { + try { + manageAsyncRequest(pendingThreadQueue.remove()); + } catch (java.util.NoSuchElementException ex) { + // may have been gobbled by another thread + } + } + } + }; + + new Thread(r).start(); + } +} + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpRequest.java b/Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpRequest.java new file mode 100644 index 0000000000..8af4623451 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpRequest.java @@ -0,0 +1,70 @@ +package org.opensrf.net.http; +import org.opensrf.*; +import org.opensrf.util.*; + +import java.util.List; +import java.util.LinkedList; +import java.net.HttpURLConnection; + +public abstract class HttpRequest { + + protected String service; + protected Method method; + protected HttpURLConnection urlConn; + protected HttpConnection httpConn; + protected HttpRequestHandler handler; + protected List responseList; + protected Exception failure; + protected boolean failed; + protected boolean complete; + + public HttpRequest() { + failed = false; + complete = false; + handler = null; + urlConn = null; + } + + public HttpRequest(HttpConnection conn, String service, Method method) { + this(); + this.httpConn = conn; + this.service = service; + this.method = method; + } + + public void sendAsync(final HttpRequestHandler handler) { + this.handler = handler; + httpConn.manageAsyncRequest(this); + } + + protected void pushResponse(Object response) { + if (responseList == null) + responseList = new LinkedList(); + responseList.add(response); + } + + protected List responses() { + return responseList; + } + + protected Object nextResponse() { + if (complete || failed) return null; + if (responseList.size() > 0) + return responseList.remove(0); + return null; + } + + public Exception getFailure() { + return failure; + } + + public abstract HttpRequest send(); + + public abstract Object recv(); + + public boolean failed(){ + return failed; + } +} + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpRequestHandler.java b/Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpRequestHandler.java new file mode 100644 index 0000000000..9c0f9e5043 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/net/http/HttpRequestHandler.java @@ -0,0 +1,25 @@ +package org.opensrf.net.http; + +import java.util.List; + +/* + * Handler for async gateway responses. + */ +public abstract class HttpRequestHandler { + + /** + * Called when all responses have been received. + * + * If discardResponses() returns true, will be passed null. + */ + public void onComplete(HttpRequest request) { + } + + /** + * Called with each response received from the server. + * + * @param payload the value returned from the server. + */ + public void onResponse(HttpRequest request, Object response) { + } +} diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPException.java b/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPException.java new file mode 100644 index 0000000000..8c20ab7aee --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPException.java @@ -0,0 +1,10 @@ +package org.opensrf.net.xmpp; + +/** + * Used for XMPP stream/authentication errors + */ +public class XMPPException extends Exception { + public XMPPException(String info) { + super(info); + } +} diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPMessage.java b/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPMessage.java new file mode 100644 index 0000000000..b6e2c76346 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPMessage.java @@ -0,0 +1,101 @@ +package org.opensrf.net.xmpp; + +import java.io.*; + +/** + * Models a single XMPP message. + */ +public class XMPPMessage { + + /** Message body */ + private String body; + /** Message recipient */ + private String to; + /** Message sender */ + private String from; + /** Message thread */ + private String thread; + /** Message xid */ + private String xid; + + public XMPPMessage() { + } + + public String getBody() { + return body; + } + public String getTo() { + return to; + } + public String getFrom() { + return from; + } + public String getThread() { + return thread; + } + public String getXid() { + return xid; + } + public void setBody(String body) { + this.body = body; + } + public void setTo(String to) { + this.to = to; + } + public void setFrom(String from) { + this.from = from; + } + public void setThread(String thread) { + this.thread = thread; + } + public void setXid(String xid) { + this.xid = xid; + } + + + /** + * Generates the XML representation of this message. + */ + public String toXML() { + StringBuffer sb = new StringBuffer(""); + escapeXML(thread, sb); + sb.append(""); + escapeXML(body, sb); + sb.append(""); + return sb.toString(); + } + + + /** + * Escapes non-valid XML characters. + * @param s The string to escape. + * @param sb The StringBuffer to append new data to. + */ + private void escapeXML(String s, StringBuffer sb) { + if( s == null ) return; + char c; + int l = s.length(); + for( int i = 0; i < l; i++ ) { + c = s.charAt(i); + switch(c) { + case '<': + sb.append("<"); + break; + case '>': + sb.append(">"); + break; + case '&': + sb.append("&"); + break; + default: + sb.append(c); + } + } + } +} + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPReader.java b/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPReader.java new file mode 100644 index 0000000000..406298a730 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPReader.java @@ -0,0 +1,293 @@ +package org.opensrf.net.xmpp; + +import javax.xml.stream.*; +import javax.xml.stream.events.* ; +import javax.xml.namespace.QName; +import java.util.Queue; +import java.io.InputStream; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.Date; +import org.opensrf.util.Logger; + +/** + * Slim XMPP Stream reader. This reader only understands enough XMPP + * to handle logins and recv messages. It's implemented as a StAX parser. + * @author Bill Erickson, Georgia Public Library Systems + */ +public class XMPPReader implements Runnable { + + /** Queue of received messages. */ + private Queue msgQueue; + /** Incoming XMPP XML stream */ + private InputStream inStream; + /** Current message body */ + private StringBuffer msgBody; + /** Current message thread */ + private StringBuffer msgThread; + /** Current message status */ + private StringBuffer msgStatus; + /** Current message error type */ + private StringBuffer msgErrType; + /** Current message sender */ + private String msgFrom; + /** Current message recipient */ + private String msgTo; + /** Current message error code */ + private int msgErrCode; + + /** Where this reader currently is in the document */ + private XMLState xmlState; + + /** The current connect state to the XMPP server */ + private XMPPStreamState streamState; + + + /** Used to represent out connection state to the XMPP server */ + public static enum XMPPStreamState { + DISCONNECTED, /* not connected to the server */ + CONNECT_SENT, /* we've sent the initial connect message */ + CONNECT_RECV, /* we've received a response to our connect message */ + AUTH_SENT, /* we've sent an authentication request */ + CONNECTED /* authentication is complete */ + }; + + + /** Used to represents where we are in the XML document stream. */ + public static enum XMLState { + IN_NOTHING, + IN_BODY, + IN_THREAD, + IN_STATUS + }; + + + /** + * Creates a new reader. Initializes the message queue. + * Sets the stream state to disconnected, and the xml + * state to in_nothing. + * @param inStream the inbound XML stream + */ + public XMPPReader(InputStream inStream) { + msgQueue = new ConcurrentLinkedQueue(); + this.inStream = inStream; + resetBuffers(); + xmlState = XMLState.IN_NOTHING; + streamState = XMPPStreamState.DISCONNECTED; + } + + /** + * Change the connect state and notify that a core + * event has occurred. + */ + protected void setXMPPStreamState(XMPPStreamState state) { + streamState = state; + notifyCoreEvent(); + } + + /** + * @return The current stream state of the reader + */ + public XMPPStreamState getXMPPStreamState() { + return streamState; + } + + + /** + * @return The next message in the queue, or null + */ + public XMPPMessage popMessageQueue() { + return (XMPPMessage) msgQueue.poll(); + } + + + /** + * Initializes the message buffers + */ + private void resetBuffers() { + msgBody = new StringBuffer(); + msgThread = new StringBuffer(); + msgStatus = new StringBuffer(); + msgErrType = new StringBuffer(); + msgFrom = ""; + msgTo = ""; + } + + + /** + * Notifies the waiting thread that a core event has occurred. + * Each reader should have exactly one dependent session thread. + */ + private synchronized void notifyCoreEvent() { + notifyAll(); + } + + + /** + * Waits up to timeout milliseconds for a core event to occur. + * Also, having a message already waiting in the queue + * constitutes a core event. + * @param timeout The number of milliseconds to wait. If + * timeout is negative, waits potentially forever. + * @return The number of milliseconds in wait + */ + public synchronized long waitCoreEvent(long timeout) { + + if(msgQueue.peek() != null || timeout == 0) return 0; + long start = new Date().getTime(); + + try{ + if(timeout < 0) + wait(); + else + wait(timeout); + } catch(InterruptedException ie) {} + + return new Date().getTime() - start; + } + + + + /** Kickoff the thread */ + public void run() { + read(); + } + + + /** + * Parses XML data from the provided XMPP stream. + */ + public void read() { + + try { + + XMLInputFactory factory = XMLInputFactory.newInstance(); + + /** disable as many unused features as possible to speed up the parsing */ + factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.FALSE); + factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE); + factory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE); + factory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.FALSE); + factory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE); + + /** create the stream reader */ + XMLStreamReader reader = factory.createXMLStreamReader(inStream); + int eventType; + + while(reader.hasNext()) { + /** cycle through the XML events */ + + eventType = reader.next(); + + switch(eventType) { + + case XMLEvent.START_ELEMENT: + handleStartElement(reader); + break; + + case XMLEvent.CHARACTERS: + switch(xmlState) { + case IN_BODY: + msgBody.append(reader.getText()); + break; + case IN_THREAD: + msgThread.append(reader.getText()); + break; + case IN_STATUS: + msgStatus.append(reader.getText()); + break; + } + break; + + case XMLEvent.END_ELEMENT: + xmlState = XMLState.IN_NOTHING; + if("message".equals(reader.getName().toString())) { + + /** build a message and add it to the message queue */ + XMPPMessage msg = new XMPPMessage(); + msg.setFrom(msgFrom); + msg.setTo(msgTo); + msg.setBody(msgBody.toString()); + msg.setThread(msgThread.toString()); + + Logger.internal("xmpp message from="+msgFrom+" " + msg.getBody()); + + msgQueue.offer(msg); + resetBuffers(); + notifyCoreEvent(); + } + break; + } + } + + } catch(javax.xml.stream.XMLStreamException se) { + /* XXX log an error */ + xmlState = XMLState.IN_NOTHING; + streamState = XMPPStreamState.DISCONNECTED; + notifyCoreEvent(); + } + } + + + /** + * Handles the start_element event. + */ + private void handleStartElement(XMLStreamReader reader) { + + String name = reader.getName().toString(); + + if("message".equals(name)) { + xmlState = XMLState.IN_BODY; + + /** add a special case for the opensrf "router_from" attribute */ + String rf = reader.getAttributeValue(null, "router_from"); + if( rf != null ) + msgFrom = rf; + else + msgFrom = reader.getAttributeValue(null, "from"); + msgTo = reader.getAttributeValue(null, "to"); + return; + } + + if("body".equals(name)) { + xmlState = XMLState.IN_BODY; + return; + } + + if("thread".equals(name)) { + xmlState = XMLState.IN_THREAD; + return; + } + + if("stream:stream".equals(name)) { + setXMPPStreamState(XMPPStreamState.CONNECT_RECV); + return; + } + + if("iq".equals(name)) { + if("result".equals(reader.getAttributeValue(null, "type"))) + setXMPPStreamState(XMPPStreamState.CONNECTED); + return; + } + + if("status".equals(name)) { + xmlState = XMLState.IN_STATUS; + return; + } + + if("stream:error".equals(name)) { + setXMPPStreamState(XMPPStreamState.DISCONNECTED); + return; + } + + if("error".equals(name)) { + msgErrType.append(reader.getAttributeValue(null, "type")); + msgErrCode = Integer.parseInt(reader.getAttributeValue(null, "code")); + setXMPPStreamState(XMPPStreamState.DISCONNECTED); + return; + } + } +} + + + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPSession.java b/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPSession.java new file mode 100644 index 0000000000..f9be7d2a63 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/net/xmpp/XMPPSession.java @@ -0,0 +1,263 @@ +package org.opensrf.net.xmpp; + +import java.io.*; +import java.net.Socket; +import java.util.Map; +import java.util.Iterator; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * Represents a single XMPP session. Sessions are responsible for writing to + * the stream and for managing a stream reader. + */ +public class XMPPSession { + + /** Initial jabber message */ + public static final String JABBER_CONNECT = + ""; + + /** Basic auth message */ + public static final String JABBER_BASIC_AUTH = + "" + + "%s%s%s"; + + public static final String JABBER_DISCONNECT = ""; + + private static Map threadConnections = new ConcurrentHashMap(); + + /** jabber domain */ + private String host; + /** jabber port */ + private int port; + /** jabber username */ + private String username; + /** jabber password */ + private String password; + /** jabber resource */ + private String resource; + + /** XMPP stream reader */ + XMPPReader reader; + /** Fprint-capable socket writer */ + PrintWriter writer; + /** Raw socket output stream */ + OutputStream outStream; + /** The raw socket */ + Socket socket; + + /** The process-wide session. All communication occurs + * accross this single connection */ + private static XMPPSession globalSession; + + + /** + * Creates a new session. + * @param host The jabber domain + * @param port The jabber port + */ + public XMPPSession( String host, int port ) { + this.host = host; + this.port = port; + } + + /** + * Returns the global, process-wide session + */ + /* + public static XMPPSession getGlobalSession() { + return globalSession; + } + */ + + public static XMPPSession getThreadSession() { + return (XMPPSession) threadConnections.get(new Long(Thread.currentThread().getId())); + } + + /** + * Sets the given session as the global session for the current thread + * @param ses The session + */ + public static void setThreadSession(XMPPSession ses) { + /* every time we create a new connection, clean up any dead threads. + * this is cheaper than cleaning up the dead threads at every access. */ + cleanupThreadSessions(); + threadConnections.put(new Long(Thread.currentThread().getId()), ses); + } + + /** + * Analyzes the threadSession data to see if there are any sessions + * whose controlling thread has gone away. + */ + private static void cleanupThreadSessions() { + Thread threads[] = new Thread[Thread.activeCount()]; + Thread.enumerate(threads); + for(Iterator i = threadConnections.keySet().iterator(); i.hasNext(); ) { + boolean found = false; + Long id = (Long) i.next(); + for(Thread t : threads) { + if(t.getId() == id.longValue()) { + found = true; + break; + } + } + if(!found) + threadConnections.remove(id); + } + } + + /** + * Sets the global, process-wide section + */ + /* + public static void setGlobalSession(XMPPSession ses) { + globalSession = ses; + } + */ + + + /** true if this session is connected to the server */ + public boolean connected() { + return ( + reader != null && + reader.getXMPPStreamState() == XMPPReader.XMPPStreamState.CONNECTED && + !socket.isClosed() + ); + } + + + /** + * Connects to the network. + * @param username The jabber username + * @param password The jabber password + * @param resource The Jabber resource + */ + public void connect(String username, String password, String resource) throws XMPPException { + + this.username = username; + this.password = password; + this.resource = resource; + + try { + /* open the socket and associated streams */ + socket = new Socket(host, port); + + /** the session maintains control over the output stream */ + outStream = socket.getOutputStream(); + writer = new PrintWriter(outStream, true); + + /** pass the input stream to the reader */ + reader = new XMPPReader(socket.getInputStream()); + + } catch(IOException ioe) { + throw new + XMPPException("unable to communicate with host " + host + " on port " + port); + } + + /* build the reader thread */ + Thread thread = new Thread(reader); + thread.setDaemon(true); + thread.start(); + + synchronized(reader) { + /* send the initial jabber message */ + sendConnect(); + reader.waitCoreEvent(10000); + } + if( reader.getXMPPStreamState() != XMPPReader.XMPPStreamState.CONNECT_RECV ) + throw new XMPPException("unable to connect to jabber server"); + + synchronized(reader) { + /* send the basic auth message */ + sendBasicAuth(); + reader.waitCoreEvent(10000); + } + if(!connected()) + throw new XMPPException("Authentication failed"); + } + + /** Sends the initial jabber message */ + private void sendConnect() { + reader.setXMPPStreamState(XMPPReader.XMPPStreamState.CONNECT_SENT); + writer.printf(JABBER_CONNECT, host); + } + + /** Send the basic auth message */ + private void sendBasicAuth() { + reader.setXMPPStreamState(XMPPReader.XMPPStreamState.AUTH_SENT); + writer.printf(JABBER_BASIC_AUTH, username, password, resource); + } + + + /** + * Sends an XMPPMessage. + * @param msg The message to send. + */ + public synchronized void send(XMPPMessage msg) throws XMPPException { + checkConnected(); + try { + String xml = msg.toXML(); + outStream.write(xml.getBytes()); + } catch (Exception e) { + throw new XMPPException(e.toString()); + } + } + + + /** + * @throws XMPPException if we are no longer connected. + */ + private void checkConnected() throws XMPPException { + if(!connected()) + throw new XMPPException("Disconnected stream"); + } + + + /** + * Receives messages from the network. + * @param timeout Maximum number of milliseconds to wait for a message to arrive. + * If timeout is negative, this method will wait indefinitely. + * If timeout is 0, this method will not block at all, but will return a + * message if there is already a message available. + */ + public XMPPMessage recv(long timeout) throws XMPPException { + + XMPPMessage msg; + + if(timeout < 0) { + + while(true) { /* wait indefinitely for a message to arrive */ + reader.waitCoreEvent(timeout); + msg = reader.popMessageQueue(); + if( msg != null ) return msg; + checkConnected(); + } + + } else { + + while(timeout >= 0) { /* wait at most 'timeout' milleseconds for a message to arrive */ + msg = reader.popMessageQueue(); + if( msg != null ) return msg; + timeout -= reader.waitCoreEvent(timeout); + msg = reader.popMessageQueue(); + if( msg != null ) return msg; + checkConnected(); + if(timeout == 0) break; + } + } + + return reader.popMessageQueue(); + } + + + /** + * Disconnects from the jabber server and closes the socket + */ + public void disconnect() { + try { + outStream.write(JABBER_DISCONNECT.getBytes()); + socket.close(); + } catch(Exception e) {} + } +} + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/MathBench.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/MathBench.java new file mode 100644 index 0000000000..b6e67f953c --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/test/MathBench.java @@ -0,0 +1,79 @@ +package org.opensrf.test; +import org.opensrf.*; +import org.opensrf.util.*; +import java.util.Date; +import java.util.List; +import java.util.ArrayList; +import java.io.PrintStream; + + +public class MathBench { + + public static void main(String args[]) throws Exception { + + PrintStream out = System.out; + + if(args.length < 2) { + out.println("usage: java org.opensrf.test.MathBench "); + return; + } + + /** connect to the opensrf network */ + Sys.bootstrapClient(args[0], "/config/opensrf"); + + /** how many iterations */ + int count = Integer.parseInt(args[1]); + + /** create the client session */ + ClientSession session = new ClientSession("opensrf.math"); + + /** params are 1,2 */ + List params = new ArrayList(); + params.add(new Integer(1)); + params.add(new Integer(2)); + + Request request; + Result result; + long start; + double total = 0; + + for(int i = 0; i < count; i++) { + + start = new Date().getTime(); + + /** create (and send) the request */ + request = session.request("add", params); + + /** wait up to 3 seconds for a response */ + result = request.recv(3000); + + /** collect the round-trip time */ + total += new Date().getTime() - start; + + if(result.getStatusCode() == Status.OK) { + out.print("+"); + } else { + out.println("\nrequest failed"); + out.println("status = " + result.getStatus()); + out.println("status code = " + result.getStatusCode()); + } + + /** remove this request from the session's request set */ + request.cleanup(); + + if((i+1) % 100 == 0) /** print 100 responses per line */ + out.println(" [" + (i+1) + "]"); + } + + out.println("\nAverage request time is " + (total/count) + " ms"); + + /** remove this session from the global session cache */ + session.cleanup(); + + /** disconnect from the opensrf network */ + Sys.shutdown(); + } +} + + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestCache.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestCache.java new file mode 100644 index 0000000000..d555444ccf --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestCache.java @@ -0,0 +1,24 @@ +package org.opensrf.test; +import org.opensrf.util.Cache; + +public class TestCache { + public static void main(String args[]) throws Exception { + + /** + * args is a list of string like so: server:port server2:port server3:port ... + */ + + Cache.initCache(args); + Cache cache = new Cache(); + + cache.set("key1", "HI, MA!"); + cache.set("key2", "HI, MA! 2"); + cache.set("key3", "HI, MA! 3"); + + System.out.println("got key1 = " + (String) cache.get("key1")); + System.out.println("got key2 = " + (String) cache.get("key2")); + System.out.println("got key3 = " + (String) cache.get("key3")); + } +} + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestClient.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestClient.java new file mode 100644 index 0000000000..a1136cd719 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestClient.java @@ -0,0 +1,80 @@ +package org.opensrf.test; +import org.opensrf.*; +import org.opensrf.util.*; +import java.util.Map; +import java.util.Date; +import java.util.List; +import java.util.ArrayList; +import java.io.PrintStream; + +public class TestClient { + + public static void main(String args[]) throws Exception { + + /** which opensrf service are we sending our request to */ + String service; + /** which opensrf method we're calling */ + String method; + /** method params, captures from command-line args */ + List params; + /** knows how to read JSON */ + JSONReader reader; + /** opensrf request */ + Request request; + /** request result */ + Result result; + /** start time for the request */ + long start; + /** for brevity */ + PrintStream out = System.out; + + if(args.length < 3) { + out.println( "usage: org.opensrf.test.TestClient "+ + " [, ]"); + return; + } + + /** connect to the opensrf network, default config context + * for opensrf_core.xml is /config/opensrf */ + Sys.bootstrapClient(args[0], "/config/opensrf"); + + /* grab the server, method, and any params from the command line */ + service = args[1]; + method = args[2]; + params = new ArrayList(); + for(int i = 3; i < args.length; i++) + params.add(new JSONReader(args[i]).read()); + + + /** build the client session */ + ClientSession session = new ClientSession(service); + + /** kick off the timer */ + start = new Date().getTime(); + + /** Create the request object from the session, method and params */ + request = session.request(method, params); + + while( (result = request.recv(60000)) != null ) { + /** loop over the results and print the JSON version of the content */ + + if(result.getStatusCode() != 200) { + /** make sure the request succeeded */ + out.println("status = " + result.getStatus()); + out.println("status code = " + result.getStatusCode()); + continue; + } + + /** JSON-ify the resulting object and print it */ + out.println("\nresult JSON: " + new JSONWriter(result.getContent()).write()); + } + + /** How long did the request take? */ + out.println("Request round trip took: " + (new Date().getTime() - start) + " ms."); + + Sys.shutdown(); + } +} + + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestConfig.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestConfig.java new file mode 100644 index 0000000000..f65a84f701 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestConfig.java @@ -0,0 +1,16 @@ +package org.opensrf.test; +import org.opensrf.*; +import org.opensrf.util.*; + +public class TestConfig { + public static void main(String args[]) throws Exception { + Config config = new Config(""); + config.parse(args[0]); + Config.setConfig(config); + System.out.println(config); + System.out.println(""); + + for(int i = 1; i < args.length; i++) + System.out.println("Found config value: " + args[i] + ": " + Config.global().get(args[i])); + } +} diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestJSON.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestJSON.java new file mode 100644 index 0000000000..b19d4088aa --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestJSON.java @@ -0,0 +1,51 @@ +package org.opensrf.test; + +import org.opensrf.*; +import org.opensrf.util.*; +import java.util.*; + +public class TestJSON { + + public static void main(String args[]) throws Exception { + + Map map = new HashMap(); + map.put("key1", "value1"); + map.put("key2", "value2"); + map.put("key3", "value3"); + map.put("key4", "athe\u0301s"); + map.put("key5", null); + + List list = new ArrayList(16); + list.add(new Integer(1)); + list.add(new Boolean(true)); + list.add("WATER"); + list.add(null); + map.put("key6", list); + + System.out.println(new JSONWriter(map).write() + "\n"); + + String[] fields = {"isnew", "name", "shortname", "ill_address"}; + OSRFRegistry.registerObject("aou", OSRFRegistry.WireProtocol.ARRAY, fields); + + OSRFObject obj = new OSRFObject(OSRFRegistry.getRegistry("aou")); + obj.put("name", "athens clarke county"); + obj.put("ill_address", new Integer(1)); + obj.put("shortname", "ARL-ATH"); + + map.put("key7", obj); + list.add(obj); + System.out.println(new JSONWriter(map).write() + "\n"); + + + Message m = new Message(1, Message.REQUEST); + Method method = new Method("opensrf.settings.host_config.get"); + method.addParam("app07.dev.gapines.org"); + m.setPayload(method); + + String s = new JSONWriter(m).write(); + System.out.println(s + "\n"); + + Object o = new JSONReader(s).read(); + System.out.println("Read+Wrote: " + new JSONWriter(o).write()); + } +} diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestLog.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestLog.java new file mode 100644 index 0000000000..1d60242969 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestLog.java @@ -0,0 +1,15 @@ +package org.opensrf.test; +import org.opensrf.util.Logger; +import org.opensrf.util.FileLogger; + + +/** Simple test class for tesing the logging functionality */ +public class TestLog { + public static void main(String args[]) { + Logger.init(Logger.DEBUG, new FileLogger("test.log")); + Logger.error("Hello, world"); + Logger.warn("Hello, world"); + Logger.info("Hello, world"); + Logger.debug("Hello, world"); + } +} diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestMultiSession.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestMultiSession.java new file mode 100644 index 0000000000..bb0f1a1c09 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestMultiSession.java @@ -0,0 +1,26 @@ +package org.opensrf.test; +import org.opensrf.*; +import org.opensrf.util.*; + +public class TestMultiSession { + public static void main(String[] args) { + try { + String config = args[0]; + + Sys.bootstrapClient(config, "/config/opensrf"); + MultiSession ses = new MultiSession(); + + for(int i = 0; i < 40; i++) { + ses.request("opensrf.settings", "opensrf.system.time"); + } + + while(!ses.isComplete()) + System.out.println("result = " + ses.recv(5000) + " and id = " + ses.lastId()); + + System.out.println("done"); + Sys.shutdown(); + } catch(Exception e) { + e.printStackTrace(); + } + } +} diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestSettings.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestSettings.java new file mode 100644 index 0000000000..116bbe1683 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestSettings.java @@ -0,0 +1,14 @@ +package org.opensrf.test; +import org.opensrf.*; +import org.opensrf.util.*; + +public class TestSettings { + public static void main(String args[]) throws Exception { + Sys.bootstrapClient(args[0], "/config/opensrf"); + SettingsClient client = SettingsClient.instance(); + String lang = client.getString("/apps/opensrf.settings/language"); + String impl = client.getString("/apps/opensrf.settings/implementation"); + System.out.println("opensrf.settings language = " + lang); + System.out.println("opensrf.settings implementation = " + impl); + } +} diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestThread.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestThread.java new file mode 100644 index 0000000000..bb4cf067e9 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestThread.java @@ -0,0 +1,68 @@ +package org.opensrf.test; +import org.opensrf.*; +import org.opensrf.util.*; +import java.util.Map; +import java.util.Date; +import java.util.List; +import java.util.ArrayList; +import java.io.PrintStream; + +/** + * Connects to the opensrf network once per thread and runs + * and runs a series of request acccross all launched threads. + * The purpose is to verify that the java threaded client api + * is functioning as expected + */ +public class TestThread implements Runnable { + + String args[]; + + public TestThread(String args[]) { + this.args = args; + } + + public void run() { + + try { + + Sys.bootstrapClient(args[0], "/config/opensrf"); + ClientSession session = new ClientSession(args[3]); + + List params = new ArrayList(); + for(int i = 5; i < args.length; i++) + params.add(new JSONReader(args[3]).read()); + + for(int i = 0; i < Integer.parseInt(args[2]); i++) { + System.out.println("thread " + Thread.currentThread().getId()+" sending request " + i); + Request request = session.request(args[4], params); + Result result = request.recv(3000); + if(result != null) { + System.out.println("thread " + Thread.currentThread().getId()+ + " got result JSON: " + new JSONWriter(result.getContent()).write()); + } else { + System.out.println("* thread " + Thread.currentThread().getId()+ " got NO result"); + } + } + + Sys.shutdown(); + } catch(Exception e) { + System.err.println(e); + } + } + + public static void main(String args[]) throws Exception { + + if(args.length < 5) { + System.out.println( "usage: org.opensrf.test.TestClient "+ + " [, ]"); + return; + } + + int numThreads = Integer.parseInt(args[1]); + for(int i = 0; i < numThreads; i++) + new Thread(new TestThread(args)).start(); + } +} + + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMLFlattener.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMLFlattener.java new file mode 100644 index 0000000000..c1fa394fef --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMLFlattener.java @@ -0,0 +1,11 @@ +package org.opensrf.test; +import org.opensrf.util.XMLFlattener; +import java.io.FileInputStream; + +public class TestXMLFlattener { + public static void main(String args[]) throws Exception { + FileInputStream fis = new FileInputStream(args[0]); + XMLFlattener f = new XMLFlattener(fis); + System.out.println(f.read()); + } +} diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMLTransformer.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMLTransformer.java new file mode 100644 index 0000000000..9768372f11 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMLTransformer.java @@ -0,0 +1,22 @@ +package org.opensrf.test; +import org.opensrf.util.XMLTransformer; +import java.io.File; + +public class TestXMLTransformer { + /** + * arg[0] path to an XML file + * arg[1] path to the XSL file to apply + */ + public static void main(String[] args) { + try { + File xmlFile = new File(args[0]); + File xslFile = new File(args[1]); + XMLTransformer t = new XMLTransformer(xmlFile, xslFile); + System.out.println(t.apply()); + } catch(Exception e) { + e.printStackTrace(); + } + } +} + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMPP.java b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMPP.java new file mode 100644 index 0000000000..2fba67fb8b --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/test/TestXMPP.java @@ -0,0 +1,63 @@ +package org.opensrf.test; + +import org.opensrf.net.xmpp.XMPPReader; +import org.opensrf.net.xmpp.XMPPMessage; +import org.opensrf.net.xmpp.XMPPSession; + +public class TestXMPP { + + /** + * Connects to the jabber server and waits for inbound messages. + * If a recipient is provided, a small message is sent to the recipient. + */ + public static void main(String args[]) throws Exception { + + String host; + int port; + String username; + String password; + String resource; + String recipient; + + try { + host = args[0]; + port = Integer.parseInt(args[1]); + username = args[2]; + password = args[3]; + resource = args[4]; + + } catch(ArrayIndexOutOfBoundsException e) { + System.err.println("usage: org.opensrf.test.TestXMPP []"); + return; + } + + XMPPSession session = new XMPPSession(host, port); + session.connect(username, password, resource); + + XMPPMessage msg; + + if( args.length == 6 ) { + + /** they specified a recipient */ + + recipient = args[5]; + msg = new XMPPMessage(); + msg.setTo(recipient); + msg.setThread("test-thread"); + msg.setBody("Hello, from java-xmpp"); + System.out.println("Sending message to " + recipient); + session.send(msg); + } + + while(true) { + System.out.println("waiting for message..."); + msg = session.recv(-1); /* wait forever for a message to arrive */ + System.out.println("got message: " + msg.toXML()); + } + } +} + + + + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/Cache.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/Cache.java new file mode 100644 index 0000000000..53036886cc --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/util/Cache.java @@ -0,0 +1,38 @@ +package org.opensrf.util; +import com.danga.MemCached.*; +import java.util.List; + +/** + * Memcache client + */ +public class Cache extends MemCachedClient { + + public Cache() { + super(); + setCompressThreshold(4096); /* ?? */ + } + + /** + * Initializes the cache client + * @param serverList Array of server:port strings specifying the + * set of memcache servers this client will talk to + */ + public static void initCache(String[] serverList) { + SockIOPool pool = SockIOPool.getInstance(); + pool.setServers(serverList); + pool.initialize(); + com.danga.MemCached.Logger logger = + com.danga.MemCached.Logger.getLogger(MemCachedClient.class.getName()); + logger.setLevel(logger.LEVEL_ERROR); + } + + /** + * Initializes the cache client + * @param serverList List of server:port strings specifying the + * set of memcache servers this client will talk to + */ + public static void initCache(List serverList) { + initCache(serverList.toArray(new String[]{})); + } +} + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/Config.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/Config.java new file mode 100644 index 0000000000..ddac9c0b71 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/util/Config.java @@ -0,0 +1,139 @@ +package org.opensrf.util; + +import org.json.*; +import java.util.Map; +import java.util.List; + + +/** + * Config reader and accesor module. This module reads an XML config file, + * then loads the file into an internal config, whose values may be accessed + * by xpath-style lookup paths. + */ +public class Config { + + /** The globl config instance */ + private static Config config; + /** The object form of the parsed config */ + private Map configObject; + /** + * The log parsing context. This is used as a prefix to the + * config item search path. This allows config XML chunks to + * be inserted into arbitrary XML files. + */ + private String context; + + public static Config global() { + return config; + } + + + /** + * @param context The config context + */ + public Config(String context) { + this.context = context; + } + + /** + * Sets the global config object. + * @param c The config object to use. + */ + public static void setGlobalConfig(Config c) { + config = c; + } + + /** + * Parses an XML config file. + * @param filename The path to the file to parse. + */ + public void parse(String filename) throws ConfigException { + try { + String xml = Utils.fileToString(filename); + JSONObject jobj = XML.toJSONObject(xml); + configObject = (Map) new JSONReader(jobj.toString()).readObject(); + } catch(Exception e) { + throw new ConfigException("Error parsing config", e); + } + } + + public static void setConfig(Config conf) { + config = conf; + } + + public void setConfigObject(Map config) { + this.configObject = config; + } + + protected Map getConfigObject() { + return this.configObject; + } + + + /** + * Returns the configuration value found at the requested path. + * @param path The search path + * @return The config value, or null if no value exists at the given path. + * @throws ConfigException thrown if nothing is found at the path + */ + public String getString(String path) throws ConfigException { + try { + return (String) get(path); + } catch(Exception e) { + throw new + ConfigException("No config string found at " + path); + } + } + + /** + * Gets the int value at the given path + * @param path The search path + */ + public int getInt(String path) throws ConfigException { + try { + return Integer.parseInt(getString(path)); + } catch(Exception e) { + throw new + ConfigException("No config int found at " + path); + } + } + + /** + * Returns the configuration object found at the requested path. + * @param path The search path + * @return The config value + * @throws ConfigException thrown if nothing is found at the path + */ + public Object get(String path) throws ConfigException { + try { + Object obj = Utils.findPath(configObject, context + path); + if(obj == null) + throw new ConfigException(""); + return obj; + } catch(Exception e) { + e.printStackTrace(); + throw new ConfigException("No config object found at " + path); + } + } + + /** + * Returns the first item in the list found at the given path. If + * no list is found, ConfigException is thrown. + * @param path The search path + */ + public Object getFirst(String path) throws ConfigException { + Object obj = get(path); + if(obj instanceof List) + return ((List) obj).get(0); + return obj; + } + + + /** + * Returns the config as a JSON string + */ + public String toString() { + return new JSONWriter(configObject).write(); + } +} + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/ConfigException.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/ConfigException.java new file mode 100644 index 0000000000..c1c491ec8d --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/util/ConfigException.java @@ -0,0 +1,14 @@ +package org.opensrf.util; + +/** + * Thrown by the Config module when a user requests a configuration + * item that does not exist + */ +public class ConfigException extends Exception { + public ConfigException(String info) { + super(info); + } + public ConfigException(String info, Throwable t) { + super(info, t); + } +} diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/FileLogger.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/FileLogger.java new file mode 100644 index 0000000000..9eb838df0c --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/util/FileLogger.java @@ -0,0 +1,44 @@ +package org.opensrf.util; +import java.io.BufferedWriter; +import java.io.FileWriter; + + +public class FileLogger extends Logger { + + /** File to log to */ + private String filename; + + /** + * FileLogger constructor + * @param filename The path to the log file + */ + public FileLogger(String filename) { + this.filename = filename; + } + + /** + * Logs the mesage to a file. + * @param level The log level + * @param msg The mesage to log + */ + protected synchronized void log(short level, String msg) { + if(level > logLevel) return; + + BufferedWriter out = null; + try { + out = new BufferedWriter(new FileWriter(this.filename, true)); + out.write(formatMessage(level, msg) + "\n"); + + } catch(Exception e) { + /** If we are unable to write our log message, go ahead and + * fall back to the default (stdout) logger */ + Logger.init(logLevel, new Logger()); + Logger.logByLevel(ERROR, "Unable to write to log file " + this.filename); + Logger.logByLevel(level, msg); + } + + try { + out.close(); + } catch(Exception e) {} + } +} diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/JSONException.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/JSONException.java new file mode 100644 index 0000000000..ec28e1d86d --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/util/JSONException.java @@ -0,0 +1,9 @@ +package org.opensrf.util; +/** + * Used to indicate JSON parsing errors + */ +public class JSONException extends Exception { + public JSONException(String s) { + super(s); + } +} diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/JSONReader.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/JSONReader.java new file mode 100644 index 0000000000..55eb5c03b4 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/util/JSONReader.java @@ -0,0 +1,176 @@ +package org.opensrf.util; + +import java.io.*; +import java.util.*; + +import org.json.JSONTokener; +import org.json.JSONObject; +import org.json.JSONArray; + + +/** + * JSON utilities. + */ +public class JSONReader { + + /** Special OpenSRF serializable object netClass key */ + public static final String JSON_CLASS_KEY = "__c"; + + /** Special OpenSRF serializable object payload key */ + public static final String JSON_PAYLOAD_KEY = "__p"; + + /** The JSON string to parser */ + private String json; + + /** + * @param json The JSON to parse + */ + public JSONReader(String json) { + this.json = json; + } + + /** + * Parses JSON and creates an object. + * @return The resulting object which may be a List, + * Map, Number, String, Boolean, or null + */ + public Object read() throws JSONException { + JSONTokener tk = new JSONTokener(json); + try { + return readSubObject(tk.nextValue()); + } catch(org.json.JSONException e) { + throw new JSONException(e.toString()); + } + } + + /** + * Assumes that a JSON array will be read. Returns + * the resulting array as a list. + */ + public List readArray() throws JSONException { + Object o = read(); + try { + return (List) o; + } catch(Exception e) { + throw new JSONException("readArray(): JSON cast exception"); + } + } + + /** + * Assumes that a JSON object will be read. Returns + * the resulting object as a map. + */ + public Map readObject() throws JSONException { + Object o = read(); + try { + return (Map) o; + } catch(Exception e) { + throw new JSONException("readObject(): JSON cast exception"); + } + } + + + /** + * Recurse through the object and turn items into maps, lists, etc. + */ + private Object readSubObject(Object obj) throws JSONException { + + if( obj == null || + obj instanceof String || + obj instanceof Number || + obj instanceof Boolean) + return obj; + + try { + + if( obj instanceof JSONObject ) { + + /* read objects */ + String key; + JSONObject jobj = (JSONObject) obj; + Map map = new HashMap(); + + for( Iterator e = jobj.keys(); e.hasNext(); ) { + key = (String) e.next(); + + /* we encoutered the special class key */ + if( JSON_CLASS_KEY.equals(key) ) + return buildRegisteredObject( + (String) jobj.get(key), jobj.get(JSON_PAYLOAD_KEY)); + + /* we encountered the data key */ + if( JSON_PAYLOAD_KEY.equals(key) ) + return buildRegisteredObject( + (String) jobj.get(JSON_CLASS_KEY), jobj.get(key)); + + map.put(key, readSubObject(jobj.get(key))); + } + return map; + } + + if ( obj instanceof JSONArray ) { + + JSONArray jarr = (JSONArray) obj; + int length = jarr.length(); + List list = new ArrayList(length); + + for( int i = 0; i < length; i++ ) + list.add(readSubObject(jarr.get(i))); + return list; + + } + + } catch(org.json.JSONException e) { + + throw new JSONException(e.toString()); + } + + return null; + } + + + + /** + * Builds an OSRFObject map registered OSRFHash object based on the JSON object data. + * @param netClass The network class hint for this object. + * @param paylaod The actual object on the wire. + */ + private OSRFObject buildRegisteredObject( + String netClass, Object payload) throws JSONException { + + OSRFRegistry registry = OSRFRegistry.getRegistry(netClass); + OSRFObject obj = new OSRFObject(registry); + + try { + if( payload instanceof JSONArray ) { + JSONArray jarr = (JSONArray) payload; + + /* for each array item, instert the item into the hash. the hash + * key is found by extracting the fields array from the registered + * object at the current array index */ + String fields[] = registry.getFields(); + for( int i = 0; i < jarr.length(); i++ ) { + obj.put(fields[i], readSubObject(jarr.get(i))); + } + + } else if( payload instanceof JSONObject ) { + + /* since this is a hash, simply copy the data over */ + JSONObject jobj = (JSONObject) payload; + String key; + for( Iterator e = jobj.keys(); e.hasNext(); ) { + key = (String) e.next(); + obj.put(key, readSubObject(jobj.get(key))); + } + } + + } catch(org.json.JSONException e) { + throw new JSONException(e.toString()); + } + + return obj; + } +} + + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/JSONWriter.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/JSONWriter.java new file mode 100644 index 0000000000..7cb2cca02d --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/util/JSONWriter.java @@ -0,0 +1,172 @@ +package org.opensrf.util; + +import java.io.*; +import java.util.*; + + +/** + * JSONWriter + */ +public class JSONWriter { + + /** The object to serialize to JSON */ + private Object obj; + + /** + * @param obj The object to encode + */ + public JSONWriter(Object obj) { + this.obj = obj; + } + + + /** + * Encodes a java object to JSON. + */ + public String write() { + StringBuffer sb = new StringBuffer(); + write(sb); + return sb.toString(); + } + + + + /** + * Encodes a java object to JSON. + * Maps (HashMaps, etc.) are encoded as JSON objects. + * Iterable's (Lists, etc.) are encoded as JSON arrays + */ + public void write(StringBuffer sb) { + write(obj, sb); + } + + /** + * Encodes the object as JSON into the provided buffer + */ + public void write(Object obj, StringBuffer sb) { + + /** JSON null */ + if(obj == null) { + sb.append("null"); + return; + } + + /** JSON string */ + if(obj instanceof String) { + sb.append('"'); + Utils.escape((String) obj, sb); + sb.append('"'); + return; + } + + /** JSON number */ + if(obj instanceof Number) { + sb.append(obj.toString()); + return; + } + + /** JSON array */ + if(obj instanceof Iterable) { + encodeJSONArray((Iterable) obj, sb); + return; + } + + /** OpenSRF serializable objects */ + if(obj instanceof OSRFSerializable) { + encodeOSRFSerializable((OSRFSerializable) obj, sb); + return; + } + + /** JSON object */ + if(obj instanceof Map) { + encodeJSONObject((Map) obj, sb); + return; + } + + /** JSON boolean */ + if(obj instanceof Boolean) { + sb.append((((Boolean) obj).booleanValue() ? "true" : "false")); + return; + } + } + + + /** + * Encodes a List as a JSON array + */ + private void encodeJSONArray(Iterable iterable, StringBuffer sb) { + Iterator itr = iterable.iterator(); + sb.append("["); + boolean some = false; + + while(itr.hasNext()) { + some = true; + write(itr.next(), sb); + sb.append(','); + } + + /* remove the trailing comma if the array has any items*/ + if(some) + sb.deleteCharAt(sb.length()-1); + sb.append("]"); + } + + + /** + * Encodes a Map as a JSON object + */ + private void encodeJSONObject(Map map, StringBuffer sb) { + Iterator itr = map.keySet().iterator(); + sb.append("{"); + Object key = null; + + while(itr.hasNext()) { + key = itr.next(); + write(key, sb); + sb.append(':'); + write(map.get(key), sb); + sb.append(','); + } + + /* remove the trailing comma if the object has any items*/ + if(key != null) + sb.deleteCharAt(sb.length()-1); + sb.append("}"); + } + + + /** + * Encodes a network-serializable OpenSRF object + */ + private void encodeOSRFSerializable(OSRFSerializable obj, StringBuffer sb) { + + OSRFRegistry reg = obj.getRegistry(); + String[] fields = reg.getFields(); + Map map = new HashMap(); + map.put(JSONReader.JSON_CLASS_KEY, reg.getNetClass()); + + if( reg.getWireProtocol() == OSRFRegistry.WireProtocol.ARRAY ) { + + /** encode arrays as lists */ + List list = new ArrayList(fields.length); + for(String s : fields) + list.add(obj.get(s)); + map.put(JSONReader.JSON_PAYLOAD_KEY, list); + + } else { + + /** encode hashes as maps */ + Map subMap = new HashMap(); + for(String s : fields) + subMap.put(s, obj.get(s)); + map.put(JSONReader.JSON_PAYLOAD_KEY, subMap); + + } + + /** now serialize the encoded object */ + write(map, sb); + } +} + + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/Logger.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/Logger.java new file mode 100644 index 0000000000..2923c23b9e --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/util/Logger.java @@ -0,0 +1,144 @@ +package org.opensrf.util; +import java.text.SimpleDateFormat; +import java.text.FieldPosition; +import java.util.Date; + +/** + * Basic OpenSRF logging API. This default implementation + * logs to stderr. + */ +public class Logger { + + /** Log levels */ + public static final short ERROR = 1; + public static final short WARN = 2; + public static final short INFO = 3; + public static final short DEBUG = 4; + public static final short INTERNAL = 5; + + /** The global log instance */ + private static Logger instance; + /** The global log level */ + protected static short logLevel; + + public Logger() {} + + /** Sets the global Logger instance + * @param level The global log level. + * @param l The Logger instance to use + */ + public static void init(short level, Logger l) { + instance = l; + logLevel = level; + } + + /** + * @return The global Logger instance + */ + public static Logger instance() { + return instance; + } + + /** + * Logs an error message + * @param msg The message to log + */ + public static void error(String msg) { + instance.log(ERROR, msg); + } + + /** + * Logs an warning message + * @param msg The message to log + */ + public static void warn(String msg) { + instance.log(WARN, msg); + } + + /** + * Logs an info message + * @param msg The message to log + */ + public static void info(String msg) { + instance.log(INFO, msg); + } + + /** + * Logs an debug message + * @param msg The message to log + */ + public static void debug(String msg) { + instance.log(DEBUG, msg); + } + + /** + * Logs an internal message + * @param msg The message to log + */ + public static void internal(String msg) { + instance.log(INTERNAL, msg); + } + + + /** + * Appends the text representation of the log level + * @param sb The stringbuffer to append to + * @param level The log level + */ + protected static void appendLevelString(StringBuffer sb, short level) { + switch(level) { + case DEBUG: + sb.append("DEBG"); break; + case INFO: + sb.append("INFO"); break; + case INTERNAL: + sb.append("INT "); break; + case WARN: + sb.append("WARN"); break; + case ERROR: + sb.append("ERR "); break; + } + } + + /** + * Formats a message for logging. Appends the current date+time + * and the log level string. + * @param level The log level + * @param msg The message to log + */ + protected static String formatMessage(short level, String msg) { + + StringBuffer sb = new StringBuffer(); + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format( + new Date(), sb, new FieldPosition(0)); + + sb.append(" ["); + appendLevelString(sb, level); + sb.append(":"); + sb.append(Thread.currentThread().getId()); + sb.append("] "); + sb.append(msg); + return sb.toString(); + } + + /** + * Logs a message by passing the log level explicitly + * @param level The log level + * @param msg The message to log + */ + public static void logByLevel(short level, String msg) { + instance.log(level, msg); + } + + /** + * Performs the actual logging. Subclasses should override + * this method. + * @param level The log level + * @param msg The message to log + */ + protected synchronized void log(short level, String msg) { + if(level > logLevel) return; + System.err.println(formatMessage(level, msg)); + } +} + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFObject.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFObject.java new file mode 100644 index 0000000000..af28f4a860 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFObject.java @@ -0,0 +1,63 @@ +package org.opensrf.util; + +import java.util.Map; +import java.util.HashMap; + + +/** + * Generic OpenSRF network-serializable object. This allows + * access to object fields. + */ +public class OSRFObject extends HashMap implements OSRFSerializable { + + /** This objects registry */ + private OSRFRegistry registry; + + public OSRFObject() { + } + + + /** + * Creates a new object with the provided registry + */ + public OSRFObject(OSRFRegistry reg) { + this(); + registry = reg; + } + + + /** + * Creates a new OpenSRF object based on the net class string + * */ + public OSRFObject(String netClass) { + this(OSRFRegistry.getRegistry(netClass)); + } + + + /** + * @return This object's registry + */ + public OSRFRegistry getRegistry() { + return registry; + } + + /** + * Implement get() to fulfill our contract with OSRFSerializable + */ + public Object get(String field) { + return super.get(field); + } + + /** Returns the string value found at the given field */ + public String getString(String field) { + return (String) get(field); + } + + /** Returns the int value found at the given field */ + public int getInt(String field) { + Object o = get(field); + if(o instanceof String) + return Integer.parseInt((String) o); + return ((Integer) get(field)).intValue(); + } +} diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFRegistry.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFRegistry.java new file mode 100644 index 0000000000..c6e6bc6540 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFRegistry.java @@ -0,0 +1,112 @@ +package org.opensrf.util; + +import java.io.Serializable; +import java.util.Map; +import java.util.HashMap; + + +/** + * Manages the registration of OpenSRF network-serializable objects. + * A serializable object has a class "hint" (called netClass within) which + * describes the type of object. Each object also has a set of field names + * for accessing/mutating object properties. Finally, objects have a + * serialization wire protocol. Currently supported protocols are HASH + * and ARRAY. + */ +public class OSRFRegistry implements Serializable{ + + + /** + * + */ + private static final long serialVersionUID = 1L; + /** + * Global collection of registered net objects. + * Maps netClass names to registries. + */ + private static HashMap + registry = new HashMap(); + + + /** Serialization types for registered objects */ + public enum WireProtocol { + ARRAY, HASH + }; + + + /** Array of field names for this registered object */ + String fields[]; + /** The wire protocol for this object */ + WireProtocol wireProtocol; + /** The network class for this object */ + String netClass; + + /** + * Returns the array of field names + */ + public String[] getFields() { + return this.fields; + } + + + /** + * Registers a new object. + * @param netClass The net class for this object + * @param wireProtocol The object's wire protocol + * @param fields An array of field names. For objects whose + * wire protocol is ARRAY, the positions of the field names + * will be used as the array indices for the fields at serialization time + */ + public static OSRFRegistry registerObject(String netClass, WireProtocol wireProtocol, String fields[]) { + OSRFRegistry r = new OSRFRegistry(netClass, wireProtocol, fields); + registry.put(netClass, r); + return r; + } + + /** + * Returns the registry for the given netclass + * @param netClass The network class to lookup + */ + public static OSRFRegistry getRegistry(String netClass) { + if( netClass == null ) return null; + return (OSRFRegistry) registry.get(netClass); + } + + + /** + * @param field The name of the field to lookup + * @return the index into the fields array of the given field name. + */ + public int getFieldIndex(String field) { + for( int i = 0; i < fields.length; i++ ) + if( fields[i].equals(field) ) + return i; + return -1; + } + + /** Returns the wire protocol of this object */ + public WireProtocol getWireProtocol() { + return this.wireProtocol; + } + + /** Returns the netClass ("hint") of this object */ + public String getNetClass() { + return this.netClass; + } + + /** + * Creates a new registry object. + * @param netClass The network class/hint + * @param wireProtocol The wire protocol + * @param fields The array of field names. For array-based objects, + * the fields array must be sorted in accordance with the sorting + * of the objects in the array. + */ + public OSRFRegistry(String netClass, WireProtocol wireProtocol, String fields[]) { + this.netClass = netClass; + this.wireProtocol = wireProtocol; + this.fields = fields; + } +} + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFSerializable.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFSerializable.java new file mode 100644 index 0000000000..64b5d6f4e5 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/util/OSRFSerializable.java @@ -0,0 +1,19 @@ +package org.opensrf.util; + +/** + * All network-serializable OpenSRF object must implement this interface. + */ +public interface OSRFSerializable { + + /** + * Returns the object registry object for the implementing class. + */ + public abstract OSRFRegistry getRegistry(); + + /** + * Returns the object found at the given field + */ + public abstract Object get(String field); +} + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/SettingsClient.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/SettingsClient.java new file mode 100644 index 0000000000..71e570a410 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/util/SettingsClient.java @@ -0,0 +1,53 @@ +package org.opensrf.util; +import org.opensrf.*; +import java.util.Map; + +/** + * Connects to the OpenSRF Settings server to fetch the settings config. + * Provides a Config interface for fetching settings via path + */ +public class SettingsClient extends Config { + + /** Singleton SettingsClient instance */ + private static SettingsClient client = new SettingsClient(); + + public SettingsClient() { + super(""); + } + + /** + * @return The global settings client instance + */ + public static SettingsClient instance() throws ConfigException { + if(client.getConfigObject() == null) + client.fetchConfig(); + return client; + } + + /** + * Fetches the settings object from the settings server + */ + private void fetchConfig() throws ConfigException { + + ClientSession ses = new ClientSession("opensrf.settings"); + try { + + Request req = ses.request( + "opensrf.settings.host_config.get", + new String[]{(String)Config.global().getFirst("/domain")}); + + Result res = req.recv(12000); + if(res == null) { + /** throw exception */ + } + setConfigObject((Map) res.getContent()); + + } catch(Exception e) { + throw new ConfigException("Error fetching settings config", e); + + } finally { + ses.cleanup(); + } + } +} + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/Utils.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/Utils.java new file mode 100644 index 0000000000..159d254706 --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/util/Utils.java @@ -0,0 +1,106 @@ +package org.opensrf.util; + +import java.io.*; +import java.util.*; + +/** + * Collection of general, static utility methods + */ +public class Utils { + + /** + * Returns the string representation of a given file. + * @param filename The file to turn into a string + */ + public static String fileToString(String filename) + throws FileNotFoundException, IOException { + + StringBuffer sb = new StringBuffer(); + BufferedReader in = new BufferedReader(new FileReader(filename)); + String str; + while ((str = in.readLine()) != null) + sb.append(str); + in.close(); + return sb.toString(); + } + + + /** + * Escapes a string. + */ + public static String escape(String string) { + StringBuffer sb = new StringBuffer(); + escape(string, sb); + return sb.toString(); + } + + /** + * Escapes a string. Turns bare newlines into \n, etc. + * Escapes \n, \r, \t, ", \f + * Encodes non-ascii characters as UTF-8: \u0000 + * @param string The string to escape + * @param sb The string buffer to write the escaped string into + */ + public static void escape(String string, StringBuffer sb) { + int len = string.length(); + String utf; + char c; + for( int i = 0; i < len; i++ ) { + c = string.charAt(i); + switch (c) { + case '\\': + sb.append("\\\\"); + break; + case '"': + sb.append("\\\""); + break; + case '\b': + sb.append("\\b"); + break; + case '\t': + sb.append("\\t"); + break; + case '\n': + sb.append("\\n"); + break; + case '\f': + sb.append("\\f"); + break; + case '\r': + sb.append("\\r"); + break; + default: + if (c < 32 || c > 126 ) { + /* escape all non-ascii or control characters as UTF-8 */ + utf = "000" + Integer.toHexString(c); + sb.append("\\u" + utf.substring(utf.length() - 4)); + } else { + sb.append(c); + } + } + } + } + + + /** + * Descends into the map along the given XPATH-style path + * and returns the object found there. + * @param path The XPATH-style path to search. Path + * components are separated by '/' characters. + * Example: /opensrf/loglevel + * @return The found object. + */ + + public static Object findPath(Map map, String path) { + String keys[] = path.split("/", -1); + int i = 0; + if(path.charAt(0) == '/') i++; + for(; i < keys.length - 1; i++ ) + map = (Map) map.get(keys[i]); + + return map.get(keys[i]); + } +} + + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/XMLFlattener.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/XMLFlattener.java new file mode 100644 index 0000000000..7abefe09fd --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/util/XMLFlattener.java @@ -0,0 +1,128 @@ +package org.opensrf.util; + +import javax.xml.stream.*; +import javax.xml.stream.events.* ; +import javax.xml.namespace.QName; +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; +import java.util.ListIterator; +import java.io.InputStream; +import org.opensrf.util.JSONWriter; +import org.opensrf.util.JSONReader; + +/** + * Flattens an XML file into a properties map. Values are stored as JSON strings or arrays. + * An array is created if more than one value resides at the same key. + * e.g. html.head.script = "alert('hello');" + */ +public class XMLFlattener { + + /** Flattened properties map */ + private Map props; + /** Incoming XML stream */ + private InputStream inStream; + /** Runtime list of encountered elements */ + private List elementList; + + /** + * Creates a new reader. Initializes the message queue. + * Sets the stream state to disconnected, and the xml + * state to in_nothing. + * @param inStream the inbound XML stream + */ + public XMLFlattener(InputStream inStream) { + props = new HashMap(); + this.inStream = inStream; + elementList = new ArrayList(); + } + + /** Turns an array of strings into a dot-separated key string */ + private String listToString() { + ListIterator itr = elementList.listIterator(); + StringBuffer sb = new StringBuffer(); + while(itr.hasNext()) { + sb.append(itr.next()); + if(itr.hasNext()) + sb.append("."); + } + return sb.toString(); + } + + /** + * Parses XML data from the provided stream. + */ + public Map read() throws javax.xml.stream.XMLStreamException { + + XMLInputFactory factory = XMLInputFactory.newInstance(); + + /** disable as many unused features as possible to speed up the parsing */ + factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.TRUE); + factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE); + factory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE); + factory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.FALSE); + factory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE); + + /** create the stream reader */ + XMLStreamReader reader = factory.createXMLStreamReader(inStream); + int eventType; + + while(reader.hasNext()) { + /** cycle through the XML events */ + + eventType = reader.next(); + if(reader.isWhiteSpace()) continue; + + switch(eventType) { + + case XMLEvent.START_ELEMENT: + elementList.add(reader.getName().toString()); + break; + + case XMLEvent.CHARACTERS: + String text = reader.getText(); + String key = listToString(); + + if(props.containsKey(key)) { + + /* something in the map already has this key */ + + Object o = null; + try { + o = new JSONReader(props.get(key)).read(); + } catch(org.opensrf.util.JSONException e){} + + if(o instanceof List) { + /* if the map contains a list, append to the list and re-encode */ + ((List) o).add(text); + + } else { + /* if the map just contains a string, start building a new list + * with the old string and append the new string */ + List arr = new ArrayList(); + arr.add((String) o); + arr.add(text); + o = arr; + } + + props.put(key, new JSONWriter(o).write()); + + } else { + props.put(key, new JSONWriter(text).write()); + } + break; + + case XMLEvent.END_ELEMENT: + elementList.remove(elementList.size()-1); + break; + } + } + + return props; + } +} + + + + diff --git a/Open-ILS/src/Android/opensrf/org/opensrf/util/XMLTransformer.java b/Open-ILS/src/Android/opensrf/org/opensrf/util/XMLTransformer.java new file mode 100644 index 0000000000..f8bc0d3fac --- /dev/null +++ b/Open-ILS/src/Android/opensrf/org/opensrf/util/XMLTransformer.java @@ -0,0 +1,59 @@ +package org.opensrf.util; +import javax.xml.transform.*; +import javax.xml.transform.stream.*; +import javax.xml.parsers.*; +import java.io.File; +import java.io.ByteArrayInputStream; +import java.io.OutputStream; +import java.io.ByteArrayOutputStream; + + +/** + * Performs XSL transformations. + * TODO: Add ability to pass in XSL variables + */ +public class XMLTransformer { + + /** The XML to transform */ + private Source xmlSource; + /** The stylesheet to apply */ + private Source xslSource; + + public XMLTransformer(Source xmlSource, Source xslSource) { + this.xmlSource = xmlSource; + this.xslSource = xslSource; + } + + public XMLTransformer(String xmlString, File xslFile) { + this( + new StreamSource(new ByteArrayInputStream(xmlString.getBytes())), + new StreamSource(xslFile)); + } + + public XMLTransformer(File xmlFile, File xslFile) { + this( + new StreamSource(xmlFile), + new StreamSource(xslFile)); + } + + /** + * Applies the transformation and puts the result into the provided output stream + */ + public void apply(OutputStream outStream) throws TransformerException, TransformerConfigurationException { + Result result = new StreamResult(outStream); + Transformer trans = TransformerFactory.newInstance().newTransformer(xslSource); + trans.transform(xmlSource, result); + } + + /** + * Applies the transformation and return the resulting string + * @return The String created by the XSL transformation + */ + public String apply() throws TransformerException, TransformerConfigurationException { + OutputStream outStream = new ByteArrayOutputStream(); + this.apply(outStream); + return outStream.toString(); + } +} + + -- 2.11.0